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

knowledgepixels / nanodash / 23302971328

19 Mar 2026 03:35PM UTC coverage: 16.327% (+0.08%) from 16.251%
23302971328

push

github

tkuhn
feat: add emojis to space type section titles

Add emoji icons for all 14 space types (Alliance, Consortium, etc.)
shown in section headings on the spaces list page and subspace sections
on individual space pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

728 of 5521 branches covered (13.19%)

Branch coverage included in aggregate %.

1865 of 10361 relevant lines covered (18.0%)

2.47 hits per line

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

29.84
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
import java.util.concurrent.Future;
26
import java.util.concurrent.TimeUnit;
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 extends AbstractResourceWithProfile {
32

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

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

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

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

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

58
    private static class SpaceData implements Serializable {
6✔
59

60
        List<String> altIds = new ArrayList<>();
15✔
61

62
        String description = null;
9✔
63
        Calendar startDate, endDate;
64
        IRI defaultProvenance = null;
9✔
65

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

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

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

87
    }
88

89
    private static final Map<String, String> TYPE_EMOJIS = Map.ofEntries(
27✔
90
            Map.entry("Alliance", "\uD83E\uDD1D"),
18✔
91
            Map.entry("Consortium", "\uD83C\uDFDB\uFE0F"),
18✔
92
            Map.entry("Organization", "\uD83C\uDFE2"),
18✔
93
            Map.entry("Taskforce", "\uD83C\uDFAF"),
18✔
94
            Map.entry("Division", "\uD83C\uDFD7\uFE0F"),
18✔
95
            Map.entry("Taskunit", "\u2699\uFE0F"),
18✔
96
            Map.entry("Group", "\uD83D\uDC65"),
18✔
97
            Map.entry("Project", "\uD83D\uDD28"),
18✔
98
            Map.entry("Program", "\uD83D\uDCCB"),
18✔
99
            Map.entry("Initiative", "\uD83D\uDE80"),
18✔
100
            Map.entry("Outlet", "\uD83D\uDCF0"),
18✔
101
            Map.entry("Campaign", "\uD83D\uDCE3"),
18✔
102
            Map.entry("Community", "\uD83C\uDF10"),
18✔
103
            Map.entry("Event", "\uD83D\uDCC5")
6✔
104
    );
105

106
    /**
107
     * Get the emoji associated with a space type name.
108
     *
109
     * @param typeName The short type name (e.g., "Alliance").
110
     * @return The emoji string, or an empty string if not found.
111
     */
112
    public static String getTypeEmoji(String typeName) {
113
        return TYPE_EMOJIS.getOrDefault(typeName, "");
×
114
    }
115

116
    private static String getCoreInfoString(ApiResponseEntry resp) {
117
        String id = resp.get("space");
×
118
        String rootNanopubId = resp.get("np");
×
119
        return id + " " + rootNanopubId;
×
120
    }
121

122
    private volatile boolean dataInitialized = false;
9✔
123
    private volatile boolean dataNeedsUpdate = true;
9✔
124

125
    Space(ApiResponseEntry resp) {
126
        super(resp.get("space"));
15✔
127
        initSpace(this);
9✔
128
        this.label = resp.get("label");
15✔
129
        this.type = resp.get("type");
15✔
130
        this.rootNanopubId = resp.get("np");
15✔
131
        this.rootNanopub = Utils.getAsNanopub(rootNanopubId);
15✔
132
        setCoreData(data);
12✔
133
    }
3✔
134

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

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

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

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

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

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

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

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

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

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

225

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

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

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

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

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

281
    /**
282
     * Get the list of pinned resources in this space.
283
     *
284
     * @return Set of pinned resources.
285
     */
286
    public Set<Serializable> getPinnedResources() {
287
        ensureInitialized();
×
288
        return data.pinnedResources;
×
289
    }
290

291
    /**
292
     * Get the set of tags used for grouping pinned resources.
293
     *
294
     * @return Set of tags.
295
     */
296
    public Set<String> getPinGroupTags() {
297
        ensureInitialized();
×
298
        return data.pinGroupTags;
×
299
    }
300

301
    /**
302
     * Get a map of pinned resources grouped by their tags.
303
     *
304
     * @return Map where keys are tags and values are lists of pinned resources (Templates or GrlcQueries).
305
     */
306
    public Map<String, Set<Serializable>> getPinnedResourceMap() {
307
        ensureInitialized();
×
308
        return data.pinnedResourceMap;
×
309
    }
310

311
    @Override
312
    public boolean appliesTo(String elementId, Set<IRI> classes) {
313
        triggerSpaceDataUpdate();
×
314
        return super.appliesTo(elementId, classes);
×
315
    }
316

317
    /**
318
     * Get the default provenance IRI for this space.
319
     *
320
     * @return The default provenance IRI, or null if not set.
321
     */
322
    public IRI getDefaultProvenance() {
323
        return data.defaultProvenance;
×
324
    }
325

326
    /**
327
     * Get the roles defined in this space.
328
     *
329
     * @return List of roles.
330
     */
331
    public List<SpaceMemberRoleRef> getRoles() {
332
        return data.roles;
×
333
    }
334

335
    /**
336
     * Get the super ID of the space.
337
     *
338
     * @return Always returns null. Use getIdSuperspace() instead.
339
     */
340
    public String getSuperId() {
341
        return null;
×
342
    }
343

344
    /**
345
     * Get alternative IDs for the space.
346
     *
347
     * @return List of alternative IDs.
348
     */
349
    public List<String> getAltIDs() {
350
        return data.altIds;
12✔
351
    }
352

353
    private synchronized void ensureInitialized() {
354
        Future<?> future = triggerSpaceDataUpdate();
×
355
        if (!dataInitialized && future != null) {
×
356
            try {
357
                future.get(30, TimeUnit.SECONDS);
×
358
            } catch (Exception ex) {
×
359
                logger.error("failed to await space data update", ex);
×
360
            }
×
361
        }
362
        future = super.triggerDataUpdate();
×
363
        if (!dataInitialized && future != null) {
×
364
            try {
365
                future.get(30, TimeUnit.SECONDS);
×
366
            } catch (Exception ex) {
×
367
                logger.error("failed to await data update", ex);
×
368
            }
×
369
        }
370
    }
×
371

372
    @Override
373
    public synchronized Future<?> triggerDataUpdate() {
374
        triggerSpaceDataUpdate();
×
375
        return super.triggerDataUpdate();
×
376
    }
377

378
    private synchronized Future<?> triggerSpaceDataUpdate() {
379
        if (dataNeedsUpdate) {
×
380
            logger.info("Data needs update for space {} core data, starting update thread", getId());
×
381
            dataNeedsUpdate = false;
×
382
            return NanodashThreadPool.submit(() -> {
×
383
                try {
384
                    if (getRunUpdateAfter() != null) {
×
385
                        while (System.currentTimeMillis() < getRunUpdateAfter()) {
×
386
                            Thread.sleep(100);
×
387
                        }
388
                    }
389
                    SpaceData newData = new SpaceData();
×
390
                    setCoreData(newData);
×
391

392
                    newData.roles.add(new SpaceMemberRoleRef(SpaceMemberRole.ADMIN_ROLE, null));
×
393
                    newData.roleMap.put(KPXL_TERMS.HAS_ADMIN_PREDICATE, SpaceMemberRole.ADMIN_ROLE);
×
394

395
                    // TODO Improve this:
396
                    Multimap<String, String> spaceIds = ArrayListMultimap.create();
×
397
                    Multimap<String, String> resourceIds = ArrayListMultimap.create();
×
398
                    spaceIds.put("space", getId());
×
399
                    resourceIds.put("resource", getId());
×
400
                    for (String id : newData.altIds) {
×
401
                        spaceIds.put("space", id);
×
402
                        resourceIds.put("resource", id);
×
403
                    }
×
404

405
                    ApiResponse getAdminsResponse = ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_ADMINS, spaceIds), true);
×
406
                    boolean continueAddingAdmins = true;
×
407
                    while (continueAddingAdmins) {
×
408
                        continueAddingAdmins = false;
×
409
                        for (ApiResponseEntry r : getAdminsResponse.getData()) {
×
410
                            String pubkeyHash = r.get("pubkey");
×
411
                            if (newData.adminPubkeyMap.containsKey(pubkeyHash)) {
×
412
                                IRI adminId = Utils.vf.createIRI(r.get("admin"));
×
413
                                if (!newData.admins.contains(adminId)) {
×
414
                                    continueAddingAdmins = true;
×
415
                                    newData.addAdmin(adminId, r.get("np"));
×
416
                                }
417
                            }
418
                        }
×
419
                    }
420
                    newData.admins.sort(User.getUserData().userComparator);
×
421

422
                    Multimap<String, String> getSpaceMemberParams = ArrayListMultimap.create(spaceIds);
×
423

424
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACE_MEMBER_ROLES, spaceIds), true).getData()) {
×
425
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
426
                        SpaceMemberRole role = new SpaceMemberRole(r);
×
427
                        newData.roles.add(new SpaceMemberRoleRef(role, r.get("np")));
×
428

429
                        // TODO Handle cases of overlapping properties:
430
                        for (IRI p : role.getRegularProperties()) newData.roleMap.put(p, role);
×
431
                        for (IRI p : role.getInverseProperties()) newData.roleMap.put(p, role);
×
432

433
                        role.addRoleParams(getSpaceMemberParams);
×
434
                    }
×
435

436
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_SPACE_MEMBERS, getSpaceMemberParams), true).getData()) {
×
437
                        IRI memberId = Utils.vf.createIRI(r.get("member"));
×
438
                        SpaceMemberRole role = newData.roleMap.get(Utils.vf.createIRI(r.get("role")));
×
439
                        newData.users.computeIfAbsent(memberId, (k) -> new HashSet<>()).add(new SpaceMemberRoleRef(role, r.get("np")));
×
440
                    }
