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

knowledgepixels / nanodash / 26513566694

27 May 2026 01:18PM UTC coverage: 20.535% (+0.03%) from 20.502%
26513566694

push

github

tkuhn
fix: sort spaces by label within each type panel on the spaces list

findByType returned spaces in query order, so the SpaceListPage type panels
appeared unsorted. Sort each per-type list by label once at load time
(case-insensitive, nulls last) rather than per render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

1007 of 6224 branches covered (16.18%)

Branch coverage included in aggregate %.

2604 of 11361 relevant lines covered (22.92%)

3.28 hits per line

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

66.67
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.ApiResponseEntry;
9
import org.nanopub.extra.services.QueryRef;
10
import org.slf4j.Logger;
11
import org.slf4j.LoggerFactory;
12

13
import java.util.*;
14

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

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

22
    private static final SpaceRepository INSTANCE = new SpaceRepository();
15✔
23

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

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

43
    private final Object loadLock = new Object();
15✔
44

45
    private SpaceRepository() {
6✔
46
    }
3✔
47

48
    /**
49
     * Refresh the list of spaces from the spaces repo. Pulls the latest
50
     * non-invalidated SpaceDefinition per Space IRI from {@code npa:spacesGraph}
51
     * and joins to the declaring nanopub for label and type.
52
     * <p>
53
     * The {@code get-spaces} query returns one row per (SpaceRef, SpaceDefinition)
54
     * — many spaces have multiple contributing nanopubs (root + updates) and even
55
     * multiple SpaceRefs during the rootless transition phase. The query orders by
56
     * {@code DESC(?date)}, so dedup'ing by spaceIri here (first row wins) picks the
57
     * latest update per space.
58
     */
59
    public synchronized void refresh() {
60
        List<Space> newSpaceList = new ArrayList<>();
12✔
61
        Map<String, List<Space>> newSpaceListByType = new HashMap<>();
12✔
62
        Map<String, Space> newSpacesById = new HashMap<>();
12✔
63
        Map<String, Space> newSpacesByAltId = new HashMap<>();
12✔
64
        Map<Space, Set<Space>> newSubspaceMap = new HashMap<>();
12✔
65
        Map<Space, Set<Space>> newSuperspaceMap = new HashMap<>();
12✔
66
        Set<String> seen = new HashSet<>();
12✔
67
        for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACES), true).getData()) {
48✔
68
            String spaceIri = r.get("spaceIri");
12✔
69
            if (spaceIri == null || spaceIri.isEmpty()) continue;
15!
70
            if (!seen.add(spaceIri)) continue; // first row (latest date) wins
15✔
71
            ApiResponseEntry entry = new ApiResponseEntry();
12✔
72
            entry.add("space", spaceIri);
12✔
73
            entry.add("np", r.get("np"));
18✔
74
            entry.add("label", r.get("label"));
18✔
75
            entry.add("type", r.get("type"));
18✔
76
            Space space = SpaceFactory.getOrCreate(entry);
9✔
77
            newSpaceList.add(space);
12✔
78
            newSpaceListByType.computeIfAbsent(space.getType(), k -> new ArrayList<>()).add(space);
39✔
79
            newSpacesById.put(space.getId(), space);
18✔
80
            for (String altId : space.getAltIDs()) {
33✔
81
                newSpacesByAltId.put(altId, space);
15✔
82
            }
3✔
83
        }
3✔
84
        Comparator<Space> byLabel = Comparator.comparing(Space::getLabel, Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER));
15✔
85
        for (List<Space> spacesOfType : newSpaceListByType.values()) {
33✔
86
            spacesOfType.sort(byLabel);
9✔
87
        }
3✔
88
        logger.info("Refreshed spaces from spaces repo: {} distinct spaces", newSpaceList.size());
18✔
89
        SpaceFactory.removeStale(newSpacesById.keySet());
9✔
90
        populateSubspaceRelations(newSpacesById, newSubspaceMap, newSuperspaceMap);
12✔
91
        for (Space space : newSpaceList) {
30✔
92
            space.setDataNeedsUpdate();
6✔
93
        }
3✔
94
        spacesById = newSpacesById;
9✔
95
        spacesByAltId = newSpacesByAltId;
9✔
96
        spaceListByType = newSpaceListByType;
9✔
97
        subspaceMap = newSubspaceMap;
9✔
98
        superspaceMap = newSuperspaceMap;
9✔
99
        loaded = true;
9✔
100
        lastRefreshTime = System.currentTimeMillis();
9✔
101
        spaceList = newSpaceList; // volatile write last — establishes happens-before for all above
