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

knowledgepixels / nanodash / 23057661803

13 Mar 2026 03:21PM UTC coverage: 15.686% (+0.02%) from 15.67%
23057661803

push

github

web-flow
Merge pull request #392 from knowledgepixels/391-fix-space-roles-refresh

Fix: periodic refresh now triggers recalculation of Space roles and members

710 of 5489 branches covered (12.93%)

Branch coverage included in aggregate %.

1754 of 10219 relevant lines covered (17.16%)

2.35 hits per line

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

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

3
import com.google.common.collect.ArrayListMultimap;
4
import com.google.common.collect.Multimap;
5
import com.knowledgepixels.nanodash.*;
6
import com.knowledgepixels.nanodash.template.Template;
7
import com.knowledgepixels.nanodash.template.TemplateData;
8
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
9
import jakarta.xml.bind.DatatypeConverter;
10
import org.eclipse.rdf4j.model.IRI;
11
import org.eclipse.rdf4j.model.Literal;
12
import org.eclipse.rdf4j.model.Statement;
13
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
14
import org.eclipse.rdf4j.model.vocabulary.OWL;
15
import org.nanopub.Nanopub;
16
import org.nanopub.extra.services.ApiResponse;
17
import org.nanopub.extra.services.ApiResponseEntry;
18
import org.nanopub.extra.services.QueryRef;
19
import org.nanopub.vocabulary.NTEMPLATE;
20
import org.slf4j.Logger;
21
import org.slf4j.LoggerFactory;
22

23
import java.io.Serializable;
24
import java.util.*;
25

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

31
    private static final Logger logger = LoggerFactory.getLogger(Space.class);
12✔
32

33
    @Override
34
    public boolean isDataInitialized() {
35
        triggerDataUpdate();
×
36
        return dataInitialized && super.isDataInitialized();
×
37
    }
38

39
    @Override
40
    public void setDataNeedsUpdate() {
41
        super.setDataNeedsUpdate();
6✔
42
        dataNeedsUpdate = true;
9✔
43
    }
3✔
44

45
    @Override
46
    public void forceRefresh(long waitMillis) {
47
        super.forceRefresh(waitMillis);
×
48
        dataNeedsUpdate = true;
×
49
        dataInitialized = false;
×
50
    }
×
51

52
    private String label, rootNanopubId, type;
53
    private Nanopub rootNanopub = null;
9✔
54
    private SpaceData data = new SpaceData();
15✔
55

56
    private static class SpaceData implements Serializable {
6✔
57

58
        List<String> altIds = new ArrayList<>();
15✔
59

60
        String description = null;
9✔
61
        Calendar startDate, endDate;
62
        IRI defaultProvenance = null;
9✔
63

64
        List<IRI> admins = new ArrayList<>();
15✔
65
        Map<IRI, Set<SpaceMemberRoleRef>> users = new HashMap<>();
15✔
66
        List<SpaceMemberRoleRef> roles = new ArrayList<>();
15✔
67
        Map<IRI, SpaceMemberRole> roleMap = new HashMap<>();
15✔
68

69
        Map<String, IRI> adminPubkeyMap = new HashMap<>();
15✔
70
        Set<Serializable> pinnedResources = new HashSet<>();
15✔
71
        Set<String> pinGroupTags = new HashSet<>();
15✔
72
        Map<String, Set<Serializable>> pinnedResourceMap = new HashMap<>();
18✔
73

74
        void addAdmin(IRI admin, String npId) {
75
            // TODO This isn't efficient for long owner lists:
76
            if (admins.contains(admin)) return;
15!
77
            admins.add(admin);
15✔
78
            UserData ud = User.getUserData();
6✔
79
            for (String pubkeyHash : ud.getPubkeyHashes(admin, true)) {
42✔
80
                adminPubkeyMap.put(pubkeyHash, admin);
18✔
81
            }
3✔
82
            users.computeIfAbsent(admin, (k) -> new HashSet<>()).add(new SpaceMemberRoleRef(SpaceMemberRole.ADMIN_ROLE, npId));
51✔
83
        }
3✔
84

85
    }
