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

knowledgepixels / nanodash / 23138432878

16 Mar 2026 10:09AM UTC coverage: 15.99% (+0.2%) from 15.811%
23138432878

push

github

web-flow
Merge pull request #402 from knowledgepixels/fix/401-bounded-api-cache

Fix unbounded memory growth and resource exhaustion

717 of 5509 branches covered (13.02%)

Branch coverage included in aggregate %.

1810 of 10295 relevant lines covered (17.58%)

2.39 hits per line

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

26.42
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);
12✔
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 String getCoreInfoString(ApiResponseEntry resp) {
90
        String id = resp.get("space");
×
91
        String rootNanopubId = resp.get("np");
×
92
        return id + " " + rootNanopubId;
×
93
    }
94

95
    private volatile boolean dataInitialized = false;
9✔
96
    private volatile boolean dataNeedsUpdate = true;
9✔
97

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

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

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

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

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

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

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

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

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

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

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

198

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

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

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

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

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

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

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

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

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

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

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

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

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

326
    private synchronized void ensureInitialized() {
327
        Future<?> future = triggerSpaceDataUpdate();
×
328
        if (!dataInitialized && future != null) {
×
329
            try {
330
                future.get(30, TimeUnit.SECONDS);
×
331
            } catch (Exception ex) {
×
332
                logger.error("failed to await space data update", ex);
×
333
            }
×
334
        }
335
        future = super.triggerDataUpdate();
×
336
        if (!dataInitialized && future != null) {
×
337
            try {
338
                future.get(30, TimeUnit.SECONDS);
×
339
            } catch (Exception ex) {
×
340
                logger.error("failed to await data update", ex);
×
341
            }
×
342
        }
343
    }
×
344

345
    @Override
346
    public synchronized Future<?> triggerDataUpdate() {
347
        triggerSpaceDataUpdate();
×
348
        return super.triggerDataUpdate();
×
349
    }
350

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

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

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

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

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

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

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

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

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

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

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

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