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

knowledgepixels / nanodash / 26285127181

22 May 2026 11:28AM UTC coverage: 20.872% (+0.1%) from 20.748%
26285127181

Pull #468

github

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

1034 of 6284 branches covered (16.45%)

Branch coverage included in aggregate %.

2676 of 11491 relevant lines covered (23.29%)

3.33 hits per line

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

64.34
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.SpacesRepoAccess;
5
import com.knowledgepixels.nanodash.domain.Space;
6
import com.knowledgepixels.nanodash.domain.SpaceFactory;
7
import org.nanopub.extra.services.ApiResponseEntry;
8
import org.slf4j.Logger;
9
import org.slf4j.LoggerFactory;
10

11
import java.util.*;
12

13
/**
14
 * Repository class for managing Space instances, providing methods to refresh, retrieve, and query spaces based on API responses.
15
 */
16
public class SpaceRepository {
17

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

20
    private static final SpaceRepository INSTANCE = new SpaceRepository();
15✔
21

22
    /**
23
     * Get the singleton instance of SpaceRepository.
24
     *
25
     * @return The singleton instance of SpaceRepository.
26
     */
27
    public static SpaceRepository get() {
28
        return INSTANCE;
6✔
29
    }
30

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

41
    private final Object loadLock = new Object();
15✔
42

43
    private SpaceRepository() {
6✔
44
    }
3✔
45

46
    // TODO Replace this programmatically-built SPARQL with a published grlc
47
    // query template (like the constants in QueryApiAccess), so all Nanopub
48
    // Query access goes through the same query-template pipeline.
49
    //
50
    // Returns one row per (SpaceRef, SpaceDefinition) — many spaces have
51
    // multiple contributing nanopubs (root + updates) and even multiple
52
    // SpaceRefs during the rootless transition phase. Sorting by DESC(?date)
53
    // and dedup'ing by spaceIri in Java picks the latest update per space.
54
    private static final String SPACES_QUERY = SpacesRepoAccess.PREFIXES
55
            + "SELECT ?spaceIri ?np ?date ?label ?type WHERE {\n"
56
            + "  GRAPH npa:spacesGraph {\n"
57
            + "    ?spaceRef a npa:SpaceRef ; npa:spaceIri ?spaceIri .\n"
58
            + "    ?def a npa:SpaceDefinition ;\n"
59
            + "         npa:forSpaceRef ?spaceRef ;\n"
60
            + "         npa:viaNanopub  ?np ;\n"
61
            + "         dct:created     ?date .\n"
62
            + "  }\n"
63
            + "  GRAPH npa:graph {\n"
64
            + "    ?np rdfs:label ?label .\n"
65
            + "    ?np npx:hasNanopubType ?type .\n"
66
            + "    FILTER(STRSTARTS(STR(?type), \"https://w3id.org/kpxl/gen/terms/\"))\n"
67
            + "    FILTER(?type != <https://w3id.org/kpxl/gen/terms/Space>)\n"
68
            + "  }\n"
69
            + "  FILTER NOT EXISTS { GRAPH npa:graph { ?invNp npx:invalidates ?np . } }\n"
70
            + "} ORDER BY DESC(?date)";
71

72
    /**
73
     * Refresh the list of spaces from the spaces repo. Pulls the latest
74
     * non-invalidated SpaceDefinition per Space IRI from {@code npa:spacesGraph}
75
     * and joins to the declaring nanopub for label and type.
76
     */
77
    public synchronized void refresh() {
78
        List<Space> newSpaceList = new ArrayList<>();
12✔
79
        Map<String, List<Space>> newSpaceListByType = new HashMap<>();
12✔
80
        Map<String, Space> newSpacesById = new HashMap<>();
12✔
81
        Map<String, Space> newSpacesByAltId = new HashMap<>();
12✔
82
        Map<Space, Set<Space>> newSubspaceMap = new HashMap<>();
12✔
83
        Map<Space, Set<Space>> newSuperspaceMap = new HashMap<>();
12✔
84
        Set<String> seen = new HashSet<>();
12✔
85
        SpacesRepoAccess.get().select(SPACES_QUERY, null, b -> {
33✔
86
            String spaceIri = b.getValue("spaceIri").stringValue();
15✔
87
            if (!seen.add(spaceIri)) return null; // first row (latest date) wins
18✔
88
            ApiResponseEntry entry = new ApiResponseEntry();
12✔
89
            entry.add("space", spaceIri);
12✔
90
            entry.add("np", b.getValue("np").stringValue());
21✔
91
            entry.add("label", b.getValue("label").stringValue());
21✔
92
            entry.add("type", b.getValue("type").stringValue());
21✔
93
            Space space = SpaceFactory.getOrCreate(entry);
9✔
94
            newSpaceList.add(space);
12✔
95
            newSpaceListByType.computeIfAbsent(space.getType(), k -> new ArrayList<>()).add(space);
39✔
96
            newSpacesById.put(space.getId(), space);
18✔
97
            for (String altId : space.getAltIDs()) {
33✔
98
                newSpacesByAltId.put(altId, space);
15✔
99
            }
3✔
100
            return null;
6✔
101
        });
102
        logger.info("Refreshed spaces from spaces repo: {} distinct spaces", newSpaceList.size());
18✔
103
        SpaceFactory.removeStale(newSpacesById.keySet());
9✔
104
        populateSubspaceRelations(newSpacesById, newSubspaceMap, newSuperspaceMap);
12✔
105
        for (Space space : newSpaceList) {
30✔
106
            space.setDataNeedsUpdate();
6✔
107
        }
3✔
108
        spacesById = newSpacesById;
9✔
109
        spacesByAltId = newSpacesByAltId;
9✔
110
        spaceListByType = newSpaceListByType;
9✔
111
        subspaceMap = newSubspaceMap;
9✔
112
        superspaceMap = newSuperspaceMap;
9✔
113
        loaded = true;
9✔
114
        lastRefreshTime = System.currentTimeMillis();
9✔
115
        spaceList = newSpaceList; // volatile write last — establishes happens-before for all above
9✔
116
    }
3✔
117

118
    /**
119
     * Force a refresh of the root spaces after a specified delay, allowing for any ongoing updates to complete before the next refresh.
120
     *
121
     * @param waitMillis The number of milliseconds to wait before allowing the next refresh to occur.
122
     */
123
    public void forceRootRefresh(long waitMillis) {
124
        spaceList = null;
×
125
        runRootUpdateAfter = System.currentTimeMillis() + waitMillis;
×
126
    }
×
127

128
    /**
129
     * Ensure that the spaces are loaded, fetching them from the API if necessary.
130
     */
131
    public void ensureLoaded() {
132
        if (spaceList == null) {
9✔
133
            try {
134
                synchronized (loadLock) {
15✔
135
                    if (runRootUpdateAfter != null) {
9!
136
                        while (System.currentTimeMillis() < runRootUpdateAfter) {
×
137
                            Thread.sleep(100);
×
138
                        }
139
                        runRootUpdateAfter = null;
×
140
                    }
141
                }
9✔
142
            } catch (InterruptedException ex) {
×
143
                logger.error("Interrupted", ex);
×
144
            }
3✔
145
            if (spaceList == null) { // double-check after potential wait
9!
146
                refresh();
9✔
147
            }
148
        } else if (System.currentTimeMillis() - lastRefreshTime > 60_000) {
21!
149
            refresh();
×
150
        }
151
    }
3✔
152

153
    /**
154
     * Get a space by its id.
155
     *
156
     * @param id The id of the space.
157
     * @return The corresponding Space object, or null if not found.
158
     */
159
    public Space findById(String id) {
160
        ensureLoaded();
6✔
161
        return spacesById.get(id);
18✔
162
    }
163

164
    /**
165
     * Get a space by one of its alternative IDs.
166
     *
167
     * @param altId The alternative ID of the space.
168
     * @return The corresponding Space object, or null if not found.
169
     */
170
    public Space findByAltId(String altId) {
171
        ensureLoaded();
6✔
172
        return spacesByAltId.get(altId);
18✔
173
    }
174

175
    /**
176
     * Get spaces by their type.
177
     *
178
     * @param type The type of spaces to retrieve.
179
     * @return List of Space objects matching the specified type, or an empty list if none are found.
180
     */
181
    public List<Space> findByType(String type) {
182
        ensureLoaded();
×
183
        return spaceListByType.computeIfAbsent(type, k -> new ArrayList<>());
×
184
    }
185

186
    /**
187
     * Get subspaces of a given space.
188
     *
189
     * @param space The space for which to find subspaces.
190
     * @return List of subspaces.
191
     */
192
    public List<Space> findSubspaces(Space space) {
193
        if (subspaceMap.containsKey(space)) {
×
194
            List<Space> subspaces = new ArrayList<>(subspaceMap.get(space));
×
195
            subspaces.sort(Ordering.usingToString());
×
196
            return subspaces;
×
197
        }
198
        return new ArrayList<>();
×
199
    }
200

201
    /**
202
     * Get subspaces of a given space that match a specific type.
203
     *
204
     * @param space The space for which to find subspaces.
205
     * @param type  The type of subspaces to filter by.
206
     * @return List of subspaces matching the specified type.
207
     */
208
    public List<Space> findSubspaces(Space space, String type) {
209
        List<Space> l = new ArrayList<>();
×
210
        for (Space s : findSubspaces(space)) {
×
211
            if (s.getType().equals(type)) l.add(s);
×
212
        }
×
213
        return l;
×
214
    }
215

216
    /**
217
     * Get superspaces of this space.
218
     *
219
     * @return List of superspaces.
220
     */
221
    public List<Space> findSuperspaces(Space space) {
222
        if (superspaceMap.containsKey(space)) {
×
223
            List<Space> superspaces = new ArrayList<>(superspaceMap.get(space));
×
224
            superspaces.sort(Ordering.usingToString());
×
225
            return superspaces;
×
226
        }
227
        return new ArrayList<>();
×
228
    }
229

230
    /**
231
     * Refresh the spaces and mark each as needing a downstream data update.
232
     */
233
    public void refreshAndInvalidate() {
234
        refresh();
×
235
        for (Space space : spaceList) {
×
236
            space.setDataNeedsUpdate();
×
237
        }
×
238
    }
×
239

240
    // TODO Replace this programmatically-built SPARQL with a published grlc
241
    // query template (like the constants in QueryApiAccess), so all Nanopub
242
    // Query access goes through the same query-template pipeline.
243
    private static final String SUBSPACE_LINKS_QUERY = SpacesRepoAccess.PREFIXES
244
            + "SELECT ?child ?parent WHERE {\n"
245
            + SpacesRepoAccess.CURRENT_STATE_POINTER
246
            + "  GRAPH ?g { ?child npa:isSubSpaceOf ?parent . }\n"
247
            + "}";
248

249
    private static void populateSubspaceRelations(
250
            Map<String, Space> spacesById,
251
            Map<Space, Set<Space>> subspaceMap,
252
            Map<Space, Set<Space>> superspaceMap) {
253
        SpacesRepoAccess.get().select(SUBSPACE_LINKS_QUERY, null, b -> {
27✔
254
            Space child = spacesById.get(b.getValue("child").stringValue());
24✔
255
            Space parent = spacesById.get(b.getValue("parent").stringValue());
24✔
256
            if (child == null || parent == null) return null;
18✔
257
            subspaceMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);
36✔
258
            superspaceMap.computeIfAbsent(child, k -> new HashSet<>()).add(parent);
36✔
259
            return null;
6✔
260
        });
261
    }
3✔
262

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