86

87
    private static String getCoreInfoString(ApiResponseEntry resp) {
88
        String id = resp.get("space");
×
89
        String rootNanopubId = resp.get("np");
×
90
        return id + " " + rootNanopubId;
×
91
    }
92

93
    private volatile boolean dataInitialized = false;
9✔
94
    private volatile boolean dataNeedsUpdate = true;
9✔
95

96
    Space(ApiResponseEntry resp) {
97
        super(resp.get("space"));
15✔
98
        initSpace(this);
9✔
99
        this.label = resp.get("label");
15✔
100
        this.type = resp.get("type");
15✔
101
        this.rootNanopubId = resp.get("np");
15✔
102
        this.rootNanopub = Utils.getAsNanopub(rootNanopubId);
15✔
103
        setCoreData(data);
12✔
104
    }
3✔
105

106
    /**
107
     * Get the root nanopublication ID of the space.
108
     *
109
     * @return The root nanopub ID.
110
     */
111
    @Override
112
    public String getNanopubId() {
113
        return rootNanopubId;
×
114
    }
115

116
    /**
117
     * Get a string combining the space ID and root nanopub ID for core identification.
118
     *
119
     * @return The core info string.
120
     */
121
    public String getCoreInfoString() {
122
        return getId() + " " + rootNanopubId;
×
123
    }
124

125
    /**
126
     * Get the root nanopublication of the space.
127
     *
128
     * @return The root Nanopub object.
129
     */
130
    @Override
131
    public Nanopub getNanopub() {
132
        return rootNanopub;
×
133
    }
134

135
    @Override
136
    public String getNamespace() {
137
        // FIXME this will be removed in the future
138
        return null;
×
139
    }
140

141
    /**
142
     * Get the label of the space.
143
     *
144
     * @return The space label.
145
     */
146
    @Override
147
    public String getLabel() {
148
        return label;
×
149
    }
150

151
    /**
152
     * Get the type of the space.
153
     *
154
     * @return The space type.
155
     */
156
    public String getType() {
157
        return type;
9✔
158
    }
159

160
    /**
161
     * Get the start date of the space.
162
     *
163
     * @return The start date as a Calendar object, or null if not set.
164
     */
165
    public Calendar getStartDate() {
166
        return data.startDate;
×
167
    }
168

169
    /**
170
     * Get the end date of the space.
171
     *
172
     * @return The end date as a Calendar object, or null if not set.
173
     */
174
    public Calendar getEndDate() {
175
        return data.endDate;
×
176
    }
177

178
    /**
179
     * Get a simplified label for the type of space by removing any namespace prefix.
180
     *
181
     * @return The simplified type label.
182
     */
183
    public String getTypeLabel() {
184
        return type.replaceFirst("^.*/", "");
×
185
    }
186

187
    /**
188
     * Get the description of the space.
189
     *
190
     * @return The description string.
191
     */
192
    public String getDescription() {
193
        return data.description;
×
194
    }
195

196

197
    /**
198
     * Get the list of admins in this space.
199
     *
200
     * @return List of admin IRIs.
201
     */
202
    public List<IRI> getAdmins() {
203
        ensureInitialized();
×
204
        return data.admins;
×
205
    }
206

207
    /**
208
     * Get the list of members in this space.
209
     *
210
     * @return List of member IRIs.
211
     */
212
    public List<IRI> getUsers() {
213
        ensureInitialized();
×
214
        List<IRI> users = new ArrayList<IRI>(data.users.keySet());
×
215
        users.sort(User.getUserData().userComparator);
×
216
        return users;
×
217
    }
218

