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

knowledgepixels / nanodash / 23342091898

20 Mar 2026 12:06PM UTC coverage: 16.379% (+0.05%) from 16.327%
23342091898

push

github

web-flow
Merge pull request #410 from knowledgepixels/fix/space-and-resource-data-refresh

fix: update Space and MaintainedResource data when root nanopub changes

731 of 5533 branches covered (13.21%)

Branch coverage included in aggregate %.

1877 of 10390 relevant lines covered (18.07%)

2.47 hits per line

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

57.6
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.*;
15

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

205
    /**
206
     * Mark all spaces as needing a data update.
207
     */
208
    public void refresh() {
209
        refresh(ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACES), true));
×
210
        for (Space space : spaceList) {
×
211
            space.setDataNeedsUpdate();
×
212
        }
×
213
    }
×
214

215
    private Space getIdSuperspace(Space space) {
216
        String id = space.getId();
×
217
        if (!id.matches("https?://[^/]+/.*/[^/]*/?")) return null;
×
218
        String superId = id.replaceFirst("(https?://[^/]+/.*)/[^/]*/?", "$1");
×
219
        return spacesById.get(superId);
×
220
    }
221

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