/* * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. */ package net.curisit.securis.utils; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * CacheTTL *

* Simple in-memory cache with per-entry TTL (time-to-live). A background * cleaning thread periodically removes expired entries. * *

Type-safety note: Besides generic getters, this cache provides * {@link #getSet(String, Class)} to safely retrieve {@code Set} values * without unchecked warnings at call sites. The method validates that all * elements match the requested {@code elementType}. * *

Threading: This implementation is lightweight and uses a single * cleaner thread. The internal map is not synchronized beyond the remove loop, * which is acceptable for low-concurrency scenarios. For heavier usage, * consider switching to a {@code ConcurrentHashMap} and/or a scheduled executor. * * @author roberto * Last reviewed by JRA on Oct 5, 2025. */ @ApplicationScoped public class CacheTTL { private static final Logger LOG = LogManager.getLogger(CacheTTL.class); /** Default TTL (seconds) for entries when not specified. */ private static final int DEFAULT_CACHE_DURATION = 24 * 60 * 60; /** Backing store: key → cached object + expiration. */ private final Map data = new HashMap<>(); /** Background cleaner thread. */ private final Thread cleaningThread; /** * CacheTTL

* Construct a cache and start the background cleaner that removes expired * entries every 60 seconds. */ @Inject public CacheTTL() { cleaningThread = new Thread(() -> { while (true) { try { // Check for expired objects every 60 seconds Thread.sleep(60_000); } catch (InterruptedException e) { LOG.warn("Cache cleaner interrupted. Clearing cache and stopping."); data.clear(); return; } Date now = new Date(); List keysToRemove = new ArrayList<>(); for (String key : data.keySet()) { CachedObject co = data.get(key); if (co != null && now.after(co.getExpireAt())) { keysToRemove.add(key); } } for (String key : keysToRemove) { // Avoid ConcurrentModificationException by removing after iteration data.remove(key); } } }, "CacheTTL-Cleaner"); cleaningThread.setDaemon(true); cleaningThread.start(); } // --------------------------------------------------------------------- // Putters // --------------------------------------------------------------------- /** * set

* Store a value with an explicit TTL. * * @param key cache key * @param obj value to store (may be any object, including collections) * @param ttl TTL in seconds */ public void set(String key, Object obj, int ttl) { Date expirationDate = new Date(System.currentTimeMillis() + (long) ttl * 1000L); data.put(key, new CachedObject(expirationDate, obj)); } /** * set

* Store a value with the default TTL. * * @param key cache key * @param obj value to store */ public void set(String key, Object obj) { set(key, obj, DEFAULT_CACHE_DURATION); } // --------------------------------------------------------------------- // Getters // --------------------------------------------------------------------- /** * get

* Retrieve a value as {@code Object}. Returns {@code null} if not present * or expired (expired entries are eagerly removed by the cleaner). * * @param key cache key * @return cached value or null */ public Object get(String key) { CachedObject co = data.get(key); return co == null ? null : co.getObject(); } /** * get

* Retrieve a value and cast it to the requested type. The cast is unchecked * due to type erasure, but localized within the cache implementation. * * @param key cache key * @param type expected value type * @param generic type * @return cached value typed or null */ public T get(String key, Class type) { CachedObject co = data.get(key); return co == null ? null : co.getObject(type); } /** * getSet

* Retrieve a {@code Set} in a type-safe way without unchecked warnings * at the call site. The method validates that the cached value is a * {@code Set} and that all elements are instances of {@code elementType}. * If any element does not match, the method returns {@code null} and logs a warning. * * @param key cache key * @param elementType class of the set elements (e.g., {@code Integer.class}) * @return typed set or null if missing/type-mismatch */ @SuppressWarnings("unchecked") public Set getSet(String key, Class elementType) { Object obj = get(key); if (obj == null) return null; if (!(obj instanceof Set raw)) { LOG.warn("Cache key '{}' expected Set<{}> but found {}", key, elementType.getSimpleName(), obj.getClass().getName()); return null; } // Validate element types to avoid ClassCastException later for (Object el : raw) { if (el != null && !elementType.isInstance(el)) { LOG.warn("Cache key '{}' contains element of type {}, expected {}", key, el.getClass().getName(), elementType.getName()); return null; } } // Safe due to element-wise validation return (Set) raw; } // --------------------------------------------------------------------- // Removers & maintenance // --------------------------------------------------------------------- /** * remove

* Remove and return a value typed. * * @param key cache key * @param type expected type * @return removed value or null */ public T remove(String key, Class type) { CachedObject co = data.remove(key); return co == null ? null : co.getObject(type); } /** * remove

* Remove and return a value as {@code Object}. * * @param key cache key * @return removed value or null */ public Object remove(String key) { CachedObject co = data.remove(key); return co == null ? null : co.getObject(); } /** * clear

* Remove all entries from the cache. */ public void clear() { data.clear(); } // --------------------------------------------------------------------- // Internal structure // --------------------------------------------------------------------- /** * CachedObject *

* Internal wrapper that pairs an arbitrary object with its expiration date. */ private static class CachedObject { private final Date expireAt; private final Object object; /** * Constructor

* Set expiration and payload. * * @param date * @param object */ public CachedObject(Date date, Object obj) { this.expireAt = date; this.object = obj; } /** * getExpireAt

* Return expiration date. * * @return date */ public Date getExpireAt() { return expireAt; } /** * getObject

* Return payload as {@code Object}. * * @return object */ public Object getObject() { return object; } /** * getObject

* Return payload cast to the requested type. Cast is localized here. * * @param type requested type * @param generic type * @return typed payload */ @SuppressWarnings("unchecked") public T getObject(Class type) { return (T) object; } } }