219
    /**
220
     * Get the roles of a specific member in this space.
221
     *
222
     * @param userId The IRI of the member.
223
     * @return Set of roles assigned to the member, or null if the member is not part of this space.
224
     */
225
    public Set<SpaceMemberRoleRef> getMemberRoles(IRI userId) {
226
        ensureInitialized();
×
227
        return data.users.get(userId);
×
228
    }
229

230
    /**
231
     * Check if a user is a member of this space.
232
     *
233
     * @param userId The IRI of the user to check.
234
     * @return true if the user is a member, false otherwise.
235
     */
236
    public boolean isMember(IRI userId) {
237
        ensureInitialized();
×
238
        return data.users.containsKey(userId);
×
239
    }
240

241
    /**
242
     * Check if a public key is associated with an admin of this space.
243
     *
244
     * @param pubkey The public key hash to check.
245
     * @return true if the public key is associated with an admin, false otherwise.
246
     */
247
    public boolean isAdminPubkey(String pubkey) {
248
        ensureInitialized();
×
249
        return data.adminPubkeyMap.containsKey(pubkey);
×
250
    }
251

252
    /**
253
     * Get the list of pinned resources in this space.
254
     *
255
     * @return Set of pinned resources.
256
     */
257
    public Set<Serializable> getPinnedResources() {
258
        ensureInitialized();
×
259
        return data.pinnedResources;
×
260
    }
261

262
    /**
263
     * Get the set of tags used for grouping pinned resources.
264
     *
265
     * @return Set of tags.
266
     */
267
    public Set<String> getPinGroupTags() {
268
        ensureInitialized();
×
269
        return data.pinGroupTags;
×
270
    }
271

272
    /**
273
     * Get a map of pinned resources grouped by their tags.
274
     *
275
     * @return Map where keys are tags and values are lists of pinned resources (Templates or GrlcQueries).
276
     */
277
    public Map<String, Set<Serializable>> getPinnedResourceMap() {
278
        ensureInitialized();
×
279
        return data.pinnedResourceMap;
×
280
    }
281

282
    @Override
283
    public boolean appliesTo(String elementId, Set<IRI> classes) {
284
        triggerSpaceDataUpdate();
×
285
        return super.appliesTo(elementId, classes);
×
286
    }
287

288
    /**
289
     * Get the default provenance IRI for this space.
290
     *
291
     * @return The default provenance IRI, or null if not set.
292
     */
293
    public IRI getDefaultProvenance() {
294
        return data.defaultProvenance;
×
295
    }
296

297
    /**
298
     * Get the roles defined in this space.
299
     *
300
     * @return List of roles.
301
     */
302
    public List<SpaceMemberRoleRef> getRoles() {
303
        return data.roles;
×
304
    }
305

306
    /**
307
     * Get the super ID of the space.
308
     *
309
     * @return Always returns null. Use getIdSuperspace() instead.
310
     */
311
    public String getSuperId() {
312
        return null;
×
313
    }
314

315
    /**
316
     * Get alternative IDs for the space.
317
     *
318
     * @return List of alternative IDs.
319
     */
320
    public List<String> getAltIDs() {
321
        return data.altIds;
×
322
    }
323

324
    private synchronized void ensureInitialized() {
325
        Thread thread = triggerSpaceDataUpdate();
×
326
        if (!dataInitialized && thread != null) {
×
327
            try {
328
                thread.join(30_000);
×
329
            } catch (InterruptedException ex) {
×
330
                logger.error("failed to join thread", ex);
×
331
            }
×
332
        }
333
        thread = super.triggerDataUpdate();
×
334
        if (!dataInitialized && thread != null) {
×
335
            try {
336
                thread.join(30_000);
×
337
            } catch (InterruptedException ex) {
×
338
                logger.error("failed to join thread", ex);
×
339
            }
×
340
        }
341
    }
×
342

343
    @Override
