Joaquín Reñé
2025-10-07 146a0fb8b0e90f9196e569152f649baf60d6cc8f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
/*
* 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
* <p>
* Simple in-memory cache with per-entry TTL (time-to-live). A background
* cleaning thread periodically removes expired entries.
*
* <p><b>Type-safety note:</b> Besides generic getters, this cache provides
* {@link #getSet(String, Class)} to safely retrieve {@code Set<E>} values
* without unchecked warnings at call sites. The method validates that all
* elements match the requested {@code elementType}.
*
* <p><b>Threading:</b> 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<String, CachedObject> data = new HashMap<>();
    /** Background cleaner thread. */
    private final Thread cleaningThread;
    /**
    * CacheTTL<p>
    * 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<String> 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<p>
    * 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<p>
    * 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<p>
    * 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<p>
    * 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 <T> generic type
    * @return cached value typed or null
    */
    public <T> T get(String key, Class<T> type) {
        CachedObject co = data.get(key);
        return co == null ? null : co.getObject(type);
    }
    /**
    * getSet<p>
    * Retrieve a {@code Set<E>} 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 <b>all</b> 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 <E> Set<E> getSet(String key, Class<E> 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<E>) raw;
    }
    // ---------------------------------------------------------------------
    // Removers & maintenance
    // ---------------------------------------------------------------------
    /**
    * remove<p>
    * Remove and return a value typed.
    *
    * @param key cache key
    * @param type expected type
    * @return removed value or null
    */
    public <T> T remove(String key, Class<T> type) {
        CachedObject co = data.remove(key);
        return co == null ? null : co.getObject(type);
    }
    /**
    * remove<p>
    * 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<p>
    * Remove all entries from the cache.
    */
    public void clear() {
        data.clear();
    }
    // ---------------------------------------------------------------------
    // Internal structure
    // ---------------------------------------------------------------------
    /**
    * CachedObject
    * <p>
    * Internal wrapper that pairs an arbitrary object with its expiration date.
    */
    private static class CachedObject {
        private final Date expireAt;
        private final Object object;
        /** 
         * Constructor<p>
         * Set expiration and payload. 
         * 
         * @param date
         * @param object
         */
        public CachedObject(Date date, Object obj) {
            this.expireAt = date;
            this.object = obj;
        }
        /** 
         * getExpireAt<p>
         * Return expiration date. 
         * 
         * @return date
         */
        public Date getExpireAt() {
            return expireAt;
        }
        /** 
         * getObject<p>
         * Return payload as {@code Object}. 
         * 
         * @return object
         */
        public Object getObject() {
            return object;
        }
        /**
        * getObject<p>
        * Return payload cast to the requested type. Cast is localized here.
        *
        * @param type requested type
        * @param <T>  generic type
        * @return typed payload
        */
        @SuppressWarnings("unchecked")
        public <T> T getObject(Class<T> type) {
            return (T) object;
        }
    }
}