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

knowledgepixels / nanodash / 17852532121

19 Sep 2025 08:10AM UTC coverage: 13.568% (-0.3%) from 13.87%
17852532121

push

github

tkuhn
feat: Switch to QueryRef provided by nanopub, using multimap

428 of 4008 branches covered (10.68%)

Branch coverage included in aggregate %.

1104 of 7283 relevant lines covered (15.16%)

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.extra.services.QueryRef;
22
import org.nanopub.vocabulary.NTEMPLATE;
23

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

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

33
    /**
34
     * The predicate to assign the admins of the space.
35
     */
36
    public static final IRI HAS_ADMIN = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasAdmin");
×
37

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

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

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

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

82
    public static boolean isLoaded() {
83
        return loaded;
×
84
    }
85

86
    public static void ensureLoaded() {
87
        if (spaceList == null) {
×
88
            refresh(QueryApiAccess.forcedGet(new QueryRef("get-spaces")));
×
89
        }
90
    }
×
91

92
    public static List<Space> getSpaceList() {
93
        ensureLoaded();
×
94
        return spaceList;
×
95
    }
96

97
    public static List<Space> getSpaceList(String type) {
98
        ensureLoaded();
×
99
        return spaceListByType.computeIfAbsent(type, k -> new ArrayList<>());
×
100
    }
101

102
    public static Space get(String id) {
103
        ensureLoaded();
×
104
        return spacesById.get(id);
×
105
    }
106

107
    public static void refresh() {
108
        ensureLoaded();
×
109
        for (Space space : spaceList) {
×
110
            space.dataNeedsUpdate = true;
×
111
        }
×
112
    }
×
113

114
    private String id, label, rootNanopubId, type;
115
    private Nanopub rootNanopub = null;
×
116
    private SpaceData data = new SpaceData();
×
117

118
    private static class SpaceData implements Serializable {
×
119
        String description = null;
×
120
        IRI defaultProvenance = null;
×
121
        List<IRI> admins = new ArrayList<>();
×
122
        List<IRI> members = new ArrayList<>();
×
123
        Map<String,IRI> adminPubkeyMap = new HashMap<>();
×
124
        List<Serializable> pinnedResources = new ArrayList<>();
×
125
        Set<String> pinGroupTags = new HashSet<>();
×
126
        Map<String, List<Serializable>> pinnedResourceMap = new HashMap<>();
×
127

128
        void addAdmin(IRI admin) {
129
            // TODO This isn't efficient for long owner lists:
130
            if (admins.contains(admin)) return;
×
131
            admins.add(admin);
×
132
            UserData ud = User.getUserData();
×
133
            for (String pubkeyhash : ud.getPubkeyhashes(admin, true)) {
×
134
                adminPubkeyMap.put(pubkeyhash, admin);
×
135
            }
×
136
        }
×
137
    }
138

139
    private boolean dataInitialized = false;
×
140
    private boolean dataNeedsUpdate = true;
×
141

142
    private Space(ApiResponseEntry resp) {
×
143
        this.id = resp.get("space");
×
144
        this.label = resp.get("label");
×
145
        this.type = resp.get("type");
×
146
        this.rootNanopubId = resp.get("np");
×
147
        this.rootNanopub = Utils.getAsNanopub(rootNanopubId);
×
148
        setCoreData(data);
×
149
    }
×
150

151
    public String getId() {
152
        return id;
×
153
    }
154

155
    public String getRootNanopubId() {
156
        return rootNanopubId;
×
157
    }
158

159
    public String getCoreInfoString() {
160
        return id + " " + rootNanopubId;
×
161
    }
162

163
    public Nanopub getRootNanopub() {
164
        return rootNanopub;
×
165
    }
166

167
    public String getLabel() {
168
        return label;
×
169
    }
170

171
    public String getType() {
172
        return type;
×
173
    }
174

175
    public String getTypeLabel() {
176
        return type.replaceFirst("^.*/", "");
×
177
    }
178

179
    public String getDescription() {
180
        return data.description;
×
181
    }
182

183
    public boolean isDataInitialized() {
184
        triggerDataUpdate();
×
185
        return dataInitialized;
×
186
    }
187

188
    public List<IRI> getAdmins() {
189
        triggerDataUpdate();
×
190
        return data.admins;
×
191
    }
192

193
    public List<IRI> getMembers() {
194
        triggerDataUpdate();
×
195
        return data.members;
×
196
    }
197

198
    public boolean isMember(IRI userId) {
199
        triggerDataUpdate();
×
200
        // TODO This is inefficient for large member lists:
201
        return data.admins.contains(userId) || data.members.contains(userId);
×
202
    }
203

204
    public List<Serializable> getPinnedResources() {
205
        triggerDataUpdate();
×
206
        return data.pinnedResources;
×
207
    }
208

209
    public Set<String> getPinGroupTags() {
210
        triggerDataUpdate();
×
211
        return data.pinGroupTags;
×
212
    }
213

214
    public Map<String, List<Serializable>> getPinnedResourceMap() {
215
        triggerDataUpdate();
×
216
        return data.pinnedResourceMap;
×
217
    }
218

219
    public IRI getDefaultProvenance() {
220
        return data.defaultProvenance;
×
221
    }
222

223
    public String getSuperId() {
224
        return null;
×
225
    }
226

227
    public Space getIdSuperspace() {
228
        if (!id.matches("https?://[^/]+/.*/[^/]*/?")) return null;
×
229
        String superId = id.replaceFirst("(https?://[^/]+/.*)/[^/]*/?", "$1");
×
230
        if (spacesById.containsKey(superId)) {
×
231
            return spacesById.get(superId);
×
232
        }
233
        return null;
×
234
    }
235