344
    public synchronized Thread triggerDataUpdate() {
345
        logger.info("Triggering data update for space {}", getId());
×
346
        triggerSpaceDataUpdate();
×
347
        return super.triggerDataUpdate();
×
348
    }
349

350
    private synchronized Thread triggerSpaceDataUpdate() {
351
        if (dataNeedsUpdate) {
×
352
            logger.info("Data needs update for space {} core data, starting update thread", getId());
×
353
            dataNeedsUpdate = false;
×
354
            Thread thread = new Thread(() -> {
×
355
                try {
356
                    if (getRunUpdateAfter() != null) {
×
357
                        while (System.currentTimeMillis() < getRunUpdateAfter()) {
×
358
                            Thread.sleep(100);
×
359
                        }
360
                    }
361
                    SpaceData newData = new SpaceData();
×
362
                    setCoreData(newData);
×
363

364
                    newData.roles.add(new SpaceMemberRoleRef(SpaceMemberRole.ADMIN_ROLE, null));
×
365
                    newData.roleMap.put(KPXL_TERMS.HAS_ADMIN_PREDICATE, SpaceMemberRole.ADMIN_ROLE);
×
366

367
                    // TODO Improve this:
368
                    Multimap<String, String> spaceIds = ArrayListMultimap.create();
×
369
                    Multimap<String, String> resourceIds = ArrayListMultimap.create();
×
370
                    spaceIds.put("space", getId());
×
371
                    resourceIds.put("resource", getId());
×
372
                    for (String id : newData.altIds) {
×
373
                        spaceIds.put("space", id);
×
374
                        resourceIds.put("resource", id);
×
375
                    }
×
376

377
                    ApiResponse getAdminsResponse = ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_ADMINS, spaceIds), false);
×
378
                    boolean continueAddingAdmins = true;
×
379
                    while (continueAddingAdmins) {
×
380
                        continueAddingAdmins = false;
×
381
                        for (ApiResponseEntry r : getAdminsResponse.getData()) {
×
382
                            String pubkeyHash = r.get("pubkey");
×
383
                            if (newData.adminPubkeyMap.containsKey(pubkeyHash)) {
×
384
                                IRI adminId = Utils.vf.createIRI(r.get("admin"));
×
385
                                if (!newData.admins.contains(adminId)) {
×
386
                                    continueAddingAdmins = true;
×
387
                                    newData.addAdmin(adminId, r.get("np"));
×
388
                                }
389
                            }
390
                        }
×
391
                    }
392
                    newData.admins.sort(User.getUserData().userComparator);
×
393

394
                    Multimap<String, String> getSpaceMemberParams = ArrayListMultimap.create(spaceIds);
×
395

396
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACE_MEMBER_ROLES, spaceIds), false).getData()) {
×
397
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
398
                        SpaceMemberRole role = new SpaceMemberRole(r);
×
399
                        newData.roles.add(new SpaceMemberRoleRef(role, r.get("np")));
×
400

401
                        // TODO Handle cases of overlapping properties:
402
                        for (IRI p : role.getRegularProperties()) newData.roleMap.put(p, role);
×
403
                        for (IRI p : role.getInverseProperties()) newData.roleMap.put(p, role);
×
404

405
                        role.addRoleParams(getSpaceMemberParams);
×
406
                    }
×
407

408
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACE_MEMBERS, getSpaceMemberParams), false).getData()) {
×
409
                        IRI memberId = Utils.vf.createIRI(r.get("member"));
×
410
                        SpaceMemberRole role = newData.roleMap.get(Utils.vf.createIRI(r.get("role")));
×
411
                        newData.users.computeIfAbsent(memberId, (k) -> new HashSet<>()).add(new SpaceMemberRoleRef(role, r.get("np")));
×
412
                    }
×
413

