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

knowledgepixels / nanodash / 30606286447

31 Jul 2026 05:15AM UTC coverage: 34.947% (+0.9%) from 34.084%
30606286447

push

github

web-flow
Merge pull request #579 from knowledgepixels/download-tab-doc-export

feat(download): rename Raw tab to Download and add HTML/RTF/PDF page export

2622 of 8308 branches covered (31.56%)

Branch coverage included in aggregate %.

5082 of 13737 relevant lines covered (36.99%)

5.74 hits per line

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

12.88
src/main/java/com/knowledgepixels/nanodash/domain/AbstractResourceWithProfile.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.ApiCache;
6
import com.knowledgepixels.nanodash.NanodashThreadPool;
7
import com.knowledgepixels.nanodash.QueryApiAccess;
8
import com.knowledgepixels.nanodash.ViewDisplay;
9
import com.knowledgepixels.nanodash.repository.SpaceRepository;
10
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
11
import org.eclipse.rdf4j.model.IRI;
12
import org.nanopub.Nanopub;
13
import org.nanopub.extra.services.ApiResponse;
14
import org.nanopub.extra.services.ApiResponseEntry;
15
import org.nanopub.extra.services.QueryRef;
16
import org.slf4j.Logger;
17
import org.slf4j.LoggerFactory;
18

19
import java.io.Serializable;
20
import java.util.*;
21
import java.util.concurrent.ConcurrentHashMap;
22
import java.util.concurrent.Future;
23

24
/**
25
 * Abstract class representing a resource with a profile in the Nanodash application.
26
 * This class provides common functionality for resources that have associated profiles, such as spaces and users.
27
 */
