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

knowledgepixels / nanodash / 17584062572

09 Sep 2025 01:22PM UTC coverage: 13.651% (+0.09%) from 13.561%
17584062572

push

github

tkuhn
Add ListPage

407 of 3868 branches covered (10.52%)

Branch coverage included in aggregate %.

1085 of 7062 relevant lines covered (15.36%)

0.68 hits per line

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

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

3
import org.nanopub.extra.services.APINotReachableException;
4
import org.nanopub.extra.services.ApiResponse;
5
import org.nanopub.extra.services.ApiResponseEntry;
6
import org.nanopub.extra.services.FailedApiCallException;
7
import org.nanopub.extra.services.NotEnoughAPIInstancesException;
8
import org.slf4j.Logger;
9
import org.slf4j.LoggerFactory;
10

11
import java.util.*;
12
import java.util.concurrent.ConcurrentHashMap;
13
import java.util.concurrent.ConcurrentMap;
14

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

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

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

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

41
    public static boolean isRunning(QueryRef queryRef) {
42
        return isRunning(queryRef.getName(), queryRef.getParams());
×
43
    }
44

45
    /**
46
     * Checks if a cache refresh is running for a specific query and parameters.
47
     *
48
     * @param queryName The name of the query.
49
     * @param params    The parameters for the query.
50
     * @return True if a refresh is running, false otherwise.
51
     */
52
    public static boolean isRunning(String queryName, Map<String, String> params) {
53
        return isRunning(getCacheId(queryName, params));
×
54
    }
55

56
    /**
57
     * Checks if a cache refresh is running for a specific query and a single parameter.
58
     *
59
     * @param queryName  The name of the query.
60
     * @param paramName  The name of the parameter.
61
     * @param paramValue The value of the parameter.
62
     * @return True if a refresh is running, false otherwise.
63
     */
64
    public static boolean isRunning(String queryName, String paramName, String paramValue) {
65
        Map<String, String> params = new HashMap<>();
×
66
        params.put(paramName, paramValue);
×
67
        return isRunning(getCacheId(queryName, params));
×
68
    }
69

70
    /**
71
     * Updates the cached API response for a specific query and parameters.
72
     *
73
     * @param queryName The name of the query.
74
     * @param params    The parameters for the query.
75
     * @throws FailedApiCallException If the API call fails.
76
     */
77
    private static void updateResponse(String queryName, Map<String, String> params) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
78
        Map<String, String> nanopubParams = new HashMap<>();
×
79
        for (String k : params.keySet()) nanopubParams.put(k, params.get(k));
×
80
        ApiResponse response = QueryApiAccess.get(queryName, nanopubParams);
×
81
        String cacheId = getCacheId(queryName, params);
×
82
        cachedResponses.put(cacheId, response);
×
83
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
84
    }
×
85

86
    /**
87
     * Retrieves a cached API response for a specific query and a single parameter.
88
     *
89
     * @param queryName  The name of the query.
90
     * @param paramName  The name of the parameter.
91
     * @param paramValue The value of the parameter.
92
     * @return The cached API response, or null if not cached.
93
     */
94
    public static ApiResponse retrieveResponse(String queryName, String paramName, String paramValue) {
95
        Map<String, String> params = new HashMap<>();
×
96
        params.put(paramName, paramValue);
×
97
        return retrieveResponse(queryName, params);
×
98
    }
99

100
    public static ApiResponse retrieveResponse(QueryRef queryRef) {
101
        return retrieveResponse(queryRef.getName(), queryRef.getParams());
×
102
    }
103

104
    /**
105
     * Retrieves a cached API response for a specific query and parameters.
106
     * If the cache is stale, it triggers a background refresh.
107
     *
108
     * @param queryName The name of the query.
109
     * @param params    The parameters for the query.
110
     * @return The cached API response, or null if not cached.
111
     */