×
441

442
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_PINNED_TEMPLATES, spaceIds), true).getData()) {
×
443
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
444
                        Template t = TemplateData.get().getTemplate(r.get("template"));
×
445
                        if (t == null) continue;
×
446
                        newData.pinnedResources.add(t);
×
447
                        String tag = r.get("tag");
×
448
                        if (tag != null && !tag.isEmpty()) {
×
449
                            newData.pinGroupTags.add(r.get("tag"));
×
450
                            newData.pinnedResourceMap.computeIfAbsent(tag, k -> new HashSet<>()).add(TemplateData.get().getTemplate(r.get("template")));
×
451
                        }
452
                    }
×
453
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_PINNED_QUERIES, spaceIds), true).getData()) {
×
454
                        if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
455
                        GrlcQuery query = GrlcQuery.get(r.get("query"));
×
456
                        if (query == null) continue;
×
457
                        newData.pinnedResources.add(query);
×
458
                        String tag = r.get("tag");
×
459
                        if (tag != null && !tag.isEmpty()) {
×
460
                            newData.pinGroupTags.add(r.get("tag"));
×
461
                            newData.pinnedResourceMap.computeIfAbsent(tag, k -> new HashSet<>()).add(query);
×
462
                        }
463
                    }
×
464
                    data = newData;