28
public abstract class AbstractResourceWithProfile implements Serializable, ResourceWithProfile {
29

30
    private static final Logger logger = LoggerFactory.getLogger(AbstractResourceWithProfile.class);
9✔
31

32
    private static final Map<Class<?>, Map<String, AbstractResourceWithProfile>> instances = new ConcurrentHashMap<>();
15✔
33

34
    // Backoff after a failed data update, so a persistently failing resource
35
    // doesn't respawn an update task on every page poll (~1/s per open page).
36
    private static final long FAILED_UPDATE_BACKOFF_MS = 10 * 1000;
37

38
    private final String id;
39
    private Space space;
40
    private ResourceWithProfile data = new ResourceWithProfile();
15✔
41
    private volatile boolean dataInitialized = false;
9✔
42
    private volatile boolean dataNeedsUpdate = true;
9✔
43
    private volatile Long runUpdateAfter = null;
9✔
44

45
    /**
46
     * Inner class to hold the data associated with a resource, including its view displays.
47
     */
48
    protected static class ResourceWithProfile implements Serializable {
6✔
49
        List<ViewDisplay> viewDisplays = new ArrayList<>();
18✔
50
    }
51

52
    /**
53
     * Checks if a resource with the given unique identifier exists in the system.
54
     *
55
     * @param id the unique identifier of the resource
56
     * @return true if a resource with the given id exists, false otherwise
57
     */
58
    public static boolean isResourceWithProfile(String id) {
59
        return get(id) != null;
×
60
    }
61

62
    /**
63
     * Retrieves an instance of AbstractResourceWithProfile by its unique identifier.
64
     *
65
     * @param id the unique identifier of the resource
66
     * @return the AbstractResourceWithProfile instance associated with the given id, or null if no such instance exists
67
     */
68
    public static AbstractResourceWithProfile get(String id) {
69
        for (Map<String, AbstractResourceWithProfile> map : instances.values()) {
33✔
70
            if (map.containsKey(id)) {
12✔
71
                return map.get(id);
15✔
72
            }
73
        }
3✔
74
        return null;
6✔
75
    }
76

77
    /**
78
     * Constructor for AbstractResourceWithProfile.
79
     *
80
     * @param id the unique identifier for this resource
81
     */
82
    protected AbstractResourceWithProfile(String id) {
6✔
83
        this.id = id;
9✔
84
        instances.computeIfAbsent(getClass(), k -> new ConcurrentHashMap<>()).put(id, this);
42✔
85
    }
3✔
86

87
    /**
88
     * Removes an instance of AbstractResourceWithProfile from the instances map based on its type and unique identifier.
89
     *
90
     * @param type the class type of the resource to remove
91
     * @param id   the unique identifier of the resource to remove
92
     */
93
    protected static void removeInstance(Class<?> type, String id) {
94
        Map<String, AbstractResourceWithProfile> map = instances.get(type);
×
95
        if (map != null) {
×
96
            map.remove(id);
×
97
        }
98
    }
×
99

100
    /**
101
     * Retrieves all instances of AbstractResourceWithProfile of a specific type.
102
     *
103
     * @param type the class type of the resources to retrieve
104
     * @return a map of resource IDs to AbstractResourceWithProfile instances of the specified type, or an empty map if no instances exist for that type
105
     */
106
    protected static Map<String, AbstractResourceWithProfile> getInstances(Class<?> type) {
107
        return instances.getOrDefault(type, Collections.emptyMap());
18✔
108
    }
109

110
    /**
111
     * Initializes the space for this resource.
112
     *
113
     * @param space the space to associate with this resource
114
     */
115
    protected void initSpace(Space space) {
116
        this.space = space;
9✔
117
        logger.info("Initialized space {} for resource {}", space.getId(), id);
21✔
118
    }
3✔
119

120
    @Override
121
    public String getId() {
122
        return id;
9✔
123
    }
124

125
    @Override
126
    public synchronized Future<?> triggerDataUpdate() {
127
        if (dataNeedsUpdate) {
×
128
            // Not due yet (delayed forceRefresh or backoff after a failure): don't
129
            // occupy a pool thread with sleeping; a later access re-triggers.
130
            Long after = runUpdateAfter;
×
131
            if (after != null && System.currentTimeMillis() < after) {
×
132
                return null;
×
133
            }
134
            runUpdateAfter = null;
×
135
            logger.info("Data needs update for resource {}, starting update thread", id);
×
136
            dataNeedsUpdate = false;
×
137
            return NanodashThreadPool.submit(() -> {
×
138
                try {
139
                    ResourceWithProfile newData = new ResourceWithProfile();
×
140

141
                    // The query returns both standalone view displays (bound ?display) and
142
                    // preset-supplied views (issue #302: unbound ?display, the assignment's
143
                    // preset expanded into its views server-side), already ordered by date
144
                    // (latest first) so the per-view-kind latest-wins / deactivation
145
                    // aggregation in getViewDisplays() resolves overrides between presets and
146
                    // standalone displays correctly, in either direction.
147
                    // For a space, scope the displays to its representative ref (root nanopub) so a
148
                    // multi-ref identifier doesn't merge displays across rival definitions; other
149
                    // resource kinds (and spaces with no known ref root) stay IRI-keyed.
150
                    String vdRefRoot = getViewDisplayRefRoot();
×
151
                    QueryRef vdQuery = (vdRefRoot != null && !vdRefRoot.isEmpty())
×
152
                            ? viewDisplaysRefQueryRef(vdRefRoot)
×
153
                            : new QueryRef(QueryApiAccess.GET_VIEW_DISPLAYS, "resource", id);
×
154
                    newData.viewDisplays.addAll(buildViewDisplays(vdQuery));
×
155
                    data = newData;
×
156
                    dataInitialized = true;
×
157
                } catch (Exception ex) {
×
158
                    logger.error("Error while trying to update data for resource {}", id, ex);
×
159
                    runUpdateAfter = System.currentTimeMillis() + FAILED_UPDATE_BACKOFF_MS;
×
160
                    dataNeedsUpdate = true;
×
161
                }
×
162
            });
×
163
        }
164
        return null;
×
165
    }
166

167
    /**
168
     * Forces a refresh of the resource data after a specified delay.
169
     *
170
     * @param waitMillis the delay in milliseconds before the data refresh is triggered
171
     */
172
    public void forceRefresh(long waitMillis) {
173
        logger.info("Forcing refresh of resource {} after {} ms", id, waitMillis);
×
174
        dataNeedsUpdate = true;
×
175
        dataInitialized = false;
×
176
        runUpdateAfter = System.currentTimeMillis() + waitMillis;
×
177
    }
×
178

179
    /**
180
     * Static method to force a refresh of the resource data for all instances of AbstractResourceWithProfile.
181
     */
182
    public static void refresh() {
183
        instances.values().forEach(map -> map.values().forEach(AbstractResourceWithProfile::setDataNeedsUpdate));
27✔
184
    }
3✔
185

186
    @Override
187
    public Long getRunUpdateAfter() {
188
        return runUpdateAfter;
×
189
    }
190

191
    @Override
192
    public Space getSpace() {
193
        return space;
×
194
    }
195

196
    @Override
197
    public abstract String getNanopubId();
198

199
    @Override
200
    public abstract Nanopub getNanopub();
201

202
    public abstract String getNamespace();
203

204
    @Override
205
    public void setDataNeedsUpdate() {
206
        dataNeedsUpdate = true;
9✔
207
    }
3✔
208

209
    @Override
210
    public boolean isDataInitialized() {
211
        triggerDataUpdate();
×
212
        return dataInitialized;
×
213
    }
214

215
    @Override
216
    public List<ViewDisplay> getViewDisplays() {
217
        logger.info("Getting view displays for resource {}", id);
×
218
        return data.viewDisplays;
×
219
    }
220

221
    @Override
222
    public List<ViewDisplay> getTopLevelViewDisplays() {
223
        // Pass the resource's own type(s) so that views targeting that type (e.g. a
224
        // messages view for gen:MaintainedResource) are shown at the top level, while
225
        // views targeting part types (e.g. gen:hasViewTargetClass owl:Class) are not.
226
        return getViewDisplays(true, getId(), getOwnClasses());
×
227
    }
228

229
    /**
230
     * The resource's own type IRI(s), used to match top-level views by
231
     * {@code gen:appliesToInstancesOf}. Empty by default; overridden per resource type.
232
     *
233
     * @return the resource's own classes (never null)
234
     */
235
    protected Set<IRI> getOwnClasses() {
236
        return Collections.emptySet();
×
237
    }
238

239
    @Override
240
    public List<ViewDisplay> getPartLevelViewDisplays(String resourceId, Set<IRI> classes) {
241
        return getViewDisplays(false, resourceId, classes);
×
242
    }
243

244
    private List<ViewDisplay> getViewDisplays(boolean toplevel, String resourceId, Set<IRI> classes) {
245
        triggerDataUpdate();
×
246
        return filterViewDisplays(getViewDisplays(), toplevel, resourceId, classes);
×
247
    }
248

249
    /**
250
     * Top-level view displays scoped to a specific space ref (root nanopub), fetched on demand
251
     * rather than from the IRI-keyed singleton data — used to render the Content tab of a
252
     * {@code ?root=}-pinned space page so it shows only that ref's displays. Falls back to the
253
     * default (singleton) displays when {@code refRoot} is null/empty. See docs/space-ref-identity.md.
254
     *
255
     * @param refRoot the ref's root nanopub, or null/empty for the default
256
     * @return the ref-scoped top-level view displays
257
     */
258
    public List<ViewDisplay> getTopLevelViewDisplays(String refRoot) {
259
        if (refRoot == null || refRoot.isEmpty()) return getTopLevelViewDisplays();
×
260
        return filterViewDisplays(buildViewDisplays(viewDisplaysRefQueryRef(refRoot)), true, getId(), getOwnClasses());
×
261
    }
262

263
    private List<ViewDisplay> filterViewDisplays(List<ViewDisplay> source, boolean toplevel, String resourceId, Set<IRI> classes) {
264
        List<ViewDisplay> viewDisplays = new ArrayList<>();
×
265
        Set<IRI> viewKinds = new HashSet<>();
×
266

267
        // Results are sorted by date (most recent first); only the most recent per view-kind is considered
268
        for (ViewDisplay vd : source) {
×
269
            IRI kind = vd.getViewKindIri();
×
270
            if (kind != null) {
×
271
                if (viewKinds.contains(kind)) {
×
272
                    continue;
×
273
                }
274
                viewKinds.add(kind);
×
275
            }
276

277
            if (vd.hasType(KPXL_TERMS.DEACTIVATED_VIEW_DISPLAY)) {
×
278
                continue;
×
279
            }
280

281
            if (!toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
282
                // Deprecated
283
                // do nothing
284
            } else if (vd.appliesTo(resourceId, classes)) {
×
285
                viewDisplays.add(vd);
×
286
            } else if (toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
287
                // Deprecated
288
                viewDisplays.add(vd);
×
289
            }
290
        }
×
291

292
        Collections.sort(viewDisplays);
×
293
        return viewDisplays;
×
294
    }
295

296
    /**
297
     * Fetches the applicable view displays synchronously from the API, bypassing the async
298
     * singleton data. Used by the download pages, which need the displays within the current
299
     * request; the resolution mirrors the page rendering (ref-scoped query for spaces,
300
     * preset-supplied views, per-view-kind latest-wins).
301
     *
302
     * @param partId      the part IRI, or null for the resource's top-level displays
303
     * @param partClasses the part's classes (ignored for top-level, where the resource's
304
     *                    own classes are used)
305
     * @return the applicable view displays, sorted by structural position
306
     */
307
    public List<ViewDisplay> fetchViewDisplaysSync(String partId, Set<IRI> partClasses) {
308
        String vdRefRoot = getViewDisplayRefRoot();
×
309
        QueryRef vdQuery = (vdRefRoot != null && !vdRefRoot.isEmpty())
×
310
                ? viewDisplaysRefQueryRef(vdRefRoot)
×
311
                : new QueryRef(QueryApiAccess.GET_VIEW_DISPLAYS, "resource", id);
×
312
        boolean toplevel = (partId == null);
×
313
        return filterViewDisplays(buildViewDisplays(vdQuery), toplevel,
×
314
                toplevel ? getId() : partId, toplevel ? getOwnClasses() : partClasses);
×
315
    }
316

317
    /**
318
     * Builds {@link ViewDisplay} objects from a get-view-displays(-ref) query result (standalone
319
     * displays with a bound {@code ?display}, and preset-supplied views with an unbound one).
320
     */
321
    private List<ViewDisplay> buildViewDisplays(QueryRef ref) {
322
        List<ViewDisplay> list = new ArrayList<>();
×
323
        ApiResponse response = ApiCache.retrieveResponseSync(ref, true);
×
324
        // Null on a cold cache or a failed (flaky federated) fetch — yield nothing for now; the
325
        // cache refreshes asynchronously and the page's auto-refresh repopulates it.
326
        if (response == null) return list;
×
327
        // The unresolved query variant returns ?view as the referenced version, leaving
328
        // latest-version resolution to us (View.get with resolveLatest=true, memoized;
329
        // it also covers space-governed pins); the older resolved heads return ?view
330
        // already latest-resolved server-side, so it is passed through as-is.
331
        boolean viewsPreResolved = !QueryApiAccess.GET_VIEW_DISPLAYS_UNRESOLVED.equals(ref.getQueryId());
×
332
        for (ApiResponseEntry r : response.getData()) {
×
333
            try {
334
                String display = r.get("display");
×
335
                if (display != null && !display.isEmpty()) {
×
336
                    list.add(ViewDisplay.get(display, viewsPreResolved ? r.get("view") : null));
×
337
                } else {
338
                    String view = r.get("view");
×
339
                    if (view == null || view.isEmpty()) continue;
×
340
                    boolean topLevel = KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY.stringValue().equals(r.get("displayType"));
×
341
                    boolean deactivated = KPXL_TERMS.DEACTIVATED_PRESET_ASSIGNMENT.stringValue().equals(r.get("displayMode"));
×
342
                    ViewDisplay vd = ViewDisplay.forPresetView(id, view, topLevel, deactivated, !viewsPreResolved);
×
343
                    if (vd != null) list.add(vd);
×
344
                }
345
            } catch (IllegalArgumentException ex) {
×
346
                logger.error("Couldn't generate view display object", ex);
×
347
            }
×
348
        }
×
349
        return list;
×
350
    }
351

352
    private QueryRef viewDisplaysRefQueryRef(String refRoot) {
353
        Multimap<String, String> params = ArrayListMultimap.create();
×
354
        params.put("resource", id);
×
355
        params.put("root_np", refRoot);
×
356
        return new QueryRef(QueryApiAccess.GET_VIEW_DISPLAYS_UNRESOLVED, params);
×
357
    }
358

359
    /**
360
     * The root nanopub of the space ref the resource's view displays should be scoped to, or null
361
     * to use the IRI-keyed query (merged across refs). Null by default; {@link Space} overrides it
362
     * to its representative ref so a multi-ref identifier doesn't merge Content-tab displays.
363
     *
364
     * @return the ref's root nanopub, or null
365
     */
366
    protected String getViewDisplayRefRoot() {
367
        return null;
×
368
    }
369

370
    @Override
371
    public abstract String getLabel();
372

373
    @Override
374
    public String toString() {
375
        return id;
×
376
    }
377

378
    /**
379
     * Gets the chain of superspaces from the current space up to the root space.
380
     *
381
     * @return the list of superspaces from the given space to the root space
382
     */
383
    @Override
384
    public List<AbstractResourceWithProfile> getAllSuperSpacesUntilRoot() {
385
        List<AbstractResourceWithProfile> chain = new ArrayList<>();
×
386
        Set<String> visited = new HashSet<>();
×
387
        collectAncestors(space, chain, visited);
×
388
        Collections.reverse(chain);
×
389
        return chain;
×
390
    }
391

392
    private void collectAncestors(Space current, List<AbstractResourceWithProfile> chain, Set<String> visited) {
393
        if (current == null) {
×
394
            return;
×
395
        }
396
        List<Space> parents = SpaceRepository.get().findSuperspaces(current);
×
397
        if (parents == null || parents.isEmpty()) {
×
398
            return;
×
399
        }
400
        Space parent = parents.getFirst();
×
401
        if (parent == null) {
×
402
            return;
×
403
        }
404
        String pid = parent.getId();
×
405
        if (pid == null || !visited.add(pid)) {
×
406
            return;
×
407
        }
408
        chain.add(parent);
×
409
        collectAncestors(parent, chain, visited);
×
410
    }
×
411

412
    /**
413
     * Checks if any view display of this resource applies to the given element ID and set of classes by triggering a data update and checking each view display for applicability.
414
     *
415
     * @param elementId the ID of the element to check for applicability
416
     * @param classes   the set of classes to check for applicability
417
     * @return true if any view display of this resource applies to the given element ID and set of classes, false otherwise
418
     */
419
    public boolean appliesTo(String elementId, Set<IRI> classes) {
420
        triggerDataUpdate();
×
421
        for (ViewDisplay v : getViewDisplays()) {
×
422
            if (v.appliesTo(elementId, classes)) {
×
423
                return true;
×
424
            }
425
        }
×
426
        return false;
×
427
    }
428

429
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc