| .. | .. |
|---|
| 1 | +/* |
|---|
| 2 | +* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. |
|---|
| 3 | +*/ |
|---|
| 1 | 4 | package net.curisit.securis.utils; |
|---|
| 2 | 5 | |
|---|
| 3 | 6 | import java.util.ArrayList; |
|---|
| .. | .. |
|---|
| 5 | 8 | import java.util.HashMap; |
|---|
| 6 | 9 | import java.util.List; |
|---|
| 7 | 10 | import java.util.Map; |
|---|
| 11 | +import java.util.Set; |
|---|
| 8 | 12 | |
|---|
| 9 | 13 | import jakarta.enterprise.context.ApplicationScoped; |
|---|
| 10 | 14 | import jakarta.inject.Inject; |
|---|
| .. | .. |
|---|
| 13 | 17 | import org.apache.logging.log4j.Logger; |
|---|
| 14 | 18 | |
|---|
| 15 | 19 | /** |
|---|
| 16 | | - * Cache implementation with TTL (time To Live) The objects are removed from |
|---|
| 17 | | - * cache when TTL is reached. |
|---|
| 18 | | - * |
|---|
| 19 | | - * @author roberto <roberto.sanchez@curisit.net> |
|---|
| 20 | | - */ |
|---|
| 20 | +* CacheTTL |
|---|
| 21 | +* <p> |
|---|
| 22 | +* Simple in-memory cache with per-entry TTL (time-to-live). A background |
|---|
| 23 | +* cleaning thread periodically removes expired entries. |
|---|
| 24 | +* |
|---|
| 25 | +* <p><b>Type-safety note:</b> Besides generic getters, this cache provides |
|---|
| 26 | +* {@link #getSet(String, Class)} to safely retrieve {@code Set<E>} values |
|---|
| 27 | +* without unchecked warnings at call sites. The method validates that all |
|---|
| 28 | +* elements match the requested {@code elementType}. |
|---|
| 29 | +* |
|---|
| 30 | +* <p><b>Threading:</b> This implementation is lightweight and uses a single |
|---|
| 31 | +* cleaner thread. The internal map is not synchronized beyond the remove loop, |
|---|
| 32 | +* which is acceptable for low-concurrency scenarios. For heavier usage, |
|---|
| 33 | +* consider switching to a {@code ConcurrentHashMap} and/or a scheduled executor. |
|---|
| 34 | +* |
|---|
| 35 | +* @author roberto |
|---|
| 36 | +* Last reviewed by JRA on Oct 5, 2025. |
|---|
| 37 | +*/ |
|---|
| 21 | 38 | @ApplicationScoped |
|---|
| 22 | 39 | public class CacheTTL { |
|---|
| 23 | 40 | |
|---|
| 24 | 41 | private static final Logger LOG = LogManager.getLogger(CacheTTL.class); |
|---|
| 25 | 42 | |
|---|
| 43 | + /** Default TTL (seconds) for entries when not specified. */ |
|---|
| 44 | + private static final int DEFAULT_CACHE_DURATION = 24 * 60 * 60; |
|---|
| 45 | + |
|---|
| 46 | + /** Backing store: key → cached object + expiration. */ |
|---|
| 47 | + private final Map<String, CachedObject> data = new HashMap<>(); |
|---|
| 48 | + |
|---|
| 49 | + /** Background cleaner thread. */ |
|---|
| 50 | + private final Thread cleaningThread; |
|---|
| 51 | + |
|---|
| 26 | 52 | /** |
|---|
| 27 | | - * Period before token expires, set in seconds. |
|---|
| 28 | | - */ |
|---|
| 29 | | - private static int DEFAULT_CACHE_DURATION = 24 * 60 * 60; |
|---|
| 30 | | - |
|---|
| 31 | | - private Map<String, CachedObject> data = new HashMap<>(); |
|---|
| 32 | | - |
|---|
| 33 | | - private Thread cleaningThread = null; |
|---|
| 34 | | - |
|---|
| 53 | + * CacheTTL<p> |
|---|
| 54 | + * Construct a cache and start the background cleaner that removes expired |
|---|
| 55 | + * entries every 60 seconds. |
|---|
| 56 | + */ |
|---|
| 35 | 57 | @Inject |
|---|
| 36 | 58 | public CacheTTL() { |
|---|
| 37 | | - cleaningThread = new Thread(new Runnable() { |
|---|
| 38 | | - |
|---|
| 39 | | - @Override |
|---|
| 40 | | - public void run() { |
|---|
| 41 | | - while (CacheTTL.this.data != null) { |
|---|
| 42 | | - try { |
|---|
| 43 | | - // We check for expired object every 60 seconds |
|---|
| 44 | | - Thread.sleep(60 * 1000); |
|---|
| 45 | | - } catch (InterruptedException e) { |
|---|
| 46 | | - LOG.error("Exiting from Cache Thread"); |
|---|
| 47 | | - data.clear(); |
|---|
| 48 | | - return; |
|---|
| 49 | | - } |
|---|
| 50 | | - Date now = new Date(); |
|---|
| 51 | | - List<String> keysToRemove = new ArrayList<>(); |
|---|
| 52 | | - for (String key : CacheTTL.this.data.keySet()) { |
|---|
| 53 | | - CachedObject co = CacheTTL.this.data.get(key); |
|---|
| 54 | | - if (now.after(co.getExpireAt())) { |
|---|
| 55 | | - keysToRemove.add(key); |
|---|
| 56 | | - } |
|---|
| 57 | | - } |
|---|
| 58 | | - for (String key : keysToRemove) { |
|---|
| 59 | | - // If we try to remove directly in the previous loop an |
|---|
| 60 | | - // exception is thrown |
|---|
| 61 | | - // java.util.ConcurrentModificationException |
|---|
| 62 | | - CacheTTL.this.data.remove(key); |
|---|
| 59 | + cleaningThread = new Thread(() -> { |
|---|
| 60 | + while (true) { |
|---|
| 61 | + try { |
|---|
| 62 | + // Check for expired objects every 60 seconds |
|---|
| 63 | + Thread.sleep(60_000); |
|---|
| 64 | + } catch (InterruptedException e) { |
|---|
| 65 | + LOG.warn("Cache cleaner interrupted. Clearing cache and stopping."); |
|---|
| 66 | + data.clear(); |
|---|
| 67 | + return; |
|---|
| 68 | + } |
|---|
| 69 | + Date now = new Date(); |
|---|
| 70 | + List<String> keysToRemove = new ArrayList<>(); |
|---|
| 71 | + for (String key : data.keySet()) { |
|---|
| 72 | + CachedObject co = data.get(key); |
|---|
| 73 | + if (co != null && now.after(co.getExpireAt())) { |
|---|
| 74 | + keysToRemove.add(key); |
|---|
| 63 | 75 | } |
|---|
| 64 | 76 | } |
|---|
| 77 | + for (String key : keysToRemove) { |
|---|
| 78 | + // Avoid ConcurrentModificationException by removing after iteration |
|---|
| 79 | + data.remove(key); |
|---|
| 80 | + } |
|---|
| 65 | 81 | } |
|---|
| 66 | | - }); |
|---|
| 82 | + }, "CacheTTL-Cleaner"); |
|---|
| 83 | + cleaningThread.setDaemon(true); |
|---|
| 67 | 84 | cleaningThread.start(); |
|---|
| 68 | 85 | } |
|---|
| 69 | 86 | |
|---|
| 87 | + // --------------------------------------------------------------------- |
|---|
| 88 | + // Putters |
|---|
| 89 | + // --------------------------------------------------------------------- |
|---|
| 90 | + |
|---|
| 70 | 91 | /** |
|---|
| 71 | | - * |
|---|
| 72 | | - * @param key |
|---|
| 73 | | - * @param obj |
|---|
| 74 | | - * @param ttl |
|---|
| 75 | | - * Time To Live in seconds |
|---|
| 76 | | - */ |
|---|
| 92 | + * set<p> |
|---|
| 93 | + * Store a value with an explicit TTL. |
|---|
| 94 | + * |
|---|
| 95 | + * @param key cache key |
|---|
| 96 | + * @param obj value to store (may be any object, including collections) |
|---|
| 97 | + * @param ttl TTL in seconds |
|---|
| 98 | + */ |
|---|
| 77 | 99 | public void set(String key, Object obj, int ttl) { |
|---|
| 78 | | - Date expirationDate = new Date(new Date().getTime() + ttl * 1000); |
|---|
| 100 | + Date expirationDate = new Date(System.currentTimeMillis() + (long) ttl * 1000L); |
|---|
| 79 | 101 | data.put(key, new CachedObject(expirationDate, obj)); |
|---|
| 80 | 102 | } |
|---|
| 81 | 103 | |
|---|
| 104 | + /** |
|---|
| 105 | + * set<p> |
|---|
| 106 | + * Store a value with the default TTL. |
|---|
| 107 | + * |
|---|
| 108 | + * @param key cache key |
|---|
| 109 | + * @param obj value to store |
|---|
| 110 | + */ |
|---|
| 82 | 111 | public void set(String key, Object obj) { |
|---|
| 83 | 112 | set(key, obj, DEFAULT_CACHE_DURATION); |
|---|
| 84 | 113 | } |
|---|
| 85 | 114 | |
|---|
| 115 | + // --------------------------------------------------------------------- |
|---|
| 116 | + // Getters |
|---|
| 117 | + // --------------------------------------------------------------------- |
|---|
| 118 | + |
|---|
| 119 | + /** |
|---|
| 120 | + * get<p> |
|---|
| 121 | + * Retrieve a value as {@code Object}. Returns {@code null} if not present |
|---|
| 122 | + * or expired (expired entries are eagerly removed by the cleaner). |
|---|
| 123 | + * |
|---|
| 124 | + * @param key cache key |
|---|
| 125 | + * @return cached value or null |
|---|
| 126 | + */ |
|---|
| 86 | 127 | public Object get(String key) { |
|---|
| 87 | 128 | CachedObject co = data.get(key); |
|---|
| 88 | 129 | return co == null ? null : co.getObject(); |
|---|
| 89 | 130 | } |
|---|
| 90 | 131 | |
|---|
| 132 | + /** |
|---|
| 133 | + * get<p> |
|---|
| 134 | + * Retrieve a value and cast it to the requested type. The cast is unchecked |
|---|
| 135 | + * due to type erasure, but localized within the cache implementation. |
|---|
| 136 | + * |
|---|
| 137 | + * @param key cache key |
|---|
| 138 | + * @param type expected value type |
|---|
| 139 | + * @param <T> generic type |
|---|
| 140 | + * @return cached value typed or null |
|---|
| 141 | + */ |
|---|
| 91 | 142 | public <T> T get(String key, Class<T> type) { |
|---|
| 92 | 143 | CachedObject co = data.get(key); |
|---|
| 93 | 144 | return co == null ? null : co.getObject(type); |
|---|
| 94 | 145 | } |
|---|
| 95 | 146 | |
|---|
| 147 | + /** |
|---|
| 148 | + * getSet<p> |
|---|
| 149 | + * Retrieve a {@code Set<E>} in a type-safe way without unchecked warnings |
|---|
| 150 | + * at the call site. The method validates that the cached value is a |
|---|
| 151 | + * {@code Set} and that <b>all</b> elements are instances of {@code elementType}. |
|---|
| 152 | + * If any element does not match, the method returns {@code null} and logs a warning. |
|---|
| 153 | + * |
|---|
| 154 | + * @param key cache key |
|---|
| 155 | + * @param elementType class of the set elements (e.g., {@code Integer.class}) |
|---|
| 156 | + * @return typed set or null if missing/type-mismatch |
|---|
| 157 | + */ |
|---|
| 158 | + @SuppressWarnings("unchecked") |
|---|
| 159 | + public <E> Set<E> getSet(String key, Class<E> elementType) { |
|---|
| 160 | + Object obj = get(key); |
|---|
| 161 | + if (obj == null) return null; |
|---|
| 162 | + if (!(obj instanceof Set<?> raw)) { |
|---|
| 163 | + LOG.warn("Cache key '{}' expected Set<{}> but found {}", key, elementType.getSimpleName(), obj.getClass().getName()); |
|---|
| 164 | + return null; |
|---|
| 165 | + } |
|---|
| 166 | + // Validate element types to avoid ClassCastException later |
|---|
| 167 | + for (Object el : raw) { |
|---|
| 168 | + if (el != null && !elementType.isInstance(el)) { |
|---|
| 169 | + LOG.warn("Cache key '{}' contains element of type {}, expected {}", key, |
|---|
| 170 | + el.getClass().getName(), elementType.getName()); |
|---|
| 171 | + return null; |
|---|
| 172 | + } |
|---|
| 173 | + } |
|---|
| 174 | + // Safe due to element-wise validation |
|---|
| 175 | + return (Set<E>) raw; |
|---|
| 176 | + } |
|---|
| 177 | + |
|---|
| 178 | + // --------------------------------------------------------------------- |
|---|
| 179 | + // Removers & maintenance |
|---|
| 180 | + // --------------------------------------------------------------------- |
|---|
| 181 | + |
|---|
| 182 | + /** |
|---|
| 183 | + * remove<p> |
|---|
| 184 | + * Remove and return a value typed. |
|---|
| 185 | + * |
|---|
| 186 | + * @param key cache key |
|---|
| 187 | + * @param type expected type |
|---|
| 188 | + * @return removed value or null |
|---|
| 189 | + */ |
|---|
| 96 | 190 | public <T> T remove(String key, Class<T> type) { |
|---|
| 97 | 191 | CachedObject co = data.remove(key); |
|---|
| 98 | 192 | return co == null ? null : co.getObject(type); |
|---|
| 99 | 193 | } |
|---|
| 100 | 194 | |
|---|
| 195 | + /** |
|---|
| 196 | + * remove<p> |
|---|
| 197 | + * Remove and return a value as {@code Object}. |
|---|
| 198 | + * |
|---|
| 199 | + * @param key cache key |
|---|
| 200 | + * @return removed value or null |
|---|
| 201 | + */ |
|---|
| 101 | 202 | public Object remove(String key) { |
|---|
| 102 | 203 | CachedObject co = data.remove(key); |
|---|
| 103 | 204 | return co == null ? null : co.getObject(); |
|---|
| 104 | 205 | } |
|---|
| 105 | 206 | |
|---|
| 207 | + /** |
|---|
| 208 | + * clear<p> |
|---|
| 209 | + * Remove all entries from the cache. |
|---|
| 210 | + */ |
|---|
| 106 | 211 | public void clear() { |
|---|
| 107 | 212 | data.clear(); |
|---|
| 108 | 213 | } |
|---|
| 109 | 214 | |
|---|
| 110 | | - private class CachedObject { |
|---|
| 111 | | - Date expireAt; |
|---|
| 112 | | - Object object; |
|---|
| 215 | + // --------------------------------------------------------------------- |
|---|
| 216 | + // Internal structure |
|---|
| 217 | + // --------------------------------------------------------------------- |
|---|
| 113 | 218 | |
|---|
| 219 | + /** |
|---|
| 220 | + * CachedObject |
|---|
| 221 | + * <p> |
|---|
| 222 | + * Internal wrapper that pairs an arbitrary object with its expiration date. |
|---|
| 223 | + */ |
|---|
| 224 | + private static class CachedObject { |
|---|
| 225 | + private final Date expireAt; |
|---|
| 226 | + private final Object object; |
|---|
| 227 | + |
|---|
| 228 | + /** |
|---|
| 229 | + * Constructor<p> |
|---|
| 230 | + * Set expiration and payload. |
|---|
| 231 | + * |
|---|
| 232 | + * @param date |
|---|
| 233 | + * @param object |
|---|
| 234 | + */ |
|---|
| 114 | 235 | public CachedObject(Date date, Object obj) { |
|---|
| 115 | | - expireAt = date; |
|---|
| 116 | | - object = obj; |
|---|
| 236 | + this.expireAt = date; |
|---|
| 237 | + this.object = obj; |
|---|
| 117 | 238 | } |
|---|
| 118 | 239 | |
|---|
| 240 | + /** |
|---|
| 241 | + * getExpireAt<p> |
|---|
| 242 | + * Return expiration date. |
|---|
| 243 | + * |
|---|
| 244 | + * @return date |
|---|
| 245 | + */ |
|---|
| 119 | 246 | public Date getExpireAt() { |
|---|
| 120 | 247 | return expireAt; |
|---|
| 121 | 248 | } |
|---|
| 122 | 249 | |
|---|
| 250 | + /** |
|---|
| 251 | + * getObject<p> |
|---|
| 252 | + * Return payload as {@code Object}. |
|---|
| 253 | + * |
|---|
| 254 | + * @return object |
|---|
| 255 | + */ |
|---|
| 123 | 256 | public Object getObject() { |
|---|
| 124 | 257 | return object; |
|---|
| 125 | 258 | } |
|---|
| 126 | 259 | |
|---|
| 260 | + /** |
|---|
| 261 | + * getObject<p> |
|---|
| 262 | + * Return payload cast to the requested type. Cast is localized here. |
|---|
| 263 | + * |
|---|
| 264 | + * @param type requested type |
|---|
| 265 | + * @param <T> generic type |
|---|
| 266 | + * @return typed payload |
|---|
| 267 | + */ |
|---|
| 127 | 268 | @SuppressWarnings("unchecked") |
|---|
| 128 | 269 | public <T> T getObject(Class<T> type) { |
|---|
| 129 | 270 | return (T) object; |
|---|
| 130 | 271 | } |
|---|
| 131 | | - |
|---|
| 132 | 272 | } |
|---|
| 133 | | - |
|---|
| 134 | 273 | } |
|---|
| 274 | + |
|---|