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

knowledgepixels / nanodash / 26283394584

22 May 2026 10:48AM UTC coverage: 20.906% (+0.2%) from 20.748%
26283394584

Pull #468

github

web-flow
Merge 3f76d1d2d into 65b0c8452
Pull Request #468: Source space data from nanopub-query spaces repo

1040 of 6290 branches covered (16.53%)

Branch coverage included in aggregate %.

2672 of 11466 relevant lines covered (23.3%)

3.33 hits per line

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

61.98
src/main/java/com/knowledgepixels/nanodash/repository/SpaceRepository.java
1
package com.knowledgepixels.nanodash.repository;
2

3
import com.github.jsonldjava.shaded.com.google.common.collect.Ordering;
4
import com.knowledgepixels.nanodash.ApiCache;
5
import com.knowledgepixels.nanodash.QueryApiAccess;
6
import com.knowledgepixels.nanodash.SpacesRepoAccess;
7
import com.knowledgepixels.nanodash.domain.Space;
8
import com.knowledgepixels.nanodash.domain.SpaceFactory;
9
import org.nanopub.extra.services.ApiResponse;
10
import org.nanopub.extra.services.ApiResponseEntry;
11
import org.nanopub.extra.services.QueryRef;
12
import org.slf4j.Logger;
13
import org.slf4j.LoggerFactory;
14

15
import java.util.*;
16

17
/**
18
 * Repository class for managing Space instances, providing methods to refresh, retrieve, and query spaces based on API responses.
19
 */
20
public class SpaceRepository {
21

22
    private static final Logger logger = LoggerFactory.getLogger(SpaceRepository.class);
9✔
23

24
    private static final SpaceRepository INSTANCE = new SpaceRepository();
15✔
25

26
    /**
27
     * Get the singleton instance of SpaceRepository.
28
     *
29
     * @return The singleton instance of SpaceRepository.
30
     */
31
    public static SpaceRepository get() {
32
        return INSTANCE;
6✔
33
    }
34

35
    private volatile List<Space> spaceList;
36
    private Map<String, List<Space>> spaceListByType;
37
    private Map<String, Space> spacesById;
38
    private Map<String, Space> spacesByAltId;
39
    private Map<Space, Set<Space>> subspaceMap;
40
    private Map<Space, Set<Space>> superspaceMap;
41
    private boolean loaded = false;
9✔
42
    private volatile Long runRootUpdateAfter = null;
9✔
43
    private volatile long lastRefreshTime = 0;
9✔
44

45
    private final Object loadLock = new Object();
15✔
46

47
    private SpaceRepository() {
6✔
48
    }
3✔
49

50
    /**
51
     * Refresh the list of spaces from the API response.
52
     *
53
     * @param resp The API response containing space data.
54
     */
55
    public synchronized void refresh(ApiResponse resp) {
56
        logger.info("Refreshing spaces from API response with {} entries", resp.getData().size());
21✔
57
        List<Space> newSpaceList = new ArrayList<>();
12✔
58
        Map<String, List<Space>> newSpaceListByType = new HashMap<>();
12✔
59
        Map<String, Space> newSpacesById = new HashMap<>();
12✔
60
        Map<String, Space> newSpacesByAltId = new HashMap<>();
12✔
61
        Map<Space, Set<Space>> newSubspaceMap = new HashMap<>();
12✔
62
        Map<Space, Set<Space>> newSuperspaceMap = new HashMap<>();
12✔
63
        for (ApiResponseEntry entry : resp.getData()) {
33✔
64
            Space space;
65
            space = SpaceFactory.getOrCreate(entry);
9✔
66
            newSpaceList.add(space);
12✔
67
            newSpaceListByType.computeIfAbsent(space.getType(), k -> new ArrayList<>()).add(space);
39✔
68
            newSpacesById.put(space.getId(), space);
18✔
69
            for (String altId : space.getAltIDs()) {
33✔
70
                newSpacesByAltId.put(altId, space);
15✔
71
            }
3✔
72
        }
3✔
73
        SpaceFactory.removeStale(newSpacesById.keySet());
9✔
74
        populateSubspaceRelations(newSpacesById, newSubspaceMap, newSuperspaceMap);
12✔
75
        for (Space space : newSpaceList) {
30✔
76
            space.setDataNeedsUpdate();
6✔
77
        }
3✔
78
        spacesById = newSpacesById;
9✔
79
        spacesByAltId = newSpacesByAltId;
9✔
80
        spaceListByType = newSpaceListByType;
9✔
81
        subspaceMap = newSubspaceMap;
9✔
82
        superspaceMap = newSuperspaceMap;
9✔
83
        loaded = true;
9✔
84
        lastRefreshTime = System.currentTimeMillis();
9✔
85
        spaceList = newSpaceList; // volatile write last — establishes happens-before for all above
9✔
86
    }
3✔
87

88
    /**
89
     * Force a refresh of the root spaces after a specified delay, allowing for any ongoing updates to complete before the next refresh.
90
     *
91
     * @param waitMillis The number of milliseconds to wait before allowing the next refresh to occur.
92
     */
93
    public void forceRootRefresh(long waitMillis) {
94
        spaceList = null;
×
95
        runRootUpdateAfter = System.currentTimeMillis() + waitMillis;
×
96
    }
×
97

98
    /**
99
     * Ensure that the spaces are loaded, fetching them from the API if necessary.
100
     */
101
    public void ensureLoaded() {
102
        if (spaceList == null) {
9✔
103
            try {
104
                synchronized (loadLock) {
15✔
105
                    if (runRootUpdateAfter != null) {
9!
106
                        while (System.currentTimeMillis() < runRootUpdateAfter) {
×
107
                            Thread.sleep(100);
×
108
                        }
109
                        runRootUpdateAfter = null;
×
110
                    }
111
                }
9✔
112
            } catch (InterruptedException ex) {
×
113
                logger.error("Interrupted", ex);
×
114
            }
3✔
115
            if (spaceList == null) { // double-check after potential wait
9!
116
                refresh(ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACES), true));
27✔
117
            }
118
        } else if (System.currentTimeMillis() - lastRefreshTime > 60_000) {
21!
119
            refresh(ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACES), true));
