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

knowledgepixels / nanodash / 18531946059

15 Oct 2025 02:15PM UTC coverage: 13.615% (-0.9%) from 14.516%
18531946059

push

github

tkuhn
fix: Show pinned resources only once

452 of 4192 branches covered (10.78%)

Branch coverage included in aggregate %.

1171 of 7729 relevant lines covered (15.15%)

0.68 hits per line

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

2.35
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.time.format.DateTimeParseException;
7
import java.util.ArrayList;
8
import java.util.Calendar;
9
import java.util.Collections;
10
import java.util.HashMap;
11
import java.util.HashSet;
12
import java.util.List;
13
import java.util.Map;
14
import java.util.Set;
15

16
import org.eclipse.rdf4j.model.IRI;
17
import org.eclipse.rdf4j.model.Literal;
18
import org.eclipse.rdf4j.model.Statement;
19
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
20
import org.eclipse.rdf4j.model.vocabulary.OWL;
21
import org.nanopub.Nanopub;
22
import org.nanopub.extra.services.ApiResponse;
23
import org.nanopub.extra.services.ApiResponseEntry;
24
import org.nanopub.extra.services.QueryRef;
25
import org.nanopub.vocabulary.NTEMPLATE;
26
import org.slf4j.Logger;
27
import org.slf4j.LoggerFactory;
28

29
import com.github.jsonldjava.shaded.com.google.common.collect.Ordering;
30
import com.google.common.collect.ArrayListMultimap;
31
import com.google.common.collect.Multimap;
32
import com.knowledgepixels.nanodash.template.Template;
33
import com.knowledgepixels.nanodash.template.TemplateData;
34

35
import jakarta.xml.bind.DatatypeConverter;
36

37
/**
38
 * Class representing a "Space", which can be any kind of collaborative unit, like a project, group, or event.
39
 */
