Joaquín Reñé
2025-10-07 146a0fb8b0e90f9196e569152f649baf60d6cc8f
securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
....@@ -1,3 +1,6 @@
1
+/*
2
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
3
+*/
14 package net.curisit.securis.utils;
25
36 import java.util.ArrayList;
....@@ -5,6 +8,7 @@
58 import java.util.HashMap;
69 import java.util.List;
710 import java.util.Map;
11
+import java.util.Set;
812
913 import jakarta.enterprise.context.ApplicationScoped;
1014 import jakarta.inject.Inject;
....@@ -13,122 +17,258 @@
1317 import org.apache.logging.log4j.Logger;
1418
1519 /**
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
+*/
2138 @ApplicationScoped
2239 public class CacheTTL {
2340
2441 private static final Logger LOG = LogManager.getLogger(CacheTTL.class);
2542
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
+
2652 /**
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
+ */
3557 @Inject
3658 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);
6375 }
6476 }
77
+ for (String key : keysToRemove) {
78
+ // Avoid ConcurrentModificationException by removing after iteration
79
+ data.remove(key);
80
+ }
6581 }
66
- });
82
+ }, "CacheTTL-Cleaner");
83
+ cleaningThread.setDaemon(true);
6784 cleaningThread.start();
6885 }
6986
87
+ // ---------------------------------------------------------------------
88
+ // Putters
89
+ // ---------------------------------------------------------------------
90
+
7091 /**
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
+ */
7799 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);
79101 data.put(key, new CachedObject(expirationDate, obj));
80102 }
81103
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
+ */
82111 public void set(String key, Object obj) {
83112 set(key, obj, DEFAULT_CACHE_DURATION);
84113 }
85114
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
+ */
86127 public Object get(String key) {
87128 CachedObject co = data.get(key);
88129 return co == null ? null : co.getObject();
89130 }
90131
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
+ */
91142 public <T> T get(String key, Class<T> type) {
92143 CachedObject co = data.get(key);
93144 return co == null ? null : co.getObject(type);
94145 }
95146
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
+ */
96190 public <T> T remove(String key, Class<T> type) {
97191 CachedObject co = data.remove(key);
98192 return co == null ? null : co.getObject(type);
99193 }
100194
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
+ */
101202 public Object remove(String key) {
102203 CachedObject co = data.remove(key);
103204 return co == null ? null : co.getObject();
104205 }
105206
207
+ /**
208
+ * clear<p>
209
+ * Remove all entries from the cache.
210
+ */
106211 public void clear() {
107212 data.clear();
108213 }
109214
110
- private class CachedObject {
111
- Date expireAt;
112
- Object object;
215
+ // ---------------------------------------------------------------------
216
+ // Internal structure
217
+ // ---------------------------------------------------------------------
113218
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
+ */
114235 public CachedObject(Date date, Object obj) {
115
- expireAt = date;
116
- object = obj;
236
+ this.expireAt = date;
237
+ this.object = obj;
117238 }
118239
240
+ /**
241
+ * getExpireAt<p>
242
+ * Return expiration date.
243
+ *
244
+ * @return date
245
+ */
119246 public Date getExpireAt() {
120247 return expireAt;
121248 }
122249
250
+ /**
251
+ * getObject<p>
252
+ * Return payload as {@code Object}.
253
+ *
254
+ * @return object
255
+ */
123256 public Object getObject() {
124257 return object;
125258 }
126259
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
+ */
127268 @SuppressWarnings("unchecked")
128269 public <T> T getObject(Class<T> type) {
129270 return (T) object;
130271 }
131
-
132272 }
133
-
134273 }
274
+