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

knowledgepixels / nanodash / 17700776486

13 Sep 2025 07:00PM UTC coverage: 13.736% (-0.04%) from 13.777%
17700776486

push

github

tkuhn
feat(Spaces): Improve Spaces pages

433 of 3986 branches covered (10.86%)

Branch coverage included in aggregate %.

1112 of 7262 relevant lines covered (15.31%)

0.68 hits per line

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

0.0
src/main/java/com/knowledgepixels/nanodash/Space.java
1
package com.knowledgepixels.nanodash;
2

3
import static com.knowledgepixels.nanodash.Utils.vf;
4

5
import java.io.Serializable;
6
import java.util.ArrayList;
7
import java.util.Collections;
8
import java.util.HashMap;
9
import java.util.HashSet;
10
import java.util.List;
11
import java.util.Map;
12
import java.util.Set;
13

14
import org.eclipse.rdf4j.model.IRI;
15
import org.eclipse.rdf4j.model.Literal;
16
import org.eclipse.rdf4j.model.Statement;
17
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
18
import org.nanopub.Nanopub;
19
import org.nanopub.extra.services.ApiResponse;
20
import org.nanopub.extra.services.ApiResponseEntry;
21
import org.nanopub.vocabulary.NTEMPLATE;
22

23
import com.github.jsonldjava.shaded.com.google.common.collect.Ordering;
24
import com.knowledgepixels.nanodash.template.Template;
25
import com.knowledgepixels.nanodash.template.TemplateData;
26

27
/**
28
 * Class representing a "Space", which can be any kind of collaborative unit, like a project, group, or event.
29
 */
