• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanodash / 20952016558

13 Jan 2026 09:42AM UTC coverage: 14.349% (-0.6%) from 14.953%
20952016558

push

github

tkuhn
fix(SearchPage): Search page tampered with cached list

546 of 5034 branches covered (10.85%)

Branch coverage included in aggregate %.

1501 of 9232 relevant lines covered (16.26%)

2.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

41.31
src/main/java/com/knowledgepixels/nanodash/ApiCache.java
1
package com.knowledgepixels.nanodash;
2

3
import org.nanopub.extra.services.*;
4
import org.slf4j.Logger;
5
import org.slf4j.LoggerFactory;
6

7
import java.util.HashMap;
8
import java.util.List;
9
import java.util.Map;
10
import java.util.Random;
11
import java.util.concurrent.ConcurrentHashMap;
12
import java.util.concurrent.ConcurrentMap;
13

14
/**
15
 * A utility class for caching API responses and maps to reduce redundant API calls.
16
 * This class is thread-safe and ensures that cached data is refreshed periodically.
17
 */
18
public class ApiCache {
19

20
    private ApiCache() {
21
    } // no instances allowed
22

23
    private transient static ConcurrentMap<String, ApiResponse> cachedResponses = new ConcurrentHashMap<>();
12✔
24
    private transient static ConcurrentMap<String, Integer> failed = new ConcurrentHashMap<>();
12✔
25
    private transient static ConcurrentMap<String, Map<String, String>> cachedMaps = new ConcurrentHashMap<>();
12✔
26
    private transient static ConcurrentMap<String, Long> lastRefresh = new ConcurrentHashMap<>();
12✔
27
    private transient static ConcurrentMap<String, Long> refreshStart = new ConcurrentHashMap<>();
12✔
28
    private transient static ConcurrentMap<String, Long> runAfter = new ConcurrentHashMap<>();
12✔
29
    private static final Logger logger = LoggerFactory.getLogger(ApiCache.class);
12✔
30

31
    /**
32
     * Checks if a cache refresh is currently running for the given cache ID.
33
     *
34
     * @param cacheId The unique identifier for the cache.
35
     * @return True if a refresh is running, false otherwise.
36
     */
37
    private static boolean isRunning(String cacheId) {
38
        if (!refreshStart.containsKey(cacheId)) return false;
18✔
39
        return System.currentTimeMillis() - refreshStart.get(cacheId) < 60 * 1000;
39!
40
    }
41

42
    /**
43
     * Checks if a cache refresh is currently running for the given QueryRef.
44
     *
45
     * @param queryRef The query reference
46
     * @return True if a refresh is running, false otherwise.
47
     */
48
    public static boolean isRunning(QueryRef queryRef) {
49
        return isRunning(queryRef.getAsUrlString());
12✔
50
    }
51

52
    /**
53
     * Updates the cached API response for a specific query reference.
54
     *
55
     * @param queryRef The query reference
56
     * @throws FailedApiCallException If the API call fails.
57
     */
58
    private static void updateResponse(QueryRef queryRef, boolean forced) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
59
        ApiResponse response;
60
        if (forced) {
6✔
61
            response = QueryApiAccess.forcedGet(queryRef);
12✔
62
        } else {
63
            response = QueryApiAccess.get(queryRef);
9✔
64
        }
65
        String cacheId = queryRef.getAsUrlString();
9✔
66
        logger.info("updateResponse {}", cacheId);
12✔
67
        cachedResponses.put(cacheId, response);
15✔
68
        lastRefresh.put(cacheId, System.currentTimeMillis());
18✔
69
    }
3✔
70