40
public class Space implements Serializable {
41

42
    private static final Logger logger = LoggerFactory.getLogger(Space.class);
3✔
43

44
    /**
45
     * The predicate to assign the admins of the space.
46
     */
47
    public static final IRI HAS_ADMIN = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasAdmin");
4✔
48

49
    /**
50
     * The predicate for pinned templates in the space.
51
     */
52
    public static final IRI HAS_PINNED_TEMPLATE = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasPinnedTemplate");
4✔
53

54
    /**
55
     * The predicate for pinned queries in the space.
56
     */
57
    public static final IRI HAS_PINNED_QUERY = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasPinnedQuery");
4✔
58

59
    private static List<Space> spaceList;
60
    private static Map<String, List<Space>> spaceListByType;
61
    private static Map<String, Space> spacesByCoreInfo = new HashMap<>();
4✔
62
    private static Map<String, Space> spacesById;
63
    private static Map<Space, Set<Space>> subspaceMap;
64
    private static Map<Space, Set<Space>> superspaceMap;
65
    private static boolean loaded = false;
3✔
66

67
    /**
68
     * Refresh the list of spaces from the API response.
69
     *
70
     * @param resp The API response containing space data.
71
     */
72
    public static synchronized void refresh(ApiResponse resp) {
73
        spaceList = new ArrayList<>();
×
74
        spaceListByType = new HashMap<>();
×
75
        Map<String, Space> prevSpacesByCoreInfoPrev = spacesByCoreInfo;
×
76
        spacesByCoreInfo = new HashMap<>();
×
77
        spacesById = new HashMap<>();
×
78
        subspaceMap = new HashMap<>();
×
79
        superspaceMap = new HashMap<>();
×
80
        for (ApiResponseEntry entry : resp.getData()) {
×
81
            Space space = new Space(entry);
×
82
            Space prevSpace = prevSpacesByCoreInfoPrev.get(space.getCoreInfoString());
×
83
            if (prevSpace != null) space = prevSpace;
×
84
            spaceList.add(space);
×
85
            spaceListByType.computeIfAbsent(space.getType(), k -> new ArrayList<>()).add(space);
×
86
            spacesByCoreInfo.put(space.getCoreInfoString(), space);
×
87
            spacesById.put(space.getId(), space);
×
88
        }
×
89
        for (Space space : spaceList) {
×
90
            Space superSpace = space.getIdSuperspace();
×
91
            if (superSpace == null) continue;
×
92
            subspaceMap.computeIfAbsent(superSpace, k -> new HashSet<>()).add(space);
×
93
            superspaceMap.computeIfAbsent(space, k -> new HashSet<>()).add(superSpace);
×
94
        }
×
95
        loaded = true;
×
96
    }
×
97

98
    /**
99
     * Check if the spaces have been loaded.
100
     *
101
     * @return true if loaded, false otherwise.
102
     */
103
    public static boolean isLoaded() {
104
        return loaded;
×
105
    }
106

107
    /**
108
     * Ensure that the spaces are loaded, fetching them from the API if necessary.
109
     */
110
    public static void ensureLoaded() {
111
        if (spaceList == null) {
2!
112
            refresh(QueryApiAccess.forcedGet(new QueryRef("get-spaces")));
×
113
        }
114
    }
×
115

116
    /**
117
     * Get the list of all spaces.
118
     *
119
     * @return List of spaces.
120
     */
121
    public static List<Space> getSpaceList() {
122
        ensureLoaded();
×
123
        return spaceList;
×
124
    }
125

126
    /**
127
     * Get the list of spaces of a specific type.
128
     *
129
     * @param type The type of spaces to retrieve.
130
     * @return List of spaces of the specified type.
131
     */
132
    public static List<Space> getSpaceList(String type) {
133
        ensureLoaded();
×
134
        return spaceListByType.computeIfAbsent(type, k -> new ArrayList<>());
×
135
    }
136

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

148
    /**
149
     * Mark all spaces as needing a data update.
150
     */
151
    public static void refresh() {
152
        ensureLoaded();
×
153
        for (Space space : spaceList) {
×
154
            space.dataNeedsUpdate = true;
×
155
        }
×
156
    }
×
157

158
    private String id, label, rootNanopubId, type;
159
    private Nanopub rootNanopub = null;
×
160
    private SpaceData data = new SpaceData();
×
161

162
    private static class SpaceData implements Serializable {
×
163

164
        List<String> altIds = new ArrayList<>();
×
165

166
        String description = null;
×
167
        Calendar startDate, endDate;
168
        IRI defaultProvenance = null;
×
169

170
        List<IRI> admins = new ArrayList<>();
×
171
        Map<IRI, Set<SpaceMemberRole>> users = new HashMap<>();
×
172
        List<SpaceMemberRole> roles = new ArrayList<>();
×
173
        Map<IRI, SpaceMemberRole> roleMap = new HashMap<>();
×
174

175
        Map<String, IRI> adminPubkeyMap = new HashMap<>();
×
176
        Set<Serializable> pinnedResources = new HashSet<>();
×
177
        List<SpaceQueryView> views = new ArrayList<>();
×
178
        Set<String> pinGroupTags = new HashSet<>();
×
179
        Map<String, Set<Serializable>> pinnedResourceMap = new HashMap<>();
×
180

181
        void addAdmin(IRI admin) {
182
            // TODO This isn't efficient for long owner lists:
183
            if (admins.contains(admin)) return;
×
184
            admins.add(admin);
×
185
            UserData ud = User.getUserData();
×
186
            for (String pubkeyhash : ud.getPubkeyhashes(admin, true)) {
×
187
                adminPubkeyMap.put(pubkeyhash, admin);
×
188
            }
×
189
        }
×
190

191
    }
192

193
    private boolean dataInitialized = false;
×
194
    private boolean dataNeedsUpdate = true;
×
195

196
    private Space(ApiResponseEntry resp) {
×
197
        this.id = resp.get("space");
×
198
        this.label = resp.get("label");
×
199
        this.type = resp.get("type");
×
200
        this.rootNanopubId = resp.get("np");
×
201
        this.rootNanopub = Utils.getAsNanopub(rootNanopubId);
×
202
        setCoreData(data);
×
203
    }
×
204

205
    /**
206
     * Get the ID of the space.
207
     *
208
     * @return The space ID.
209
     */
210
    public String getId() {
211
        return id;
×
212
    }
213

214
    /**
215
     * Get the root nanopublication ID of the space.
216
     *
217
     * @return The root nanopub ID.
218
     */
219
    public String getRootNanopubId() {
220
        return rootNanopubId;
×
221
    }
222

223
    /**
224
     * Get a string combining the space ID and root nanopub ID for core identification.
225
     *
226
     * @return The core info string.
227
     */
228
    public String getCoreInfoString() {
229
        return id + " " + rootNanopubId;
×
230
    }
231

232
    /**
233
     * Get the root nanopublication of the space.
234
     *
235
     * @return The root Nanopub object.
236
     */
237
    public Nanopub getRootNanopub() {
238
        return rootNanopub;
×
239
    }
240

241
    /**
242
     * Get the label of the space.
243
     *
244
     * @return The space label.
245
     */
246
    public String getLabel() {
247
        return label;
×
248
    }
249

250
    /**
251
     * Get the type of the space.
252
     *
253
     * @return The space type.
254
     */
255
    public String getType() {
256
        return type;
×
257
    }
258

259
    /**
260
     * Get the start date of the space.
261
     *
262
     * @return The start date as a Calendar object, or null if not set.
263
     */
264
    public Calendar getStartDate() {
265
        return data.startDate;
×
266
    }
267

268
    /**
269
     * Get the end date of the space.
270
     *
271
     * @return The end date as a Calendar object, or null if not set.
272
     */
273
    public Calendar getEndDate() {
274
        return data.endDate;
×
275
    }
276

277
    /**
278
     * Get a simplified label for the type of space by removing any namespace prefix.
279
     *
280
     * @return The simplified type label.
281
     */
282
    public String getTypeLabel() {
283
        return type.replaceFirst("^.*/", "");
×
284
    }
285

286
    /**
287
     * Get the description of the space.
288
     *
289
     * @return The description string.
290
     */
291
    public String getDescription() {
292
        return data.description;
×
293
    }
294

295
    /**
296
     * Check if the space data has been initialized.
297
     *
298
     * @return true if initialized, false otherwise.
299
     */
300
    public boolean isDataInitialized() {
301
        triggerDataUpdate();
×
302
        return dataInitialized;
×
303
    }
304

305
    /**
306
     * Get the list of admins in this space.
307
     *
308
     * @return List of admin IRIs.
309
     */
310
    public List<IRI> getAdmins() {
311
        triggerDataUpdate();
×
312
        return data.admins;
×
313
    }
314

315
    /**
316
     * Get the list of members in this space.
317
     *
318
     * @return List of member IRIs.
319
     */
320
    public List<IRI> getUsers() {
321
        triggerDataUpdate();
×
322
        List<IRI> users = new ArrayList<IRI>(data.users.keySet());
×
323
        users.sort(User.getUserData().userComparator);
×
324
        return users;
×
325
    }
326

327
    /**
328
     * Get the roles of a specific member in this space.
329
     *
330
     * @param userId The IRI of the member.
331
     * @return Set of roles assigned to the member, or null if the member is not part of this space.
332
     */
333
    public Set<SpaceMemberRole> getMemberRoles(IRI userId) {
334
        triggerDataUpdate();
×
335
        return data.users.get(userId);
×
336
    }
337

338
    /**
339
     * Check if a user is a member of this space.
340
     *
341
     * @param userId The IRI of the user to check.
342
     * @return true if the user is a member, false otherwise.
343
     */
344
    public boolean isMember(IRI userId) {
345
        triggerDataUpdate();
×
346
        return data.users.containsKey(userId);
×
347
    }
348

349
    /**
350
     * Get the list of pinned resources in this space.
351
     *
352
     * @return List of pinned resources.
353
     */
354
    public Set<Serializable> getPinnedResources() {
355
        triggerDataUpdate();
×
356
        return data.pinnedResources;
×
357
    }
358

359
    /**
360
     * Get the set of tags used for grouping pinned resources.
361
     *
362
     * @return Set of tags.
363
     */
364
    public Set<String> getPinGroupTags() {
365
        triggerDataUpdate();
×
366
        return data.pinGroupTags;
×
367
    }
368

369
    /**
370
     * Get a map of pinned resources grouped by their tags.
371
     *
372
     * @return Map where keys are tags and values are lists of pinned resources (Templates or GrlcQueries).
373
     */
374
    public Map<String, Set<Serializable>> getPinnedResourceMap() {
375
        triggerDataUpdate();
×
376
        return data.pinnedResourceMap;
×
377
    }
378

379
    /**
380
     * Get the list of views (GrlcQueries) associated with this space.
381
     *
382
     * @return List of GrlcQuery views.
383
     */
384
    public List<SpaceQueryView> getViews() {
385
        return data.views;
×
386
    }
387

388
    /**
389
     * Get the default provenance IRI for this space.
390
     *
391
     * @return The default provenance IRI, or null if not set.
392
     */
393
    public IRI getDefaultProvenance() {
394
        return data.defaultProvenance;
×
395
    }
396

397
    /**
398
     * Get the roles defined in this space.
399
     *
400
     * @return List of roles.
401
     */
402
    public List<SpaceMemberRole> getRoles() {
403
        return data.roles;
×
404
    }
405

406
    /**
407
     * Get the super ID of the space.
408
     *
409
     * @return Always returns null. Use getIdSuperspace() instead.
410
     */
411
    public String getSuperId() {
412
        return null;
×
413
    }
414

415
    /**
416
     * Get the superspace ID.
417
     *
418
     * @return The superspace, or null if not applicable.
419
     */
420
    public Space getIdSuperspace() {
421
        if (!id.matches("https?://[^/]+/.*/[^/]*/?")) return null;
×
422
        String superId = id.replaceFirst("(https?://[^/]+/.*)/[^/]*/?", "$1");
×
423
        if (spacesById.containsKey(superId)) {
×
424
            return spacesById.get(superId);
×
425
        }
426
        return null;
×
427
    }
428

429
    /**
430
     * Get superspaces of this space.
431
     *
432
     * @return List of superspaces.
433
     */
434
    public List<Space> getSuperspaces() {
435
        if (superspaceMap.containsKey(this)) {
×
436
            List<Space> superspaces = new ArrayList<>(superspaceMap.get(this));
×
437
            Collections.sort(superspaces, Ordering.usingToString());
×
438
            return superspaces;
×
439
        }
440
        return new ArrayList<>();
×
441
    }
442

443
    /**
444
     * Get subspaces of this space.
445
     *
446
     * @return List of subspaces.
447
     */
448
    public List<Space> getSubspaces() {
449
        if (subspaceMap.containsKey(this)) {
×
450
            List<Space> subspaces = new ArrayList<>(subspaceMap.get(this));
×
451
            Collections.sort(subspaces, Ordering.usingToString());
×
452
            return subspaces;
×
453
        }
454
        return new ArrayList<>();
×
455
    }
456

457
    /**
458
     * Get subspaces of a specific type.
459
     *
460
     * @param type The type of subspaces to retrieve.
461
     * @return List of subspaces of the specified type.
462
     */
463
    public List<Space> getSubspaces(String type) {
464
        List<Space> l = new ArrayList<>();
×
465
        for (Space s : getSubspaces()) {
×
466
            if (s.getType().equals(type)) l.add(s);
×
467
        }
×
468
        return l;
×
469
    }
470

471
    /**
472
     * Get alternative IDs for the space.
473
     *
474
     * @return List of alternative IDs.
475
     */
476
    public List<String> getAltIDs() {
477
        return data.altIds;
×
478
    }
479

480
    private synchronized void triggerDataUpdate() {
481
        if (dataNeedsUpdate) {
×
482
            new Thread(() -> {
×
483
                try {
484
                    SpaceData newData = new SpaceData();
×
485
                    setCoreData(newData);
×
486

487
                    newData.roles.add(SpaceMemberRole.ADMIN_ROLE);
×
488
                    newData.roleMap.put(SpaceMemberRole.HAS_ADMIN_PREDICATE, SpaceMemberRole.ADMIN_ROLE);
×
489

490
                    Multimap<String, String> spaceIds = ArrayListMultimap.create();
×
491
                    spaceIds.put("space", id);
×
492
                    for (String id : newData.altIds) {
×
493
                        spaceIds.put("space", id);
×
494
                    }
×
495

496
                    for (ApiResponseEntry r : QueryApiAccess.get(new QueryRef("get-admins", spaceIds)).getData()) {
×
497
                        String pubkeyhash = r.get("pubkey");
×
498
                        if (newData.adminPubkeyMap.containsKey(pubkeyhash)) {
×
499
                            IRI adminId = Utils.vf.createIRI(r.get("admin"));
×
500
                            newData.addAdmin(adminId);
×
501
                            newData.users.computeIfAbsent(adminId, (k) -> new HashSet<>()).add(SpaceMemberRole.ADMIN_ROLE);
×
502
                        }
503
                    }
×
504
                    newData.admins.sort(User.getUserData().userComparator);
×
505

506
                    Multimap<String, String> getSpaceMemberParams = ArrayListMultimap.create(spaceIds);
×
507

508
                    for (ApiResponseEntry r : QueryApiAccess.get(new QueryRef("get-space-member-roles", spaceIds)).getData()) {
×
509
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
510
                        SpaceMemberRole role = new SpaceMemberRole(r);
×
511
                        newData.roles.add(role);
×
512

513
                        // TODO Handle cases of overlapping properties:
514
                        for (IRI p : role.getRegularProperties()) newData.roleMap.put(p, role);
×
515
                        for (IRI p : role.getInverseProperties()) newData.roleMap.put(p, role);
×
516

517
                        role.addRoleParams(getSpaceMemberParams);
×
518
                    }
×
519

520
                    for (ApiResponseEntry r : QueryApiAccess.get(new QueryRef("get-space-members", getSpaceMemberParams)).getData()) {
×
521
                        IRI memberId = Utils.vf.createIRI(r.get("member"));
×
522
                        SpaceMemberRole role = newData.roleMap.get(Utils.vf.createIRI(r.get("role")));
×
523
                        newData.users.computeIfAbsent(memberId, (k) -> new HashSet<>()).add(role);
×
524
                    }
×
525

526
                    for (ApiResponseEntry r : QueryApiAccess.get(new QueryRef("get-pinned-templates", spaceIds)).getData()) {
×
527
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
528
                        Template t = TemplateData.get().getTemplate(r.get("template"));
×
529
                        if (t == null) continue;
×
530
                        newData.pinnedResources.add(t);
×
531
                        String tag = r.get("tag");
×
532
                        if (tag != null && !tag.isEmpty()) {
×
533
                            newData.pinGroupTags.add(r.get("tag"));
×
534
                            newData.pinnedResourceMap.computeIfAbsent(tag, k -> new HashSet<>()).add(TemplateData.get().getTemplate(r.get("template")));
×
535
                        }
536
                    }
×
537
                    for (ApiResponseEntry r : QueryApiAccess.get(new QueryRef("get-pinned-queries", spaceIds)).getData()) {
×
538
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
539
                        GrlcQuery query = GrlcQuery.get(r.get("query"));
×
540
                        if (query == null) continue;
×
541
                        newData.pinnedResources.add(query);
×
542
                        String tag = r.get("tag");
×
543
                        if (tag != null && !tag.isEmpty()) {
×
544
                            newData.pinGroupTags.add(r.get("tag"));
×
545
                            newData.pinnedResourceMap.computeIfAbsent(tag, k -> new HashSet<>()).add(query);
×
546
                        }
547
                    }
×
548
                    for (ApiResponseEntry r : QueryApiAccess.get(new QueryRef("get-views-for-space", spaceIds)).getData()) {
×
549
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
550
                        GrlcQuery query = GrlcQuery.get(r.get("query"));
×
551
                        if (query == null) continue;
×
552
                        SpaceQueryView view = new SpaceQueryView(this, query, r.get("title"));
×
553
                        newData.views.add(view);
×
554
                    }
×
555
                    data = newData;
×
556
                    dataInitialized = true;
×
557
                } catch (Exception ex) {
×
558
                    logger.error("Error while trying to update space data: {}", ex);
×
559
                }
×
560
            }).start();
×
561
            dataNeedsUpdate = false;
×
562
        }
563
    }
×
564

565
    private void setCoreData(SpaceData data) {
566
        for (Statement st : rootNanopub.getAssertion()) {
×
567
            if (st.getSubject().stringValue().equals(getId())) {
×
568
                if (st.getPredicate().equals(OWL.SAMEAS) && st.getObject() instanceof IRI objIri) {
×
569
                    data.altIds.add(objIri.stringValue());
×
570
                } else if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
×
571
                    data.description = st.getObject().stringValue();
×
572
                } else if (st.getPredicate().stringValue().equals("http://schema.org/startDate")) {
×
573
                    try {
574
                        data.startDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
×
575
                    } catch (DateTimeParseException ex) {
×
576
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
577
                    }
×
578
                } else if (st.getPredicate().stringValue().equals("http://schema.org/endDate")) {
×
579
                    try {
580
                        data.endDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
×
581
                    } catch (IllegalArgumentException ex) {
×
582
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
583
                    }
×
584
                } else if (st.getPredicate().equals(HAS_ADMIN) && st.getObject() instanceof IRI obj) {
×
585
                    data.addAdmin(obj);
×
586
                } else if (st.getPredicate().equals(HAS_PINNED_TEMPLATE) && st.getObject() instanceof IRI obj) {
×
587
                    data.pinnedResources.add(TemplateData.get().getTemplate(obj.stringValue()));
×
588
                } else if (st.getPredicate().equals(HAS_PINNED_QUERY) && st.getObject() instanceof IRI obj) {
×
589
                    data.pinnedResources.add(GrlcQuery.get(obj.stringValue()));
×
590
                } else if (st.getPredicate().equals(NTEMPLATE.HAS_DEFAULT_PROVENANCE) && st.getObject() instanceof IRI obj) {
×
591
                    data.defaultProvenance = obj;
×
592
                }
593
            } else if (st.getPredicate().equals(NTEMPLATE.HAS_TAG) && st.getObject() instanceof Literal l) {
×
594
                data.pinGroupTags.add(l.stringValue());
×
595
                Set<Serializable> list = data.pinnedResourceMap.get(l.stringValue());
×
596
                if (list == null) {
×
597
                    list = new HashSet<>();
×
598
                    data.pinnedResourceMap.put(l.stringValue(), list);
×
599
                }
600
                list.add(TemplateData.get().getTemplate(st.getSubject().stringValue()));
×
601
            }
602
        }
×
603
    }
×
604

605
    @Override
606
    public String toString() {
607
        return id;
×
608
    }
609

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