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

knowledgepixels / nanodash / 26284735769

22 May 2026 11:18AM UTC coverage: 20.838% (+0.09%) from 20.748%
26284735769

Pull #468

github

web-flow
Merge ddda26da3 into 65b0c8452
Pull Request #468: Source space data from nanopub-query spaces repo

1034 of 6284 branches covered (16.45%)

Branch coverage included in aggregate %.

2668 of 11482 relevant lines covered (23.24%)

3.32 hits per line

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

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

3
import com.knowledgepixels.nanodash.*;
4
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
5
import jakarta.xml.bind.DatatypeConverter;
6
import org.eclipse.rdf4j.model.IRI;
7
import org.eclipse.rdf4j.model.Statement;
8
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
9
import org.eclipse.rdf4j.model.vocabulary.OWL;
10
import org.nanopub.Nanopub;
11
import org.nanopub.extra.services.ApiResponseEntry;
12
import org.nanopub.vocabulary.NTEMPLATE;
13
import org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15

16
import java.io.Serializable;
17
import java.util.*;
18
import java.util.concurrent.Future;
19
import java.util.concurrent.TimeUnit;
20

21
/**
22
 * Class representing a "Space", which can be any kind of collaborative unit, like a project, group, or event.
23
 */
24
public class Space extends AbstractResourceWithProfile {
25

26
    private static final Logger logger = LoggerFactory.getLogger(Space.class);
9✔
27

28
    @Override
29
    public boolean isDataInitialized() {
30
        triggerDataUpdate();
×
31
        return dataInitialized && super.isDataInitialized();
×
32
    }
33

34
    @Override
35
    public void setDataNeedsUpdate() {
36
        super.setDataNeedsUpdate();
6✔
37
        dataNeedsUpdate = true;
9✔
38
    }
3✔
39

40
    @Override
41
    public void forceRefresh(long waitMillis) {
42
        super.forceRefresh(waitMillis);
×
43
        dataNeedsUpdate = true;
×
44
        dataInitialized = false;
×
45
    }
×
46

47
    private String label, rootNanopubId, type;
48
    private Nanopub rootNanopub = null;
9✔
49
    private SpaceData data = new SpaceData();
15✔
50

51
    private static class SpaceData implements Serializable {
6✔
52

53
        List<String> altIds = new ArrayList<>();
15✔
54

55
        String description = null;
9✔
56
        Calendar startDate, endDate;
57
        IRI defaultProvenance = null;
9✔
58

59
        List<IRI> admins = new ArrayList<>();
15✔
60
        Map<IRI, Set<SpaceMemberRoleRef>> users = new HashMap<>();
15✔
61
        List<SpaceMemberRoleRef> roles = new ArrayList<>();
15✔
62
        Map<IRI, SpaceMemberRole> roleMap = new HashMap<>();
15✔
63

64
        Map<String, IRI> adminPubkeyMap = new HashMap<>();
15✔
65
        Map<String, IRI> userPubkeyMap = new HashMap<>();
18✔
66

67
        void addAdmin(IRI admin, String npId) {
68
            // TODO This isn't efficient for long owner lists:
69
            if (admins.contains(admin)) return;
15!
70
            admins.add(admin);
15✔
71
            // adminPubkeyMap is populated in bulk by loadAdminPubkeyHashesFromSpacesRepo,
72
            // sourcing trust-state-validated (agent, pubkey-hash) pairs from the
73
            // spaces repo's npa:AccountState rows.
74
            users.computeIfAbsent(admin, (k) -> new HashSet<>()).add(new SpaceMemberRoleRef(SpaceMemberRole.ADMIN_ROLE, npId));
51✔
75
        }
3✔
76

77
    }
78

79
    private static final Map<String, String> TYPE_EMOJIS = Map.ofEntries(
27✔
80
            Map.entry("Alliance", "\uD83E\uDD1D"),
18✔
81
            Map.entry("Consortium", "\u2602\uFE0F"),
18✔
82
            Map.entry("Organization", "\uD83C\uDFE2"),
18✔
83
            Map.entry("Taskforce", "\uD83C\uDFAF"),
18✔
84
            Map.entry("Division", "\uD83C\uDFD7\uFE0F"),
18✔
85
            Map.entry("Taskunit", "\u2699\uFE0F"),
18✔
86
            Map.entry("Group", "\uD83D\uDC65"),
18✔
87
            Map.entry("Project", "\uD83D\uDE80"),
18✔
88
            Map.entry("Program", "\uD83D\uDCCB"),
18✔
89
            Map.entry("Initiative", "\uD83D\uDCA1"),
18✔
90
            Map.entry("Outlet", "\uD83D\uDCF0"),
18✔
91
            Map.entry("Campaign", "\uD83D\uDCE3"),
18✔
92
            Map.entry("Community", "\uD83C\uDF10"),
18✔
93
            Map.entry("Event", "\uD83C\uDFAA")
6✔
94
    );
95

96
    /**
97
     * Get the emoji associated with a space type name.
98
     *
99
     * @param typeName The short type name (e.g., "Alliance").
100
     * @return The emoji string, or an empty string if not found.
101
     */
102
    public static String getTypeEmoji(String typeName) {
103
        return TYPE_EMOJIS.getOrDefault(typeName, "");
×
104
    }
105

106
    private static String getCoreInfoString(ApiResponseEntry resp) {
107
        String id = resp.get("space");
×
108
        String rootNanopubId = resp.get("np");
×
109
        return id + " " + rootNanopubId;
×
110
    }
111

112
    private volatile boolean dataInitialized = false;
9✔
113
    private volatile boolean dataNeedsUpdate = true;
9✔
114
    private transient volatile Future<?> spaceDataFuture = null;
9✔
115

116
    Space(ApiResponseEntry resp) {
117
        super(resp.get("space"));
15✔
118
        initSpace(this);
9✔
119
        this.label = resp.get("label");
15✔
120
        this.type = resp.get("type");
15✔
121
        this.rootNanopubId = resp.get("np");
15✔
122
        this.rootNanopub = Utils.getAsNanopub(rootNanopubId);
15✔
123
        setCoreData(data);
12✔
124
    }
3✔
125

126
    void updateFromApi(ApiResponseEntry resp) {
127
        String newNpId = resp.get("np");
×
128
        if (!newNpId.equals(this.rootNanopubId)) {
×
129
            this.label = resp.get("label");
×
130
            this.type = resp.get("type");
×
131
            this.rootNanopubId = newNpId;
×
132
            this.rootNanopub = Utils.getAsNanopub(newNpId);
×
133
            setDataNeedsUpdate();
×
134
        }
135
    }
×
136

137
    /**
138
     * Get the root nanopublication ID of the space.
139
     *
140
     * @return The root nanopub ID.
141
     */
142
    @Override
143
    public String getNanopubId() {
144
        return rootNanopubId;
×
145
    }
146

147
    /**
148
     * Get a string combining the space ID and root nanopub ID for core identification.
149
     *
150
     * @return The core info string.
151
     */
152
    public String getCoreInfoString() {
153
        return getId() + " " + rootNanopubId;
×
154
    }
155

156
    /**
157
     * Get the root nanopublication of the space.
158
     *
159
     * @return The root Nanopub object.
160
     */
161
    @Override
162
    public Nanopub getNanopub() {
163
        return rootNanopub;
×
164
    }
165

166
    @Override
167
    public String getNamespace() {
168
        // FIXME this will be removed in the future
169
        return null;
×
170
    }
171

172
    /**
173
     * Get the label of the space.
174
     *
175
     * @return The space label.
176
     */
177
    @Override
178
    public String getLabel() {
179
        return label;
×
180
    }
181

182
    /**
183
     * Get the type of the space.
184
     *
185
     * @return The space type.
186
     */
187
    public String getType() {
188
        return type;
9✔
189
    }
190

191
    /**
192
     * Get the start date of the space.
193
     *
194
     * @return The start date as a Calendar object, or null if not set.
195
     */
196
    public Calendar getStartDate() {
197
        return data.startDate;
×
198
    }
199

200
    /**
201
     * Get the end date of the space.
202
     *
203
     * @return The end date as a Calendar object, or null if not set.
204
     */
205
    public Calendar getEndDate() {
206
        return data.endDate;
×
207
    }
208

209
    /**
210
     * Get a simplified label for the type of space by removing any namespace prefix.
211
     *
212
     * @return The simplified type label.
213
     */
214
    public String getTypeLabel() {
215
        return type.replaceFirst("^.*/", "");
×
216
    }
217

218
    /**
219
     * Get the description of the space.
220
     *
221
     * @return The description string.
222
     */
223
    public String getDescription() {
224
        return data.description;
×
225
    }
226

227

228
    /**
229
     * Get the list of admins in this space.
230
     *
231
     * @return List of admin IRIs.
232
     */
233
    public List<IRI> getAdmins() {
234
        ensureInitialized();
×
235
        return data.admins;
×
236
    }
237

238
    /**
239
     * Get the list of members in this space.
240
     *
241
     * @return List of member IRIs.
242
     */
243
    public List<IRI> getUsers() {
244
        ensureInitialized();
×
245
        List<IRI> users = new ArrayList<IRI>(data.users.keySet());
×
246
        users.sort(User.getUserData().userComparator);
×
247
        return users;
×
248
    }
249

250
    /**
251
     * Get the roles of a specific member in this space.
252
     *
253
     * @param userId The IRI of the member.
254
     * @return Set of roles assigned to the member, or null if the member is not part of this space.
255
     */
256
    public Set<SpaceMemberRoleRef> getMemberRoles(IRI userId) {
257
        ensureInitialized();
×
258
        return data.users.get(userId);
×
259
    }
260

261
    /**
262
     * Check if a user is a member of this space.
263
     *
264
     * @param userId The IRI of the user to check.
265
     * @return true if the user is a member, false otherwise.
266
     */
267
    public boolean isMember(IRI userId) {
268
        ensureInitialized();
×
269
        return data.users.containsKey(userId);
×
270
    }
271

272
    /**
273
     * Check if a public key is associated with an admin of this space.
274
     *
275
     * @param pubkey The public key hash to check.
276
     * @return true if the public key is associated with an admin, false otherwise.
277
     */
278
    public boolean isAdminPubkey(String pubkey) {
279
        ensureInitialized();
×
280
        return data.adminPubkeyMap.containsKey(pubkey);
×
281
    }
282

283
    /**
284
     * Get the trust-state-validated pubkey hashes of this space's admins.
285
     *
286
     * @return Set of admin pubkey hashes, sourced from the spaces repo.
287
     */
288
    public Set<String> getAdminPubkeyHashes() {
289
        ensureInitialized();
×
290
        return data.adminPubkeyMap.keySet();
×
291
    }
292

293
    /**
294
     * Get the trust-state-validated pubkey hashes of this space's users
295
     * (admins + members).
296
     *
297
     * @return Set of user pubkey hashes, sourced from the spaces repo.
298
     */
299
    public Set<String> getUserPubkeyHashes() {
300
        ensureInitialized();
×
301
        return data.userPubkeyMap.keySet();
×
302
    }
303

304
    @Override
305
    public boolean appliesTo(String elementId, Set<IRI> classes) {
306
        triggerSpaceDataUpdate();
×
307
        return super.appliesTo(elementId, classes);
×
308
    }
309

310
    /**
311
     * Get the default provenance IRI for this space.
312
     *
313
     * @return The default provenance IRI, or null if not set.
314
     */
315
    public IRI getDefaultProvenance() {
316
        return data.defaultProvenance;
×
317
    }
318

319
    /**
320
     * Get the roles defined in this space.
321
     *
322
     * @return List of roles.
323
     */
324
    public List<SpaceMemberRoleRef> getRoles() {
325
        return data.roles;
×
326
    }
327

328
    /**
329
     * Get the super ID of the space.
330
     *
331
     * @return Always returns null. Use getIdSuperspace() instead.
332
     */
333
    public String getSuperId() {
334
        return null;
×
335
    }
336

337
    /**
338
     * Get alternative IDs for the space.
339
     *
340
     * @return List of alternative IDs.
341
     */
342
    public List<String> getAltIDs() {
343
        return data.altIds;
12✔
344
    }
345

346
    private synchronized void ensureInitialized() {
347
        triggerSpaceDataUpdate();
×
348
        if (!dataInitialized && spaceDataFuture != null) {
×
349
            try {
350
                spaceDataFuture.get(30, TimeUnit.SECONDS);
×
351
            } catch (Exception ex) {
×
352
                logger.error("failed to await space data update", ex);
×
353
            }
×
354
        }
355
        Future<?> future = super.triggerDataUpdate();
×
356
        if (!dataInitialized && future != null) {
×
357
            try {
358
                future.get(30, TimeUnit.SECONDS);
×
359
            } catch (Exception ex) {
×
360
                logger.error("failed to await data update", ex);
×
361
            }
×
362
        }
363
    }
×
364

365
    @Override
366
    public synchronized Future<?> triggerDataUpdate() {
367
        triggerSpaceDataUpdate();
×
368
        return super.triggerDataUpdate();
×
369
    }
370

371
    private synchronized Future<?> triggerSpaceDataUpdate() {
372
        if (dataNeedsUpdate) {
×
373
            logger.info("Data needs update for space {} core data, starting update thread", getId());
×
374
            dataNeedsUpdate = false;
×
375
            spaceDataFuture = NanodashThreadPool.submit(() -> {
×
376
                try {
377
                    if (getRunUpdateAfter() != null) {
×
378
                        while (System.currentTimeMillis() < getRunUpdateAfter()) {
×
379
                            Thread.sleep(100);
×
380
                        }
381
                    }
382
                    SpaceData newData = new SpaceData();
×
383
                    setCoreData(newData);
×
384

385
                    newData.roles.add(new SpaceMemberRoleRef(SpaceMemberRole.ADMIN_ROLE, null));
×
386
                    newData.roleMap.put(KPXL_TERMS.HAS_ADMIN_PREDICATE, SpaceMemberRole.ADMIN_ROLE);
×
387

388
                    List<String> spaceIris = new ArrayList<>();
×
389
                    spaceIris.add(getId());
×
390
                    spaceIris.addAll(newData.altIds);
×
391

392
                    loadAdminsFromSpacesRepo(newData, spaceIris);
×
393
                    newData.admins.sort(User.getUserData().userComparator);
×
394
                    loadAdminPubkeyHashesFromSpacesRepo(newData, spaceIris);
×
395

396
                    loadRolesFromSpacesRepo(newData, spaceIris);
×
397
                    loadMembersFromSpacesRepo(newData, spaceIris);
×
398
                    loadUserPubkeyHashesFromSpacesRepo(newData);
×
399

400
                    data = newData;
×
401
                    dataInitialized = true;
×
402
                } catch (Exception ex) {
×
403
                    logger.error("Error while trying to update space data: {}", ex.getMessage());
×
404
                    dataNeedsUpdate = true;
×
405
                }
×
406
            });
×
407
            return spaceDataFuture;
×
408
        }
409
        return spaceDataFuture;
×
410
    }
411

412
    private static String spaceValuesClause(List<String> spaceIris) {
413
        StringBuilder sb = new StringBuilder("  VALUES ?space { ");
×
414
        for (String iri : spaceIris) sb.append('<').append(iri).append("> ");
×
415
        sb.append("}\n");
×
416
        return sb.toString();
×
417
    }
418

419
    private static void loadAdminsFromSpacesRepo(SpaceData data, List<String> spaceIris) {
420
        // TODO Replace this programmatically-built SPARQL with a published grlc
421
        // query template (like the constants in QueryApiAccess), so all Nanopub
422
        // Query access goes through the same query-template pipeline.
423
        String sparql = SpacesRepoAccess.PREFIXES
×
424
                + "SELECT DISTINCT ?agent ?np WHERE {\n"
425
                + SpacesRepoAccess.CURRENT_STATE_POINTER
426
                + spaceValuesClause(spaceIris)
×
427
                + "  GRAPH ?g {\n"
428
                + "    ?ri a gen:RoleInstantiation ;\n"
429
                + "        npa:inverseProperty gen:hasAdmin ;\n"
430
                + "        npa:forSpace ?space ;\n"
431
                + "        npa:forAgent ?agent ;\n"
432
                + "        npa:viaNanopub ?np .\n"
433
                + "  }\n"
434
                + "}";
435
        SpacesRepoAccess.get().select(sparql, null, b -> {
×
436
            IRI adminId = Utils.vf.createIRI(b.getValue("agent").stringValue());
×
437
            String np = b.getValue("np").stringValue();
×
438
            if (!data.admins.contains(adminId)) data.addAdmin(adminId, np);
×
439
            return null;
×
440
        });
441
    }
×
442

443
    private static void loadAdminPubkeyHashesFromSpacesRepo(SpaceData data, List<String> spaceIris) {
444
        // TODO Replace this programmatically-built SPARQL with a published grlc
445
        // query template (like the constants in QueryApiAccess), so all Nanopub
446
        // Query access goes through the same query-template pipeline. Better
447
        // still: push the view-display admin gate server-side (a published
448
        // get-view-displays-v2 that joins through the spaces-repo admins) so
449
        // the adminPubkeyMap dependency disappears entirely — see callers of
450
        // Space.isAdminPubkey in AbstractResourceWithProfile and DownloadRdfPage.
451
        //
452
        // Source: trust-state-validated (agent, pubkey-hash) pairs for the
453
        // space's admin RIs. Stricter than the prior UserData.getPubkeyHashes
454
        // source (only pubkeys that are in the current trust state count),
455
        // matching the trust model the rest of this PR moves toward.
456
        String sparql = SpacesRepoAccess.PREFIXES
×
457
                + "SELECT DISTINCT ?agent ?pkh WHERE {\n"
458
                + SpacesRepoAccess.CURRENT_STATE_POINTER
459
                + spaceValuesClause(spaceIris)
×
460
                + "  GRAPH ?g {\n"
461
                + "    ?ri a gen:RoleInstantiation ;\n"
462
                + "        npa:inverseProperty gen:hasAdmin ;\n"
463
                + "        npa:forSpace ?space ;\n"
464
                + "        npa:forAgent ?agent .\n"
465
                + "    ?acct a npa:AccountState ;\n"
466
                + "          npa:agent ?agent ;\n"
467
                + "          npa:pubkey ?pkh .\n"
468
                + "  }\n"
469
                + "}";
470
        SpacesRepoAccess.get().select(sparql, null, b -> {
×
471
            IRI adminId = Utils.vf.createIRI(b.getValue("agent").stringValue());
×
472
            String pkh = b.getValue("pkh").stringValue();
×
473
            data.adminPubkeyMap.put(pkh, adminId);
×
474
            return null;
×
475
        });
476
    }
×
477

478
    private static void loadRolesFromSpacesRepo(SpaceData data, List<String> spaceIris) {
479
        // TODO Replace this programmatically-built SPARQL with a published grlc
480
        // query template (like the constants in QueryApiAccess), so all Nanopub
481
        // Query access goes through the same query-template pipeline.
482
        String sparql = SpacesRepoAccess.PREFIXES
×
483
                + "SELECT ?role ?roleLabel ?roleName ?roleTitle ?roleAssignmentTemplate\n"
484
                + "       (GROUP_CONCAT(DISTINCT ?reg; separator=\" \") AS ?regularProperties)\n"
485
                + "       (GROUP_CONCAT(DISTINCT ?inv; separator=\" \") AS ?inverseProperties)\n"
486
                + "       ?ra_np WHERE {\n"
487
                + SpacesRepoAccess.CURRENT_STATE_POINTER
488
                + spaceValuesClause(spaceIris)
×
489
                + "  GRAPH ?g {\n"
490
                + "    ?ra a gen:RoleAssignment ;\n"
491
                + "        npa:forSpace ?space ;\n"
492
                + "        gen:hasRole ?role ;\n"
493
                + "        npa:viaNanopub ?ra_np .\n"
494
                + "  }\n"
495
                + "  GRAPH npa:spacesGraph {\n"
496
                + "    ?roleDecl a npa:RoleDeclaration ;\n"
497
                + "              npa:role ?role ;\n"
498
                + "              npa:viaNanopub ?role_np .\n"
499
                + "    OPTIONAL { ?roleDecl gen:hasRegularProperty ?reg }\n"
500
                + "    OPTIONAL { ?roleDecl gen:hasInverseProperty ?inv }\n"
501
                + "  }\n"
502
                + "  GRAPH npa:graph { ?role_np np:hasAssertion ?role_a . }\n"
503
                // Each OPTIONAL wraps its own GRAPH so a role with none of these
504
                // properties still produces a row (GRAPH ?x { OPTIONAL { ... } }
505
                // returns no solutions in RDF4J when the inner pattern is unmatched).
506
                + "  OPTIONAL { GRAPH ?role_a { ?role rdfs:label ?roleLabel } }\n"
507
                + "  OPTIONAL { GRAPH ?role_a { ?role dct:title ?roleTitle } }\n"
508
                + "  OPTIONAL { GRAPH ?role_a { ?role schema:name ?roleName } }\n"
509
                + "  OPTIONAL { GRAPH ?role_a { ?role gen:hasRoleAssignmentTemplate ?roleAssignmentTemplate } }\n"
510
                + "}\n"
511
                + "GROUP BY ?role ?roleLabel ?roleName ?roleTitle ?roleAssignmentTemplate ?ra_np";
512
        SpacesRepoAccess.get().select(sparql, null, b -> {
×
513
            ApiResponseEntry entry = new ApiResponseEntry();
×
514
            for (String k : List.of("role", "roleLabel", "roleName", "roleTitle",
×
515
                    "roleAssignmentTemplate", "regularProperties", "inverseProperties")) {
516
                if (b.getValue(k) != null) entry.add(k, b.getValue(k).stringValue());
×
517
            }
×
518
            SpaceMemberRole role = new SpaceMemberRole(entry);
×
519
            String raNp = b.getValue("ra_np") == null ? null : b.getValue("ra_np").stringValue();
×
520
            data.roles.add(new SpaceMemberRoleRef(role, raNp));
×
521
            for (IRI p : role.getRegularProperties()) data.roleMap.put(p, role);
×
522
            for (IRI p : role.getInverseProperties()) data.roleMap.put(p, role);
×
523
            return null;
×
524
        });
525
    }
×
526

527
    private static void loadMembersFromSpacesRepo(SpaceData data, List<String> spaceIris) {
528
        // TODO Replace this programmatically-built SPARQL with a published grlc
529
        // query template (like the constants in QueryApiAccess), so all Nanopub
530
        // Query access goes through the same query-template pipeline.
531
        // Pulls RI candidates from the extraction graph (npa:spacesGraph) rather
532
        // than the validated state graph. The spaces-repo materialiser's
533
        // per-tier admit query can time out on some spaces (a planner blow-up
534
        // in RDF4J, observed on fdo-connect), leaving member RIs extracted but
535
        // never validated. To keep parity with the pre-migration behaviour
536
        // (legacy GET_SPACE_MEMBERS gated rows client-side by admin pubkey),
537
        // we read the universe of candidates here and apply the same client-side
538
        // admin-pubkey gate via data.adminPubkeyMap. Invalidated rows are
539
        // filtered out via the npx:invalidates triple in npa:graph.
540
        String sparql = SpacesRepoAccess.PREFIXES
×
541
                + "SELECT ?member ?np ?pkh ?regProp ?invProp WHERE {\n"
542
                + spaceValuesClause(spaceIris)
×
543
                + "  GRAPH npa:spacesGraph {\n"
544
                + "    ?ri a gen:RoleInstantiation ;\n"
545
                + "        npa:forSpace ?space ;\n"
546
                + "        npa:forAgent ?member ;\n"
547
                + "        npa:viaNanopub ?np ;\n"
548
                + "        npa:pubkeyHash ?pkh .\n"
549
                + "    OPTIONAL { ?ri npa:regularProperty ?regProp }\n"
550
                + "    OPTIONAL { ?ri npa:inverseProperty ?invProp }\n"
551
                + "    FILTER NOT EXISTS { ?ri npa:inverseProperty gen:hasAdmin }\n"
552
                + "  }\n"
553
                + "  FILTER NOT EXISTS { GRAPH npa:graph { ?invNp npx:invalidates ?np . } }\n"
554
                + "}";
555
        SpacesRepoAccess.get().select(sparql, null, b -> {
×
556
            String pkh = b.getValue("pkh") == null ? null : b.getValue("pkh").stringValue();
×
557
            if (pkh == null || !data.adminPubkeyMap.containsKey(pkh)) return null;
×
558
            IRI memberId = Utils.vf.createIRI(b.getValue("member").stringValue());
×
559
            String np = b.getValue("np").stringValue();
×
560
            SpaceMemberRole role = null;
×
561
            for (String key : new String[] {"regProp", "invProp"}) {
×
562
                if (b.getValue(key) != null) {
×
563
                    IRI pred = Utils.vf.createIRI(b.getValue(key).stringValue());
×
564
                    SpaceMemberRole candidate = data.roleMap.get(pred);
×
565
                    if (candidate != null) { role = candidate; break; }
×
566
                }
567
            }
568
            data.users.computeIfAbsent(memberId, k -> new HashSet<>())
×
569
                    .add(new SpaceMemberRoleRef(role, np));
×
570
            return null;
×
571
        });
572
    }
×
573

574
    private static void loadUserPubkeyHashesFromSpacesRepo(SpaceData data) {
575
        // TODO Replace this programmatically-built SPARQL with a published grlc
576
        // query template (like the constants in QueryApiAccess), so all Nanopub
577
        // Query access goes through the same query-template pipeline. The
578
        // long-term direction is to push the per-view pubkey filtering server
579
        // side too, so view queries gate by user-of-space without nanodash
580
        // having to expand the placeholder client-side at all — see callers
581
        // of Space.getUserPubkeyHashes / getAdminPubkeyHashes in ViewList and
582
        // DownloadRdfPage.
583
        //
584
        // Source: trust-state-validated (agent, pubkey-hash) pairs from
585
        // npa:AccountState for every user agent (admin or admin-gated member)
586
        // already loaded into data.users. Stricter than the prior
587
        // UserData.getPubkeyHashes source — pubkey hashes of users not in
588
        // the current trust state are not returned.
589
        if (data.users.isEmpty()) return;
×
590
        StringBuilder values = new StringBuilder("  VALUES ?agent { ");
×
591
        for (IRI agent : data.users.keySet()) {
×
592
            values.append('<').append(agent.stringValue()).append("> ");
×
593
        }
×
594
        values.append("}\n");
×
595
        String sparql = SpacesRepoAccess.PREFIXES
×
596
                + "SELECT DISTINCT ?agent ?pkh WHERE {\n"
597
                + SpacesRepoAccess.CURRENT_STATE_POINTER
598
                + values
599
                + "  GRAPH ?g {\n"
600
                + "    ?acct a npa:AccountState ;\n"
601
                + "          npa:agent ?agent ;\n"
602
                + "          npa:pubkey ?pkh .\n"
603
                + "  }\n"
604
                + "}";
605
        SpacesRepoAccess.get().select(sparql, null, b -> {
×
606
            IRI agentId = Utils.vf.createIRI(b.getValue("agent").stringValue());
×
607
            String pkh = b.getValue("pkh").stringValue();
×
608
            data.userPubkeyMap.put(pkh, agentId);
×
609
            return null;
×
610
        });
611
    }
×
612

613
    private void setCoreData(SpaceData data) {
614
        for (Statement st : rootNanopub.getAssertion()) {
36✔
615
            if (st.getSubject().stringValue().equals(getId())) {
21!
616
                if (st.getPredicate().equals(OWL.SAMEAS) && st.getObject() instanceof IRI objIri) {
42!
617
                    data.altIds.add(objIri.stringValue());
21✔
618
                } else if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
15✔
619
                    data.description = st.getObject().stringValue();
18✔
620
                } else if (st.getPredicate().stringValue().equals("http://schema.org/startDate")) {
18✔
621
                    try {
622
                        data.startDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
18✔
623
                    } catch (IllegalArgumentException ex) {
×
624
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
625
                    }
3✔
626
                } else if (st.getPredicate().stringValue().equals("http://schema.org/endDate")) {
18✔
627
                    try {
628
                        data.endDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
18✔
629
                    } catch (IllegalArgumentException ex) {
×
630
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
631
                    }
3✔
632
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ADMIN) && st.getObject() instanceof IRI obj) {
42!
633
                    data.addAdmin(obj, rootNanopub.getUri().stringValue());
24✔
634
                } else if (st.getPredicate().equals(NTEMPLATE.HAS_DEFAULT_PROVENANCE) && st.getObject() instanceof IRI obj) {
15!
635
                    data.defaultProvenance = obj;
×
636
                }
637
            }
638
        }
3✔
639
    }
3✔
640

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