71
    public static ApiResponse retrieveResponseSync(QueryRef queryRef, boolean forced) {
72
        long timeNow = System.currentTimeMillis();
6✔
73
        String cacheId = queryRef.getAsUrlString();
9✔
74
        logger.info("retrieveResponseSync {}", cacheId);
12✔
75
        boolean needsRefresh = true;
6✔
76
        if (cachedResponses.containsKey(cacheId) && cachedResponses.get(cacheId) != null) {
24!
77
            long cacheAge = timeNow - lastRefresh.get(cacheId);
24✔
78
            needsRefresh = cacheAge > 60 * 1000;
18!
79
        }
80
        if (failed.get(cacheId) != null && failed.get(cacheId) > 2) {
12!
81
            failed.remove(cacheId);
×
82
            throw new RuntimeException("Query failed: " + cacheId);
×
83
        }
84
        if (needsRefresh && !isRunning(cacheId)) {
15!
85
            logger.info("Refreshing cache for {}", cacheId);
12✔
86
            refreshStart.put(cacheId, timeNow);
18✔
87
            try {
88
                if (runAfter.containsKey(cacheId)) {
12!
89
                    while (System.currentTimeMillis() < runAfter.get(cacheId)) {
×
90
                        Thread.sleep(100);
×
91
                    }
92
                    runAfter.remove(cacheId);
×
93
                }
94
                if (failed.get(cacheId) != null) {
12!
95
                    // 1 second pause between failed attempts;
96
                    Thread.sleep(1000);
×
97
                }
98
                Thread.sleep(100 + new Random().nextLong(400));
24✔
99
            } catch (InterruptedException ex) {
×
100
                logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
101
            }
3✔
102
            try {
103
                ApiCache.updateResponse(queryRef, forced);
9✔
104
            } catch (Exception ex) {
×
105
                logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
106
                cachedResponses.remove(cacheId);
×
107
                if (failed.get(cacheId) == null) {
×
108
                    failed.put(cacheId, 1);
×
109
                } else {
110
                    failed.put(cacheId, failed.get(cacheId) + 1);
×
111
                }
112
                lastRefresh.put(cacheId, System.currentTimeMillis());
×
113
            } finally {
114
                refreshStart.remove(cacheId);
12✔
115
            }
116
        }
117
        if (cachedResponses.containsKey(cacheId)) {
12!
118
            return cachedResponses.get(cacheId);
15✔
119
        } else {
120
            return null;
×
121
        }
122
    }
123

124
    /**
125
     * Retrieves a cached API response for a specific QueryRef.
126
     *
127
     * @param queryRef The QueryRef object containing the query name and parameters.
128
     * @return The cached API response, or null if not cached.
129
     */
130
    public static ApiResponse retrieveResponseAsync(QueryRef queryRef) {
131
        long timeNow = System.currentTimeMillis();
6✔
132
        String cacheId = queryRef.getAsUrlString();
9✔
133
        boolean isCached = false;
6✔
134
        boolean needsRefresh = true;
6✔
135
        if (cachedResponses.containsKey(cacheId) && cachedResponses.get(cacheId) != null) {
24!
136
            long cacheAge = timeNow - lastRefresh.get(cacheId);
24✔
137
            isCached = cacheAge < 24 * 60 * 60 * 1000;
21!
138
            needsRefresh = cacheAge > 60 * 1000;
18!
139
        }
140
        if (failed.get(cacheId) != null && failed.get(cacheId) > 2) {
12!
141
            failed.remove(cacheId);
×
142
            throw new RuntimeException("Query failed: " + cacheId);
×
143
        }
144
        if (needsRefresh && !isRunning(cacheId)) {
15!
145
            refreshStart.put(cacheId, timeNow);
18✔
146
            new Thread(() -> {
18✔
147
                try {
148
                    if (runAfter.containsKey(cacheId)) {
12!
149
                        while (System.currentTimeMillis() < runAfter.get(cacheId)) {
×
150
                            Thread.sleep(100);
×
151
                        }
152
                        runAfter.remove(cacheId);
×
153
                    }
154
                    if (failed.get(cacheId) != null) {
12!
155
                        // 1 second pause between failed attempts;
156
                        Thread.sleep(1000);
×
157
                    }
158
                    Thread.sleep(100 + new Random().nextLong(400));
24✔
159
                } catch (InterruptedException ex) {
×
160
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
161
                }
3✔
162
                try {
163
                    ApiCache.updateResponse(queryRef, false);
9✔
164
                } catch (Exception ex) {
×
165
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
166
                    cachedResponses.remove(cacheId);
×
167
                    if (failed.get(cacheId) == null) {
×
168
                        failed.put(cacheId, 1);
×
169
                    } else {
170
                        failed.put(cacheId, failed.get(cacheId) + 1);
×
171
                    }
172
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
173
                } finally {
174
                    refreshStart.remove(cacheId);
12✔
175
                }
176
            }).start();
6✔
177
        }
178
        if (isCached) {
6✔
179
            return cachedResponses.get(cacheId);
15✔
180
        } else {
181
            return null;
6✔
182
        }
183
    }