414
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_PINNED_TEMPLATES, spaceIds), false).getData()) {
×
415
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
416
                        Template t = TemplateData.get().getTemplate(r.get("template"));
×
417
                        if (t == null) continue;
×
418
                        newData.pinnedResources.add(t);
×
419
                        String tag = r.get("tag");
×
420
                        if (tag != null && !tag.isEmpty()) {
×
421
                            newData.pinGroupTags.add(r.get("tag"));
×
422
                            newData.pinnedResourceMap.computeIfAbsent(tag, k -> new HashSet<>()).add(TemplateData.get().getTemplate(r.get("template")));
×
423
                        }
424
                    }
×
425
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_PINNED_QUERIES, spaceIds), false).getData()) {
×
426
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
427
                        GrlcQuery query = GrlcQuery.get(r.get("query"));
×
428
                        if (query == null) continue;
×
429
                        newData.pinnedResources.add(query);
×
430
                        String tag = r.get("tag");
×
431
                        if (tag != null && !tag.isEmpty()) {
×
432
                            newData.pinGroupTags.add(r.get("tag"));
×
433
                            newData.pinnedResourceMap.computeIfAbsent(tag, k -> new HashSet<>()).add(query);
×
434
                        }
435
                    }
×
436
                    data = newData;
×
437
                    dataInitialized = true;
×
438
                } catch (Exception ex) {
×
439
                    logger.error("Error while trying to update space data: {}", ex.getMessage());
×
440
                    dataNeedsUpdate = true;
×
441
                }
×
442
            });
×
443
            thread.start();
×
444
            return thread;
×
445
        }
446
        return null;
×
447
    }
448

449
    private void setCoreData(SpaceData data) {
450
        for (Statement st : rootNanopub.getAssertion()) {
36✔
451
            if (st.getSubject().stringValue().equals(getId())) {
21!
452
                if (st.getPredicate().equals(OWL.SAMEAS) && st.getObject() instanceof IRI objIri) {
42!
453
                    data.altIds.add(objIri.stringValue());
21✔
454
                } else if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
15✔
455
                    data.description = st.getObject().stringValue();
18✔
456
                } else if (st.getPredicate().stringValue().equals("http://schema.org/startDate")) {
18✔
457
                    try {
458
                        data.startDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
18✔
459
                    } catch (IllegalArgumentException ex) {
×
460
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
461
                    }
3✔
462
                } else if (st.getPredicate().stringValue().equals("http://schema.org/endDate")) {
18✔
463
                    try {
464
                        data.endDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
18✔
465
                    } catch (IllegalArgumentException ex) {
×
466
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
467
                    }
3✔
468
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ADMIN) && st.getObject() instanceof IRI obj) {
42!
469
                    data.addAdmin(obj, rootNanopub.getUri().stringValue());
24✔
470
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_PINNED_TEMPLATE) && st.getObject() instanceof IRI obj) {
15!
471
                    data.pinnedResources.add(TemplateData.get().getTemplate(obj.stringValue()));
×
472
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_PINNED_QUERY) && st.getObject() instanceof IRI obj) {
15!
473
                    data.pinnedResources.add(GrlcQuery.get(obj.stringValue()));
×
474
                } else if (st.getPredicate().equals(NTEMPLATE.HAS_DEFAULT_PROVENANCE) && st.getObject() instanceof IRI obj) {
15!
475
                    data.defaultProvenance = obj;
3✔
476
                }
477
            } else if (st.getPredicate().equals(NTEMPLATE.HAS_TAG) && st.getObject() instanceof Literal l) {
×
478
                data.pinGroupTags.add(l.stringValue());
×
479
                Set<Serializable> list = data.pinnedResourceMap.get(l.stringValue());
×
480
                if (list == null) {
×
481
                    list = new HashSet<>();
×
482
                    data.pinnedResourceMap.put(l.stringValue(), list);
×
483
                }
484
                list.add(TemplateData.get().getTemplate(st.getSubject().stringValue()));
×
485
            }
486
        }
3✔
487
    }
3✔
488

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