9✔
102
    }
3✔
103

104
    /**
105
     * Force a refresh of the root spaces after a specified delay, allowing for any ongoing updates to complete before the next refresh.
106
     *
107
     * @param waitMillis The number of milliseconds to wait before allowing the next refresh to occur.
108
     */
109
    public void forceRootRefresh(long waitMillis) {
110
        spaceList = null;
×
111
        runRootUpdateAfter = System.currentTimeMillis() + waitMillis;
×
112
    }
×
113

114
    /**
115
     * Ensure that the spaces are loaded, fetching them from the API if necessary.
116
     */
117
    public void ensureLoaded() {
118
        if (spaceList == null) {
9✔
119
            try {
120
                synchronized (loadLock) {
15✔
121
                    if (runRootUpdateAfter != null) {
9!
122
                        while (System.currentTimeMillis() < runRootUpdateAfter) {
×
123
                            Thread.sleep(100);
×
124
                        }
125
                        runRootUpdateAfter = null;
×
126
                    }
127
                }
9✔
128
            } catch (InterruptedException ex) {
×
129
                logger.error("Interrupted", ex);
×
130
            }
3✔
131
            if (spaceList == null) { // double-check after potential wait
9!
132
                refresh();
9✔
133
            }
134
        } else if (System.currentTimeMillis() - lastRefreshTime > 60_000) {
21!
135
            refresh();
×
136
        }
137
    }
3✔
138

139
    /**
140
     * Get a space by its id.
141
     *
142
     * @param id The id of the space.
143
     * @return The corresponding Space object, or null if not found.
144
     */
145
    public Space findById(String id) {
146
        ensureLoaded();
6✔
147
        return spacesById.get(id);
18✔
148
    }
149

150
    /**
151
     * Get a space by one of its alternative IDs.
152
     *
153
     * @param altId The alternative ID of the space.
154
     * @return The corresponding Space object, or null if not found.
155
     */
156
    public Space findByAltId(String altId) {
157
        ensureLoaded();
6✔
158
        return spacesByAltId.get(altId);
18✔
159
    }
160

161
    /**
162
     * Get spaces by their type.
163
     *
164
     * @param type The type of spaces to retrieve.
165
     * @return List of Space objects matching the specified type, or an empty list if none are found.
166
     */
167
    public List<Space> findByType(String type) {
168
        ensureLoaded();
×
169
        return spaceListByType.computeIfAbsent(type, k -> new ArrayList<>());
×
170
    }
171

172
    /**
173
     * Get subspaces of a given space.
174
     *
175
     * @param space The space for which to find subspaces.
176
     * @return List of subspaces.
177
     */
178
    public List<Space> findSubspaces(Space space) {
179
        if (subspaceMap.containsKey(space)) {
×
180
            List<Space> subspaces = new ArrayList<>(subspaceMap.get(space));
×
181
            subspaces.sort(Ordering.usingToString());
×
182
            return subspaces;
×
183
        }
184
        return new ArrayList<>();
×
185
    }
186

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

202
    /**
203
     * Get superspaces of this space.
204
     *
205
     * @return List of superspaces.
206
     */
207
    public List<Space> findSuperspaces(Space space) {
208
        if (superspaceMap.containsKey(space)) {
×
209
            List<Space> superspaces = new ArrayList<>(superspaceMap.get(space));
×
210
            superspaces.sort(Ordering.usingToString());
×
211
            return superspaces;
×
212
        }
213
        return new ArrayList<>();
×
214
    }
215

216
    /**
217
     * Refresh the spaces and mark each as needing a downstream data update.
218
     */
219
    public void refreshAndInvalidate() {
220
        refresh();
×
221
        for (Space space : spaceList) {
×
222
            space.setDataNeedsUpdate();
×
223
        }
×
224
    }
×
225

226
    private static void populateSubspaceRelations(
227
            Map<String, Space> spacesById,
228
            Map<Space, Set<Space>> subspaceMap,
229
            Map<Space, Set<Space>> superspaceMap) {
230
        for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SUB_SPACE_LINKS), true).getData()) {
48✔
231
            Space child = spacesById.get(r.get("child"));
21✔
232
            Space parent = spacesById.get(r.get("parent"));
21✔
233
            if (child == null || parent == null) continue;
15✔
234
            subspaceMap.computeIfAbsent(parent, k -> new HashSet<>()).add(child);
36✔
235
            superspaceMap.computeIfAbsent(child, k -> new HashSet<>()).add(parent);
36✔
236
        }
3✔
237
    }
3✔
238

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