184

185
    /**
186
     * Updates the cached map for a specific query reference.
187
     *
188
     * @param queryRef The query reference
189
     * @throws FailedApiCallException If the API call fails.
190
     */
191
    private static void updateMap(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
192
        Map<String, String> map = new HashMap<>();
×
193
        List<ApiResponseEntry> respList = QueryApiAccess.get(queryRef).getData();
×
194
        while (respList != null && !respList.isEmpty()) {
×
195
            ApiResponseEntry resultEntry = respList.remove(0);
×
196
            map.put(resultEntry.get("key"), resultEntry.get("value"));
×
197
        }
×
198
        String cacheId = queryRef.getAsUrlString();
×
199
        cachedMaps.put(cacheId, map);
×
200
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
201
    }
×
202

203
    /**
204
     * Retrieves a cached map for a specific query reference.
205
     * If the cache is stale, it triggers a background refresh.
206
     *
207
     * @param queryRef The query reference
208
     * @return The cached map, or null if not cached.
209
     */
210
    public static synchronized Map<String, String> retrieveMap(QueryRef queryRef) {
211
        long timeNow = System.currentTimeMillis();
×
212
        String cacheId = queryRef.getAsUrlString();
×
213
        boolean isCached = false;
×
214
        boolean needsRefresh = true;
×
215
        if (cachedMaps.containsKey(cacheId)) {
×
216
            long cacheAge = timeNow - lastRefresh.get(cacheId);
×
217
            isCached = cacheAge < 24 * 60 * 60 * 1000;
×
218
            needsRefresh = cacheAge > 60 * 1000;
×
219
        }
220
        if (needsRefresh && !isRunning(cacheId)) {
×
221
            refreshStart.put(cacheId, timeNow);
×
222
            new Thread(() -> {
×
223
                try {
224
                    if (runAfter.containsKey(cacheId)) {
×
225
                        while (System.currentTimeMillis() < runAfter.get(cacheId)) {
×
226
                            Thread.sleep(100);
×
227
                        }
228
                        runAfter.remove(cacheId);
×
229
                    }
230
                    Thread.sleep(100 + new Random().nextLong(400));
×
231
                } catch (InterruptedException ex) {
×
232
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
233
                }
×
234
                try {
235
                    ApiCache.updateMap(queryRef);
×
236
                } catch (Exception ex) {
×
237
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
238
                    cachedResponses.put(cacheId, null);
×
239
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
240
                } finally {
241
                    refreshStart.remove(cacheId);
×
242
                }
243
            }).start();
×
244
        }
245
        if (isCached) {
×
246
            if (cachedResponses.get(cacheId) == null) {
×
247
                cachedResponses.remove(cacheId);
×
248
                throw new RuntimeException("Query failed: " + cacheId);
×
249
            }
250
            return cachedMaps.get(cacheId);
×
251
        } else {
252
            return null;
×
253
        }
254
    }
255

256
    public static void clearCache(QueryRef queryRef, long waitMillis) {
257
        cachedResponses.remove(queryRef.getAsUrlString());
×
258
        runAfter.put(queryRef.getAsUrlString(), System.currentTimeMillis() + waitMillis);
×
259
    }
×
260

261
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc