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

knowledgepixels / nanodash / 17852532121

19 Sep 2025 08:10AM UTC coverage: 13.568% (-0.3%) from 13.87%
17852532121

push

github

tkuhn
feat: Switch to QueryRef provided by nanopub, using multimap

428 of 4008 branches covered (10.68%)

Branch coverage included in aggregate %.

1104 of 7283 relevant lines covered (15.16%)

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 java.util.HashMap;
4
import java.util.List;
5
import java.util.Map;
6
import java.util.Random;
7
import java.util.concurrent.ConcurrentHashMap;
8
import java.util.concurrent.ConcurrentMap;
9

10
import org.nanopub.extra.services.APINotReachableException;
11
import org.nanopub.extra.services.ApiResponse;
12
import org.nanopub.extra.services.ApiResponseEntry;
13
import org.nanopub.extra.services.FailedApiCallException;
14
import org.nanopub.extra.services.NotEnoughAPIInstancesException;
15
import org.nanopub.extra.services.QueryRef;
16
import org.slf4j.Logger;
17
import org.slf4j.LoggerFactory;
18

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

25
    private ApiCache() {
26
    } // no instances allowed
27

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

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

45
    public static boolean isRunning(QueryRef queryRef) {
46
        return isRunning(queryRef.getAsUrlString());
×
47
    }
48
    /**
49
     * Updates the cached API response for a specific query reference.
50
     *
51
     * @param queryRef The query reference
52
     * @throws FailedApiCallException If the API call fails.
53
     */
54
    private static void updateResponse(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
55
        ApiResponse response = QueryApiAccess.get(queryRef);
×
56
        String cacheId = queryRef.getAsUrlString();
×
57
        cachedResponses.put(cacheId, response);
×
58
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
59
    }
×
60

61
    /**
62
     * Retrieves a cached API response for a specific QueryRef.
63
     *
64
     * @param queryRef The QueryRef object containing the query name and parameters.
65
     * @return The cached API response, or null if not cached.
66
     */
67
    public static ApiResponse retrieveResponse(QueryRef queryRef) {
68
        long timeNow = System.currentTimeMillis();
×
69
        String cacheId = queryRef.getAsUrlString();
×
70
        boolean isCached = false;
×
71
        boolean needsRefresh = true;
×
72
        if (cachedResponses.containsKey(cacheId) && cachedResponses.get(cacheId) != null) {
×
73
            long cacheAge = timeNow - lastRefresh.get(cacheId);
×
74
            isCached = cacheAge < 24 * 60 * 60 * 1000;
×
75
            needsRefresh = cacheAge > 60 * 1000;
×
76
        }
77
        if (needsRefresh && !isRunning(cacheId)) {
×
78
            refreshStart.put(cacheId, timeNow);
×
79
            new Thread(() -> {
×
80
                try {
81
                    Thread.sleep(100 + new Random().nextLong(400));
×
82
                } catch (InterruptedException ex) {
×
83
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
84
                }
×
85
                try {
86
                    ApiCache.updateResponse(queryRef);
×
87
                } catch (Exception ex) {
×
88
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
89
                    cachedResponses.put(cacheId, null);
×
90
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
91
                } finally {
92
                    refreshStart.remove(cacheId);
×
93
                }
94
            }).start();
×
95
        }
96
        if (isCached) {
×
97
            if (cachedResponses.get(cacheId) == null) {
×
98
                cachedResponses.remove(cacheId);
×
99
                throw new RuntimeException("Query failed: " + cacheId);
×
100
            }
101
            return cachedResponses.get(cacheId);
×
102
        } else {
103
            return null;
×
104
        }
105
    }
106

107
    /**
108
     * Updates the cached map for a specific query reference.
109
     *
110
     * @param queryRef The query reference
111
     * @throws FailedApiCallException If the API call fails.
112
     */
113
    private static void updateMap(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
114
        Map<String, String> map = new HashMap<>();
×
115
        List<ApiResponseEntry> respList = QueryApiAccess.get(queryRef).getData();
×
116
        while (respList != null && !respList.isEmpty()) {
×
117
            ApiResponseEntry resultEntry = respList.remove(0);
×
118
            map.put(resultEntry.get("key"), resultEntry.get("value"));
×
119
        }
×
120
        String cacheId = queryRef.getAsUrlString();
×
121
        cachedMaps.put(cacheId, map);
×
122
        lastRefresh.put(cacheId, System.currentTimeMillis());
×
123
    }
×
124

125
    /**
126
     * Retrieves a cached map for a specific query reference.
127
     * If the cache is stale, it triggers a background refresh.
128
     *
129
     * @param queryRef The query reference
130
     * @return The cached map, or null if not cached.
131
     */
132
    public static synchronized Map<String, String> retrieveMap(QueryRef queryRef) {
133
        long timeNow = System.currentTimeMillis();
×
134
        String cacheId = queryRef.getAsUrlString();
×
135
        boolean isCached = false;
×
136
        boolean needsRefresh = true;
×
137
        if (cachedMaps.containsKey(cacheId)) {
×
138
            long cacheAge = timeNow - lastRefresh.get(cacheId);
×
139
            isCached = cacheAge < 24 * 60 * 60 * 1000;
×
140
            needsRefresh = cacheAge > 60 * 1000;
×
141
        }
142
        if (needsRefresh && !isRunning(cacheId)) {
×
143
            refreshStart.put(cacheId, timeNow);
×
144
            new Thread(() -> {
×
145
                try {
146
                    Thread.sleep(100 + new Random().nextLong(400));
×
147
                } catch (InterruptedException ex) {
×
148
                    logger.error("Interrupted while waiting to refresh cache: {}", ex.getMessage());
×
149
                }
×
150
                try {
151
                    ApiCache.updateMap(queryRef);
×
152
                } catch (Exception ex) {
×
153
                    logger.error("Failed to update cache for {}: {}", cacheId, ex.getMessage());
×
154
                    cachedResponses.put(cacheId, null);
×
155
                    lastRefresh.put(cacheId, System.currentTimeMillis());
×
156
                } finally {
157
                    refreshStart.remove(cacheId);
×
158
                }
159
            }).start();
×
160
        }
161
        if (isCached) {
×
162
            if (cachedResponses.get(cacheId) == null) {
×
163
                cachedResponses.remove(cacheId);
×
164
                throw new RuntimeException("Query failed: " + cacheId);
×
165
            }
166
            return cachedMaps.get(cacheId);
×
167
        } else {
168
            return null;
×
169
        }
170
    }
171

172
}
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