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

knowledgepixels / nanodash / 26560064952

28 May 2026 07:04AM UTC coverage: 20.672% (+0.03%) from 20.643%
26560064952

Pull #473

github

web-flow
Merge e68ff10df into ad92b3169
Pull Request #473: refactor: collapse repo cache + push view-display admin gate server-side

1009 of 6168 branches covered (16.36%)

Branch coverage included in aggregate %.

2594 of 11261 relevant lines covered (23.04%)

3.3 hits per line

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

72.18
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.domain.Space;
7
import com.knowledgepixels.nanodash.domain.SpaceFactory;
8
import org.nanopub.extra.services.ApiResponse;
9
import org.nanopub.extra.services.ApiResponseEntry;
10
import org.nanopub.extra.services.QueryRef;
11
import org.slf4j.Logger;
12
import org.slf4j.LoggerFactory;
13

14
import java.util.ArrayList;
15
import java.util.Collections;
16
import java.util.Comparator;
17
import java.util.HashMap;
18
import java.util.HashSet;
19
import java.util.List;
20
import java.util.Map;
21
import java.util.Set;
22

23
/**
24
 * Repository class for managing Space instances, providing methods to refresh, retrieve, and query spaces based on API responses.
25
 */
26
public class SpaceRepository {
27

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

30
    private static final SpaceRepository INSTANCE = new SpaceRepository();
15✔
31

32
    /**
33
     * Get the singleton instance of SpaceRepository.
34
     *
35
     * @return The singleton instance of SpaceRepository.
36
     */
37
    public static SpaceRepository get() {
38
        return INSTANCE;
6✔
39
    }
40

41
    private SpaceRepository() {
6✔
42
    }
3✔
43

44
    private static final class Snapshot {
45
        final Map<String, Space> spacesById;
46
        final Map<String, Space> spacesByAltId;
47
        final Map<String, List<Space>> spaceListByType;
48
        final Map<Space, Set<Space>> subspaceMap;
49
        final Map<Space, Set<Space>> superspaceMap;
50

51
        Snapshot(Map<String, Space> byId,
52
                 Map<String, Space> byAltId,
53
                 Map<String, List<Space>> byType,
54
                 Map<Space, Set<Space>> subspaces,
55
                 Map<Space, Set<Space>> superspaces) {
6✔
56
            this.spacesById = byId;
9✔
57
            this.spacesByAltId = byAltId;
9✔
58
            this.spaceListByType = byType;
9✔
59
            this.subspaceMap = subspaces;
9✔
60
            this.superspaceMap = superspaces;
9✔
61
        }
3✔
62

63
        static final Snapshot EMPTY = new Snapshot(
9✔
64
                Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(),
9✔
65
                Collections.emptyMap(), Collections.emptyMap());
12✔
66
    }
67

68
    // Source of truth is ApiCache (60s TTL on its own); we memoise the derived
69
    // maps keyed by the ApiResponse instance identity for get-spaces, and
70
    // re-resolve the sub-space link response on each rebuild.
71
    private volatile ApiResponse cachedFor;
72
    private volatile Snapshot snapshot = Snapshot.EMPTY;
9✔
73

74
    private Snapshot current() {
75
        ApiResponse resp = ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACES), false);
21✔
76
        if (resp == null) {
6!
77
            return snapshot;
×
78
        }
79
        if (resp == cachedFor) {
12✔
80
            return snapshot;
9✔
81
        }
82
        synchronized (this) {
12✔
83
            if (resp == cachedFor) {
12!
84
                return snapshot;
×
85
            }
86
            Snapshot built = build(resp);
12✔
87
            snapshot = built;
9✔
88
            cachedFor = resp;
9✔
89
            return built;
12✔
90
        }
91
    }
92

93
    /**
94
     * Build the space lookup maps from the spaces-repo response. Pulls the
95
     * latest non-invalidated SpaceDefinition per Space IRI from
96
     * {@code npa:spacesGraph} and joins to the declaring nanopub for label and
97
     * type.
98
     * <p>
99
     * The {@code get-spaces} query returns one row per (SpaceRef, SpaceDefinition)
100
     * — many spaces have multiple contributing nanopubs (root + updates) and even
101
     * multiple SpaceRefs during the rootless transition phase. The query orders by
102
     * {@code DESC(?date)}, so dedup'ing by spaceIri here (first row wins) picks the
103
     * latest update per space.
104
     */