30
public class Space implements Serializable {
31

32
    /**
33
     * The predicate for the owner of the space.
34
     */
35
    public static final IRI HAS_OWNER = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasOwner");
×
36

37
    /**
38
     * The predicate for pinned templates in the space.
39
     */
40
    public static final IRI HAS_PINNED_TEMPLATE = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasPinnedTemplate");
×
41

42
    /**
43
     * The predicate for pinned queries in the space.
44
     */
45
    public static final IRI HAS_PINNED_QUERY = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasPinnedQuery");
×
46

47
    private static List<Space> spaceList;
48
    private static Map<String, List<Space>> spaceListByType;
49
    private static Map<String,Space> spacesByCoreInfo = new HashMap<>();
×
50
    private static Map<String,Space> spacesById;
51
    private static Map<Space,Set<Space>> subspaceMap;
52
    private static Map<Space,Set<Space>> superspaceMap;
53

54
    public static synchronized void refresh(ApiResponse resp) {
55
        spaceList = new ArrayList<>();
×
56
        spaceListByType = new HashMap<>();
×
57
        Map<String,Space> prevSpacesByCoreInfoPrev = spacesByCoreInfo;
×
58
        spacesByCoreInfo = new HashMap<>();
×
59
        spacesById = new HashMap<>();
×
60
        subspaceMap = new HashMap<>();
×
61
        superspaceMap = new HashMap<>();
×
62
        for (ApiResponseEntry entry : resp.getData()) {
×
63
            Space space = new Space(entry);
×
64
            Space prevSpace = prevSpacesByCoreInfoPrev.get(space.getCoreInfoString());
×
65
            if (prevSpace != null) space = prevSpace;
×
66
            spaceList.add(space);
×
67
            spaceListByType.computeIfAbsent(space.getType(), k -> new ArrayList<>()).add(space);
×
68
            spacesByCoreInfo.put(space.getCoreInfoString(), space);
×
69
            spacesById.put(space.getId(), space);
×
70
        }
×
71
        for (Space space : spaceList) {
×
72
            Space superSpace = space.getIdSuperspace();
×
73
            if (superSpace == null) continue;
×
74
            subspaceMap.computeIfAbsent(superSpace, k -> new HashSet<>()).add(space);
×
75
            superspaceMap.computeIfAbsent(space, k -> new HashSet<>()).add(superSpace);
×
76
        }
×
77
    }
×
78

79
    public static void ensureLoaded() {
80
        if (spaceList == null) {
×
81
            refresh(QueryApiAccess.forcedGet("get-spaces"));
×
82
        }
83
    }
×
84

85
    public static List<Space> getSpaceList() {
86
        ensureLoaded();
×
87
        return spaceList;
×
88
    }
89

90
    public static List<Space> getSpaceList(String type) {
91
        ensureLoaded();
×
92
        return spaceListByType.computeIfAbsent(type, k -> new ArrayList<>());
×
93
    }
94

95
    public static Space get(String id) {
96
        ensureLoaded();
×
97
        return spacesById.get(id);
×
98
    }
99

100
    public static void refresh() {
101
        ensureLoaded();
×
102
        for (Space space : spaceList) {
×
103
            space.dataNeedsUpdate = true;
×
104
        }
×
105
    }
×
106

107
    private String id, label, rootNanopubId, type;
108
    private Nanopub rootNanopub = null;
×
109
    private SpaceData data = new SpaceData();
×
110

111
    private static class SpaceData implements Serializable {
×
112
        String description = null;
×
113
        IRI defaultProvenance = null;
×
114
        List<IRI> owners = new ArrayList<>();
×
115
        List<IRI> members = new ArrayList<>();
×
116
        Map<String,IRI> ownerPubkeyMap = new HashMap<>();
×
117
        List<Serializable> pinnedResources = new ArrayList<>();
×
118
        Set<String> pinGroupTags = new HashSet<>();
×
119
        Map<String, List<Serializable>> pinnedResourceMap = new HashMap<>();
×
120

121
        void addOwner(IRI owner) {
122
            // TODO This isn't efficient for long owner lists:
123
            if (owners.contains(owner)) return;
×
124
            owners.add(owner);
×
125
            UserData ud = User.getUserData();
×
126
            for (String pubkeyhash : ud.getPubkeyhashes(owner, true)) {
×
127
                ownerPubkeyMap.put(pubkeyhash, owner);
×
128
            }
×
129
        }
×
130
    }
131

132
    private boolean dataInitialized = false;
×
133
    private boolean dataNeedsUpdate = true;
×
134

135
    private Space(ApiResponseEntry resp) {
×
136
        this.id = resp.get("space");
×
137
        this.label = resp.get("label");
×
138
        this.type = resp.get("type");
×
139
        this.rootNanopubId = resp.get("np");
×
140
        this.rootNanopub = Utils.getAsNanopub(rootNanopubId);
×
141
        setCoreData(data);
×
142
    }
×
143

144
    public String getId() {
145
        return id;
×
146
    }
147

148
    public String getRootNanopubId() {
149
        return rootNanopubId;
×
150
    }
151

152
    public String getCoreInfoString() {
153
        return id + " " + rootNanopubId;
×
154
    }
155

156
    public Nanopub getRootNanopub() {
157
        return rootNanopub;
×
158
    }
159

160
    public String getLabel() {
161
        return label;
×
162
    }
163

164
    public String getType() {
165
        return type;
×
166
    }
167

168
    public String getTypeLabel() {
169
        return type.replaceFirst("^.*/", "");
×
170
    }
171

172
    public String getDescription() {
173
        return data.description;
×
174
    }
175

176
    public boolean isDataInitialized() {
177
        triggerDataUpdate();
×
178
        return dataInitialized;
×
179
    }
180

181
    public List<IRI> getOwners() {
182
        triggerDataUpdate();
×
183
        return data.owners;
×
184
    }
185

186
    public List<IRI> getMembers() {
187
        triggerDataUpdate();
×
188
        return data.members;
×
189
    }
190

191
    public List<Serializable> getPinnedResources() {
192
        triggerDataUpdate();
×
193
        return data.pinnedResources;
×
194
    }
195

196
    public Set<String> getPinGroupTags() {
197
        triggerDataUpdate();
×
198
        return data.pinGroupTags;
×
199
    }
200

201
    public Map<String, List<Serializable>> getPinnedResourceMap() {
202
        triggerDataUpdate();
×
203
        return data.pinnedResourceMap;
×
204
    }
205

206
    public IRI getDefaultProvenance() {
207
        return data.defaultProvenance;
×
208
    }
209

210
    public String getSuperId() {
211
        return null;
×
212
    }
213

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

223
    public List<Space> getSuperspaces() {
224
        if (superspaceMap.containsKey(this)) {
×
225
            List<Space> superspaces = new ArrayList<>(superspaceMap.get(this));
×
226
            Collections.sort(superspaces, Ordering.usingToString());
×
227
            return superspaces;
×
228
        }
229
        return new ArrayList<>();
×
230
    }
231

232
    public List<Space> getSubspaces() {
233
        if (subspaceMap.containsKey(this)) {
×
234
            List<Space> subspaces = new ArrayList<>(subspaceMap.get(this));
×
235
            Collections.sort(subspaces, Ordering.usingToString());
×
236
            return subspaces;
×
237
        }
238
        return new ArrayList<>();
×
239
    }
240

241
    public List<Space> getSubspaces(String type) {
242
        List<Space> l = new ArrayList<>();
×
243
        for (Space s : getSubspaces()) {
×
244
            if (s.getType().equals(type)) l.add(s);
×
245
        }
×
246
        return l;
×
247
    }
248

249
    private synchronized void triggerDataUpdate() {
250
        if (dataNeedsUpdate) {
×
251
            new Thread(() -> {
×
252
                SpaceData newData = new SpaceData();
×
253
                setCoreData(newData);
×
254

255
                for (ApiResponseEntry r : QueryApiAccess.forcedGet("get-owners", "unit", id).getData()) {
×
256
                    String pubkeyhash = r.get("pubkeyhash");
×
257
                    if (newData.ownerPubkeyMap.containsKey(pubkeyhash)) {
×
258
                        newData.addOwner(Utils.vf.createIRI(r.get("owner")));
×
259
                    }
260
                }
×
261
                newData.members = new ArrayList<>();
×
262
                for (ApiResponseEntry r : QueryApiAccess.forcedGet("get-members", "unit", id).getData()) {
×
263
                    IRI memberId = Utils.vf.createIRI(r.get("member"));
×
264
                    // TODO These checks are inefficient for long member lists:
265
                    if (newData.owners.contains(memberId)) continue;
×
266
                    if (newData.members.contains(memberId)) continue;
×
267
                    newData.members.add(memberId);
×
268
                }
×
269
                newData.owners.sort(User.getUserData().userComparator);
×
270
                newData.members.sort(User.getUserData().userComparator);
×
271

272
                for (ApiResponseEntry r : QueryApiAccess.forcedGet("get-pinned-templates", "space", id).getData()) {
×
273
                    if (!newData.ownerPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
274
                    Template t = TemplateData.get().getTemplate(r.get("template"));
×
275
                    if (t == null) continue;
×
276
                    newData.pinnedResources.add(t);
×
277
                    String tag = r.get("tag");
×
278
                    if (tag != null && !tag.isEmpty()) {
×
279
                        newData.pinGroupTags.add(r.get("tag"));
×
280
                        newData.pinnedResourceMap.computeIfAbsent(tag, k -> new ArrayList<>()).add(TemplateData.get().getTemplate(r.get("template")));
×
281
                    }
282
                }
×
283
                for (ApiResponseEntry r : QueryApiAccess.forcedGet("get-pinned-queries", "space", id).getData()) {
×
284
                    if (!newData.ownerPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
285
                    GrlcQuery query = GrlcQuery.get(r.get("query"));
×
286
                    if (query == null) continue;
×
287
                    newData.pinnedResources.add(query);
×
288
                    String tag = r.get("tag");
×
289
                    if (tag != null && !tag.isEmpty()) {
×
290
                        newData.pinGroupTags.add(r.get("tag"));
×
291
                        newData.pinnedResourceMap.computeIfAbsent(tag, k -> new ArrayList<>()).add(query);
×
292
                    }
293
                }
×
294
                data = newData;
×
295
                dataInitialized = true;
×
296
            }).start();
×
297
            dataNeedsUpdate = false;
×
298
        }
299
    }
×
300

301
    private void setCoreData(SpaceData data) {
302
        for (Statement st : rootNanopub.getAssertion()) {
×
303
            if (st.getSubject().stringValue().equals(getId())) {
×
304
                if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
×
305
                    data.description = st.getObject().stringValue();
×
306
                } else if (st.getPredicate().equals(HAS_OWNER) && st.getObject() instanceof IRI obj) {
×
307
                    data.addOwner(obj);
×
308
                } else if (st.getPredicate().equals(HAS_PINNED_TEMPLATE) && st.getObject() instanceof IRI obj) {
×
309
                    data.pinnedResources.add(TemplateData.get().getTemplate(obj.stringValue()));
×
310
                } else if (st.getPredicate().equals(HAS_PINNED_QUERY) && st.getObject() instanceof IRI obj) {
×
311
                    data.pinnedResources.add(GrlcQuery.get(obj.stringValue()));
×
312
                } else if (st.getPredicate().equals(NTEMPLATE.HAS_DEFAULT_PROVENANCE) && st.getObject() instanceof IRI obj) {
×
313
                    data.defaultProvenance = obj;
×
314
                }
315
            } else if (st.getPredicate().equals(NTEMPLATE.HAS_TAG) && st.getObject() instanceof Literal l) {
×
316
                data.pinGroupTags.add(l.stringValue());
×
317
                List<Serializable> list = data.pinnedResourceMap.get(l.stringValue());
×
318
                if (list == null) {
×
319
                    list = new ArrayList<>();
×
320
                    data.pinnedResourceMap.put(l.stringValue(), list);
×
321
                }
322
                list.add(TemplateData.get().getTemplate(st.getSubject().stringValue()));
×
323
            }
324
        }
×
325
    }
×
326

327
    @Override
328
    public String toString() {
329
        return id;
×
330
    }
331

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