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

knowledgepixels / nanodash / 26599283002

28 May 2026 08:07PM UTC coverage: 20.732% (-0.03%) from 20.76%
26599283002

Pull #475

github

web-flow
Merge 59b029ab3 into 03dc49b33
Pull Request #475: feat: render /spaces as a single sortable All Spaces table

1003 of 6148 branches covered (16.31%)

Branch coverage included in aggregate %.

2584 of 11154 relevant lines covered (23.17%)

3.31 hits per line

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

72.5
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.HashMap;
17
import java.util.HashSet;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Set;
21

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

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

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

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

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

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

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

59
        static final Snapshot EMPTY = new Snapshot(
9✔
60
                Collections.emptyMap(), Collections.emptyMap(),
6✔
61
                Collections.emptyMap(), Collections.emptyMap());
12✔
62
    }
63

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

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

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

136
    /**
137
     * Invalidate the underlying ApiCache entries, optionally delaying the next refresh.
138
     *
139
     * @param waitMillis Delay in milliseconds before the next access may trigger a refresh; 0 for immediate.
140
     */
141
    public void forceRootRefresh(long waitMillis) {
142
        ApiCache.clearCache(new QueryRef(QueryApiAccess.GET_SPACES), waitMillis);
×
143
        ApiCache.clearCache(new QueryRef(QueryApiAccess.GET_SUB_SPACE_LINKS), waitMillis);
×
144
    }
×
145

146
    /**
147
     * Get a space by its id.
148
     *
149
     * @param id The id of the space.
150
     * @return The corresponding Space object, or null if not found.
151
     */
152
    public Space findById(String id) {
153
        return current().spacesById.get(id);
21✔
154
    }
155

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

166
    /**
167
     * Get subspaces of a given space.
168
     *
169
     * @param space The space for which to find subspaces.
170
     * @return List of subspaces.
171
     */
172
    public List<Space> findSubspaces(Space space) {
173
        Set<Space> subspaces = current().subspaceMap.get(space);
×
174
        if (subspaces == null) return new ArrayList<>();
×
175
        List<Space> sorted = new ArrayList<>(subspaces);
×
176
        sorted.sort(Ordering.usingToString());
×
177
        return sorted;
×
178
    }
179

180
    /**
181
     * Get subspaces of a given space that match a specific type.
182
     *
183
     * @param space The space for which to find subspaces.
184
     * @param type  The type of subspaces to filter by.
185
     * @return List of subspaces matching the specified type.
186
     */
187
    public List<Space> findSubspaces(Space space, String type) {
188
        List<Space> l = new ArrayList<>();
×
189
        for (Space s : findSubspaces(space)) {
×
190
            if (s.getType().equals(type)) l.add(s);
×
191
        }
×
192
        return l;
×
193
    }
194

195
    /**
196
     * Get superspaces of this space.
197
     *
198
     * @return List of superspaces.
199
     */
200
    public List<Space> findSuperspaces(Space space) {
201
        Set<Space> superspaces = current().superspaceMap.get(space);
×
202
        if (superspaces == null) return new ArrayList<>();
×
203
        List<Space> sorted = new ArrayList<>(superspaces);
×
204
        sorted.sort(Ordering.usingToString());
×
205
        return sorted;
×
206
    }
207

208
    private static void populateSubspaceRelations(
209
            Map<String, Space> spacesById,
210
            Map<Space, Set<Space>> subspaceMap,
211
            Map<Space, Set<Space>> superspaceMap) {
212
        ApiResponse resp = ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SUB_SPACE_LINKS), false);
21✔
213
        if (resp == null) return;
6!
214
        for (ApiResponseEntry r : resp.getData()) {
33✔
215
            Space child = spacesById.get(r.get("child"));
21✔
216
            Space parent = spacesById.get(r.get("parent"));
21✔
217
            if (child == null || parent == null) continue;
15✔
218
            subspaceMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);
36✔
219
            superspaceMap.computeIfAbsent(child, k -> new HashSet<>()).add(parent);
36✔
220
        }
3✔
221
    }
3✔
222

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