105
    private Snapshot build(ApiResponse resp) {
106
        Map<String, Space> byId = new HashMap<>();
12✔
107
        Map<String, Space> byAltId = new HashMap<>();
12✔
108
        Map<String, List<Space>> byType = new HashMap<>();
12✔
109
        Map<Space, Set<Space>> subspaceMap = new HashMap<>();
12✔
110
        Map<Space, Set<Space>> superspaceMap = new HashMap<>();
12✔
111
        List<Space> spaceList = new ArrayList<>();
12✔
112
        Set<String> seen = new HashSet<>();
12✔
113
        for (ApiResponseEntry r : resp.getData()) {
33✔
114
            String spaceIri = r.get("spaceIri");
12✔
115
            if (spaceIri == null || spaceIri.isEmpty()) continue;
15!
116
            if (!seen.add(spaceIri)) continue; // first row (latest date) wins
15✔
117
            ApiResponseEntry entry = new ApiResponseEntry();
12✔
118
            entry.add("space", spaceIri);
12✔
119
            entry.add("np", r.get("np"));
18✔
120
            entry.add("label", r.get("label"));
18✔
121
            entry.add("type", r.get("type"));
18✔
122
            Space space = SpaceFactory.getOrCreate(entry);
9✔
123
            spaceList.add(space);
12✔
124
            byType.computeIfAbsent(space.getType(), k -> new ArrayList<>()).add(space);
39✔
125
            byId.put(space.getId(), space);
18✔
126
            for (String altId : space.getAltIDs()) {
33✔
127
                byAltId.put(altId, space);
15✔
128
            }
3✔
129
        }
3✔
130
        Comparator<Space> byLabel = Comparator.comparing(Space::getLabel, Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER));
15✔
131
        for (List<Space> spacesOfType : byType.values()) {
33✔
132
            spacesOfType.sort(byLabel);
9✔
133
        }
3✔
134
        logger.info("Refreshed spaces from spaces repo: {} distinct spaces", spaceList.size());
18✔
135
        SpaceFactory.removeStale(byId.keySet());
9✔
136
        populateSubspaceRelations(byId, subspaceMap, superspaceMap);
12✔
137
        // Mark each space's per-space detail data stale; the upstream spaces
138
        // listing has refreshed, so members/admins/roles should be re-fetched
139
        // on next access.
140
        for (Space space : spaceList) {
30✔
141
            space.setDataNeedsUpdate();
6✔
142
        }
3✔
143
        return new Snapshot(byId, byAltId, byType, subspaceMap, superspaceMap);
27✔
144
    }
145

146
    /**
147
     * Invalidate the underlying ApiCache entries, optionally delaying the next refresh.
148
     *
149
     * @param waitMillis Delay in milliseconds before the next access may trigger a refresh; 0 for immediate.
150
     */
151
    public void forceRootRefresh(long waitMillis) {
152
        ApiCache.clearCache(new QueryRef(QueryApiAccess.GET_SPACES), waitMillis);
×
153
        ApiCache.clearCache(new QueryRef(QueryApiAccess.GET_SUB_SPACE_LINKS), waitMillis);
×
154
    }
×
155

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

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

176
    /**
177
     * Get spaces by their type.
178
     *
179
     * @param type The type of spaces to retrieve.
180
     * @return List of Space objects matching the specified type, or an empty list if none are found.
181
     */
182
    public List<Space> findByType(String type) {
183
        List<Space> l = current().spaceListByType.get(type);
×
184
        return l != null ? l : new ArrayList<>();
×
185
    }
186

187
    /**
188
     * Get subspaces of a given space.
189
     *
190
     * @param space The space for which to find subspaces.
191
     * @return List of subspaces.
192
     */
193
    public List<Space> findSubspaces(Space space) {
194
        Set<Space> subspaces = current().subspaceMap.get(space);
×
195
        if (subspaces == null) return new ArrayList<>();
×
196
        List<Space> sorted = new ArrayList<>(subspaces);
×
197
        sorted.sort(Ordering.usingToString());
×
198
        return sorted;
×
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
        Set<Space> superspaces = current().superspaceMap.get(space);
×
223
        if (superspaces == null) return new ArrayList<>();
×
224
        List<Space> sorted = new ArrayList<>(superspaces);
×
225
        sorted.sort(Ordering.usingToString());
×
226
        return sorted;
×
227
    }
228

229
    private static void populateSubspaceRelations(
230
            Map<String, Space> spacesById,
231
            Map<Space, Set<Space>> subspaceMap,
232
            Map<Space, Set<Space>> superspaceMap) {
233
        ApiResponse resp = ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SUB_SPACE_LINKS), false);
21✔
234
        if (resp == null) return;
6!
235
        for (ApiResponseEntry r : resp.getData()) {
33✔
236
            Space child = spacesById.get(r.get("child"));
21✔
237
            Space parent = spacesById.get(r.get("parent"));
21✔
238
            if (child == null || parent == null) continue;
15✔
239
            subspaceMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);
36✔
240
            superspaceMap.computeIfAbsent(child, k -> new HashSet<>()).add(parent);
36✔
241
        }
3✔
242
    }
3✔
243

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