112
    public static synchronized ApiResponse retrieveResponse(final String queryName, final Map<String, String> params) {
113
        long timeNow = System.currentTimeMillis();
×
114
        String cacheId = getCacheId(queryName, params);
×
115
        boolean isCached = false;
×
116
        boolean needsRefresh = true;
×
117
        if (cachedResponses.containsKey(cacheId) && cachedResponses.get(cacheId) != null) {
×
118
            long cacheAge = timeNow - lastRefresh.get(cacheId);
×
119
            isCached = cacheAge < 24 * 60 * 60 * 1000;
×
120
            needsRefresh = cacheAge > 60 * 1000;
×
121
        }
122
        if (needsRefresh && !isRunning(cacheId)) {
×
123
            refreshStart.put(cacheId, timeNow);
×
124
            new Thread(() -> {
×
125
                try {
126
                    Thread.sleep(100 + new Random().nextLong(400));
×
127
                } catch (InterruptedException ex) {
×
128
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
129
                }
×
130
                try {
131
                    ApiCache.updateResponse(queryName, params);
×
132
                } catch (Exception ex) {
×
133
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
134
                    cachedResponses.put(cacheId, null);
×
135
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
136
                } finally {
137
                    refreshStart.remove(cacheId);
×
138
                }
139
            }).start();
×
140
        }
141
        if (isCached) {
×
142
            if (cachedResponses.get(cacheId) == null) {
×
143
                cachedResponses.remove(cacheId);
×
144
                throw new RuntimeException("Query failed: " + cacheId);
×
145
            }
146
            return cachedResponses.get(cacheId);
×
147
        } else {
148
            return null;
×
149
        }
150
    }
151

152
    /**
153
     * Updates the cached map for a specific query and parameters.
154
     *
155
     * @param queryName The name of the query.
156
     * @param params    The parameters for the query.
157
     * @throws FailedApiCallException If the API call fails.
158
     */
159
    private static void updateMap(String queryName, Map<String, String> params) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
160
        Map<String, String> map = new HashMap<>();
×
161
        Map<String, String> nanopubParams = new HashMap<>();
×
162
        for (String k : params.keySet()) nanopubParams.put(k, params.get(k));
×
163
        List<ApiResponseEntry> respList = QueryApiAccess.get(queryName, nanopubParams).getData();
×
164
        while (respList != null && !respList.isEmpty()) {
×
165
            ApiResponseEntry resultEntry = respList.remove(0);
×
166
            map.put(resultEntry.get("key"), resultEntry.get("value"));
×
167
        }
×
168
        String cacheId = getCacheId(queryName, params);
×
169
        cachedMaps.put(cacheId, map);
×
170
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
171
    }
×
172

173
    /**
174
     * Retrieves a cached map for a specific query and parameters.
175
     * If the cache is stale, it triggers a background refresh.
176
     *
177
     * @param queryName The name of the query.
178
     * @param params    The parameters for the query.
179
     * @return The cached map, or null if not cached.
180
     */
181
    public static synchronized Map<String, String> retrieveMap(String queryName, Map<String, String> params) {
182
        long timeNow = System.currentTimeMillis();
×
183
        String cacheId = getCacheId(queryName, params);
×
184
        boolean isCached = false;
×
185
        boolean needsRefresh = true;
×
186
        if (cachedMaps.containsKey(cacheId)) {
×
187
            long cacheAge = timeNow - lastRefresh.get(cacheId);
×
188
            isCached = cacheAge < 24 * 60 * 60 * 1000;
×
189
            needsRefresh = cacheAge > 60 * 1000;
×
190
        }
191
        if (needsRefresh && !isRunning(cacheId)) {
×
192
            refreshStart.put(cacheId, timeNow);
×
193
            new Thread(() -> {
×
194
                try {
195
                    Thread.sleep(100 + new Random().nextLong(400));
×
196
                } catch (InterruptedException ex) {
×
197
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
198
                }
×
199
                try {
200
                    ApiCache.updateMap(queryName, params);
×
201
                } catch (Exception ex) {
×
202
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
203
                    cachedResponses.put(cacheId, null);
×
204
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
205
                } finally {
206
                    refreshStart.remove(cacheId);
×
207
                }
208
            }).start();
×
209
        }
210
        if (isCached) {
×
211
            if (cachedResponses.get(cacheId) == null) {
×
212
                cachedResponses.remove(cacheId);
×
213
                throw new RuntimeException("Query failed: " + cacheId);
×
214
            }
215
            return cachedMaps.get(cacheId);
×
216
        } else {
217
            return null;
×
218
        }
219
    }
220

221
    /**
222
     * Converts a map of parameters to a sorted string representation.
223
     *
224
     * @param params The map of parameters.
225
     * @return A string representation of the parameters.
226
     */
227
    private static String paramsToString(Map<String, String> params) {
228
        List<String> keys = new ArrayList<>(params.keySet());
×
229
        Collections.sort(keys);
×
230
        String s = "";
×
231
        for (String k : keys) s += " " + k + "=" + params.get(k);
×
232
        return s;
×
233
    }
234

235
    /**
236
     * Generates a unique cache ID for a specific query and parameters.
237
     *
238
     * @param queryName The name of the query.
239
     * @param params    The parameters for the query.
240
     * @return The unique cache ID.
241
     */
242
    public static String getCacheId(String queryName, Map<String, String> params) {
243
        return queryName + " " + paramsToString(params);
×
244
    }
245
}
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