236
    public List<Space> getSuperspaces() {
237
        if (superspaceMap.containsKey(this)) {
×
238
            List<Space> superspaces = new ArrayList<>(superspaceMap.get(this));
×
239
            Collections.sort(superspaces, Ordering.usingToString());
×
240
            return superspaces;
×
241
        }
242
        return new ArrayList<>();
×
243
    }
244

245
    public List<Space> getSubspaces() {
246
        if (subspaceMap.containsKey(this)) {
×
247
            List<Space> subspaces = new ArrayList<>(subspaceMap.get(this));
×
248
            Collections.sort(subspaces, Ordering.usingToString());
×
249
            return subspaces;
×
250
        }
251
        return new ArrayList<>();
×
252
    }
253

254
    public List<Space> getSubspaces(String type) {
255
        List<Space> l = new ArrayList<>();
×
256
        for (Space s : getSubspaces()) {
×
257
            if (s.getType().equals(type)) l.add(s);
×
258
        }
×
259
        return l;
×
260
    }
261

262
    private synchronized void triggerDataUpdate() {
263
        if (dataNeedsUpdate) {
×
264
            new Thread(() -> {
×
265
                SpaceData newData = new SpaceData();
×
266
                setCoreData(newData);
×
267

268
                for (ApiResponseEntry r : QueryApiAccess.forcedGet(new QueryRef("get-admins", "unit", id)).getData()) {
×
269
                    String pubkeyhash = r.get("pubkeyhash");
×
270
                    if (newData.adminPubkeyMap.containsKey(pubkeyhash)) {
×
271
                        newData.addAdmin(Utils.vf.createIRI(r.get("admin")));
×
272
                    }
273
                }
×
274
                newData.members = new ArrayList<>();
×
275
                for (ApiResponseEntry r : QueryApiAccess.forcedGet(new QueryRef("get-members", "unit", id)).getData()) {
×
276
                    IRI memberId = Utils.vf.createIRI(r.get("member"));
×
277
                    // TODO These checks are inefficient for long member lists:
278
                    if (newData.admins.contains(memberId)) continue;
×
279
                    if (newData.members.contains(memberId)) continue;
×
280
                    newData.members.add(memberId);
×
281
                }
×
282
                newData.admins.sort(User.getUserData().userComparator);
×
283
                newData.members.sort(User.getUserData().userComparator);
×
284

285
                for (ApiResponseEntry r : QueryApiAccess.forcedGet(new QueryRef("get-pinned-templates", "space", id)).getData()) {
×
286
                    if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
287
                    Template t = TemplateData.get().getTemplate(r.get("template"));
×
288
                    if (t == null) continue;
×
289
                    newData.pinnedResources.add(t);
×
290
                    String tag = r.get("tag");
×
291
                    if (tag != null && !tag.isEmpty()) {
×
292
                        newData.pinGroupTags.add(r.get("tag"));
×
293
                        newData.pinnedResourceMap.computeIfAbsent(tag, k -> new ArrayList<>()).add(TemplateData.get().getTemplate(r.get("template")));
×
294
                    }
295
                }
×
296
                for (ApiResponseEntry r : QueryApiAccess.forcedGet(new QueryRef("get-pinned-queries", "space", id)).getData()) {
×
297
                    if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
298
                    GrlcQuery query = GrlcQuery.get(r.get("query"));
×
299
                    if (query == null) continue;
×
300
                    newData.pinnedResources.add(query);
×
301
                    String tag = r.get("tag");
×
302
                    if (tag != null && !tag.isEmpty()) {
×
303
                        newData.pinGroupTags.add(r.get("tag"));
×
304
                        newData.pinnedResourceMap.computeIfAbsent(tag, k -> new ArrayList<>()).add(query);
×
305
                    }
306
                }
×
307
                data = newData;
×
308
                dataInitialized = true;
×
309
            }).start();
×
310
            dataNeedsUpdate = false;
×
311
        }
312
    }
×
313

314
    private void setCoreData(SpaceData data) {
315
        for (Statement st : rootNanopub.getAssertion()) {
×
316
            if (st.getSubject().stringValue().equals(getId())) {
×
317
                if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
×
318
                    data.description = st.getObject().stringValue();
×
319
                } else if (st.getPredicate().equals(HAS_ADMIN) && st.getObject() instanceof IRI obj) {
×
320
                    data.addAdmin(obj);
×
321
                } else if (st.getPredicate().equals(HAS_PINNED_TEMPLATE) && st.getObject() instanceof IRI obj) {
×
322
                    data.pinnedResources.add(TemplateData.get().getTemplate(obj.stringValue()));
×
323
                } else if (st.getPredicate().equals(HAS_PINNED_QUERY) && st.getObject() instanceof IRI obj) {
×
324
                    data.pinnedResources.add(GrlcQuery.get(obj.stringValue()));
×
325
                } else if (st.getPredicate().equals(NTEMPLATE.HAS_DEFAULT_PROVENANCE) && st.getObject() instanceof IRI obj) {
×
326
                    data.defaultProvenance = obj;
×
327
                }
328
            } else if (st.getPredicate().equals(NTEMPLATE.HAS_TAG) && st.getObject() instanceof Literal l) {
×
329
                data.pinGroupTags.add(l.stringValue());
×
330
                List<Serializable> list = data.pinnedResourceMap.get(l.stringValue());
×
331
                if (list == null) {
×
332
                    list = new ArrayList<>();
×
333
                    data.pinnedResourceMap.put(l.stringValue(), list);
×
334
                }
335
                list.add(TemplateData.get().getTemplate(st.getSubject().stringValue()));
×
336
            }
337
        }
×
338
    }
×
339

340
    @Override
341
    public String toString() {
342
        return id;
×
343
    }
344

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