×
120
        }
121
    }
3✔
122

123
    /**
124
     * Get a space by its id.
125
     *
126
     * @param id The id of the space.
127
     * @return The corresponding Space object, or null if not found.
128
     */
129
    public Space findById(String id) {
130
        ensureLoaded();
6✔
131
        return spacesById.get(id);
18✔
132
    }
133

134
    /**
135
     * Get a space by one of its alternative IDs.
136
     *
137
     * @param altId The alternative ID of the space.
138
     * @return The corresponding Space object, or null if not found.
139
     */
140
    public Space findByAltId(String altId) {
141
        ensureLoaded();
6✔
142
        return spacesByAltId.get(altId);
18✔
143
    }
144

145
    /**
146
     * Get spaces by their type.
147
     *
148
     * @param type The type of spaces to retrieve.
149
     * @return List of Space objects matching the specified type, or an empty list if none are found.
150
     */
151
    public List<Space> findByType(String type) {
152
        ensureLoaded();
×
153
        return spaceListByType.computeIfAbsent(type, k -> new ArrayList<>());
×
154
    }
155

156
    /**
157
     * Get subspaces of a given space.
158
     *
159
     * @param space The space for which to find subspaces.
160
     * @return List of subspaces.
161
     */
162
    public List<Space> findSubspaces(Space space) {
163
        if (subspaceMap.containsKey(space)) {
×
164
            List<Space> subspaces = new ArrayList<>(subspaceMap.get(space));
×
165
            subspaces.sort(Ordering.usingToString());
×
166
            return subspaces;
×
167
        }
168
        return new ArrayList<>();
×
169
    }
170

171
    /**
172
     * Get subspaces of a given space that match a specific type.
173
     *
174
     * @param space The space for which to find subspaces.
175
     * @param type  The type of subspaces to filter by.
176
     * @return List of subspaces matching the specified type.
177
     */
178
    public List<Space> findSubspaces(Space space, String type) {
179
        List<Space> l = new ArrayList<>();
×
180
        for (Space s : findSubspaces(space)) {
×
181
            if (s.getType().equals(type)) l.add(s);
×
182
        }
×
183
        return l;
×
184
    }
185

186
    /**
187
     * Get superspaces of this space.
188
     *
189
     * @return List of superspaces.
190
     */
191
    public List<Space> findSuperspaces(Space space) {
192
        if (superspaceMap.containsKey(space)) {
×
193
            List<Space> superspaces = new ArrayList<>(superspaceMap.get(space));
×
194
            superspaces.sort(Ordering.usingToString());
×
195
            return superspaces;
×
196
        }
197
        return new ArrayList<>();
×
198
    }
199

200
    /**
201
     * Mark all spaces as needing a data update.
202
     */
203
    public void refresh() {
204
        refresh(ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACES), true));
×
205
        for (Space space : spaceList) {
×
206
            space.setDataNeedsUpdate();
×
207
        }
×
208
    }
×
209

210
    // TODO Replace this programmatically-built SPARQL with a published grlc
211
    // query template (like the constants in QueryApiAccess), so all Nanopub
212
    // Query access goes through the same query-template pipeline.
213
    private static final String SUBSPACE_LINKS_QUERY = SpacesRepoAccess.PREFIXES
214
            + "SELECT ?child ?parent WHERE {\n"
215
            + SpacesRepoAccess.CURRENT_STATE_POINTER
216
            + "  GRAPH ?g { ?child npa:isSubSpaceOf ?parent . }\n"
217
            + "}";
218

219
    private static void populateSubspaceRelations(
220
            Map<String, Space> spacesById,
221
            Map<Space, Set<Space>> subspaceMap,
222
            Map<Space, Set<Space>> superspaceMap) {
223
        SpacesRepoAccess.get().select(SUBSPACE_LINKS_QUERY, null, b -> {
27✔
224
            Space child = spacesById.get(b.getValue("child").stringValue());
24✔
225
            Space parent = spacesById.get(b.getValue("parent").stringValue());
24✔
226
            if (child == null || parent == null) return null;
18✔
227
            subspaceMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);
36✔
228
            superspaceMap.computeIfAbsent(child, k -> new HashSet<>()).add(parent);
36✔
229
            return null;
6✔
230
        });
231
    }
3✔
232

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