×
465
                    dataInitialized = true;
×
466
                } catch (Exception ex) {
×
467
                    logger.error("Error while trying to update space data: {}", ex.getMessage());
×
468
                    dataNeedsUpdate = true;
×
469
                }
×
470
            });
×
471
        }
472
        return null;
×
473
    }
474

475
    private void setCoreData(SpaceData data) {
476
        for (Statement st : rootNanopub.getAssertion()) {
36✔
477
            if (st.getSubject().stringValue().equals(getId())) {
21!
478
                if (st.getPredicate().equals(OWL.SAMEAS) && st.getObject() instanceof IRI objIri) {
42!
479
                    data.altIds.add(objIri.stringValue());
21✔
480
                } else if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
15✔
481
                    data.description = st.getObject().stringValue();
18✔
482
                } else if (st.getPredicate().stringValue().equals("http://schema.org/startDate")) {
18✔
483
                    try {
484
                        data.startDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
18✔
485
                    } catch (IllegalArgumentException ex) {
×
486
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
487
                    }
3✔
488
                } else if (st.getPredicate().stringValue().equals("http://schema.org/endDate")) {
18✔
489
                    try {
490
                        data.endDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
18✔
491
                    } catch (IllegalArgumentException ex) {
×
492
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
493
                    }
3✔
494
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ADMIN) && st.getObject() instanceof IRI obj) {
42!
495
                    data.addAdmin(obj, rootNanopub.getUri().stringValue());
24✔
496
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_PINNED_TEMPLATE) && st.getObject() instanceof IRI obj) {
15!
497
                    data.pinnedResources.add(TemplateData.get().getTemplate(obj.stringValue()));
×
498
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_PINNED_QUERY) && st.getObject() instanceof IRI obj) {
15!
499
                    data.pinnedResources.add(GrlcQuery.get(obj.stringValue()));
×
500
                } else if (st.getPredicate().equals(NTEMPLATE.HAS_DEFAULT_PROVENANCE) && st.getObject() instanceof IRI obj) {
15!
501
                    data.defaultProvenance = obj;
3✔
502
                }
503
            } else if (st.getPredicate().equals(NTEMPLATE.HAS_TAG) && st.getObject() instanceof Literal l) {
×
504
                data.pinGroupTags.add(l.stringValue());
×
505
                Set<Serializable> list = data.pinnedResourceMap.get(l.stringValue());
×
506
                if (list == null) {
×
507
                    list = new HashSet<>();
×
508
                    data.pinnedResourceMap.put(l.stringValue(), list);
×
509
                }
510
                list.add(TemplateData.get().getTemplate(st.getSubject().stringValue()));
×
511
            }
512
        }
3✔
513
    }
3✔
514

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