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

knowledgepixels / nanodash / 27693528866

17 Jun 2026 01:44PM UTC coverage: 26.673% (-0.3%) from 26.963%
27693528866

Pull #484

github

web-flow
Merge 4eef2b421 into 0f6281554
Pull Request #484: Space-ref disambiguation: conflict notice, claimants overview, ref-pinned pages

1545 of 6815 branches covered (22.67%)

Branch coverage included in aggregate %.

3414 of 11777 relevant lines covered (28.99%)

4.26 hits per line

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

14.42
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
    private final String id;
35
    private Space space;
36
    private ResourceWithProfile data = new ResourceWithProfile();
15✔
37
    private volatile boolean dataInitialized = false;
9✔
38
    private volatile boolean dataNeedsUpdate = true;
9✔
39
    private volatile Long runUpdateAfter = null;
9✔
40

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

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

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

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

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

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

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

116
    @Override
117
    public String getId() {
118
        return id;
9✔
119
    }
120

121
    @Override
122
    public synchronized Future<?> triggerDataUpdate() {
123
        if (dataNeedsUpdate) {
×
124
            logger.info("Data needs update for resource {}, starting update thread", id);
×
125
            dataNeedsUpdate = false;
×
126
            return NanodashThreadPool.submit(() -> {
×
127
                try {
128
                    if (runUpdateAfter != null) {
×
129
                        while (System.currentTimeMillis() < runUpdateAfter) {
×
130
                            Thread.sleep(100);
×
131
                        }
132
                        runUpdateAfter = null;
×
133
                    }
134

135
                    ResourceWithProfile newData = new ResourceWithProfile();
×
136

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

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

174
    /**
175
     * Static method to force a refresh of the resource data for all instances of AbstractResourceWithProfile.
176
     */
177
    public static void refresh() {
178
        instances.values().forEach(map -> map.values().forEach(AbstractResourceWithProfile::setDataNeedsUpdate));
12✔
179
    }
3✔
180

181
    @Override
182
    public Long getRunUpdateAfter() {
183
        return runUpdateAfter;
×
184
    }
185

186
    @Override
187
    public Space getSpace() {
188
        return space;
×
189
    }
190

191
    @Override
192
    public abstract String getNanopubId();
193

194
    @Override
195
    public abstract Nanopub getNanopub();
196

197
    public abstract String getNamespace();
198

199
    @Override
200
    public void setDataNeedsUpdate() {
201
        dataNeedsUpdate = true;
9✔
202
    }
3✔
203

204
    @Override
205
    public boolean isDataInitialized() {
206
        triggerDataUpdate();
×
207
        return dataInitialized;
×
208
    }
209

210
    @Override
211
    public List<ViewDisplay> getViewDisplays() {
212
        logger.info("Getting view displays for resource {}", id);
×
213
        return data.viewDisplays;
×
214
    }
215

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

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

234
    @Override
235
    public List<ViewDisplay> getPartLevelViewDisplays(String resourceId, Set<IRI> classes) {
236
        return getViewDisplays(false, resourceId, classes);
×
237
    }
238

239
    private List<ViewDisplay> getViewDisplays(boolean toplevel, String resourceId, Set<IRI> classes) {
240
        triggerDataUpdate();
×
241
        return filterViewDisplays(getViewDisplays(), toplevel, resourceId, classes);
×
242
    }
243

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

258
    private List<ViewDisplay> filterViewDisplays(List<ViewDisplay> source, boolean toplevel, String resourceId, Set<IRI> classes) {
259
        List<ViewDisplay> viewDisplays = new ArrayList<>();
×
260
        Set<IRI> viewKinds = new HashSet<>();
×
261

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

272
            if (vd.hasType(KPXL_TERMS.DEACTIVATED_VIEW_DISPLAY)) {
×
273
                continue;
×
274
            }
275

276
            if (!toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
277
                // Deprecated
278
                // do nothing
279
            } else if (vd.appliesTo(resourceId, classes)) {
×
280
                viewDisplays.add(vd);
×
281
            } else if (toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
282
                // Deprecated
283
                viewDisplays.add(vd);
×
284
            }
285
        }
×
286

287
        Collections.sort(viewDisplays);
×
288
        return viewDisplays;
×
289
    }
290

291
    /**
292
     * Builds {@link ViewDisplay} objects from a get-view-displays(-ref) query result (standalone
293
     * displays with a bound {@code ?display}, and preset-supplied views with an unbound one).
294
     */
295
    private List<ViewDisplay> buildViewDisplays(QueryRef ref) {
296
        List<ViewDisplay> list = new ArrayList<>();
×
297
        ApiResponse response = ApiCache.retrieveResponseSync(ref, true);
×
298
        // Null on a cold cache or a failed (flaky federated) fetch — yield nothing for now; the
299
        // cache refreshes asynchronously and the page's auto-refresh repopulates it.
300
        if (response == null) return list;
×
301
        for (ApiResponseEntry r : response.getData()) {
×
302
            try {
303
                String display = r.get("display");
×
304
                if (display != null && !display.isEmpty()) {
×
305
                    // The query resolves ?view to its latest version server-side, so
306
                    // pass it through to avoid a separate per-view latest-version lookup.
307
                    list.add(ViewDisplay.get(display, r.get("view")));
×
308
                } else {
309
                    String view = r.get("view");
×
310
                    if (view == null || view.isEmpty()) continue;
×
311
                    boolean topLevel = KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY.stringValue().equals(r.get("displayType"));
×
312
                    boolean deactivated = KPXL_TERMS.DEACTIVATED_PRESET_ASSIGNMENT.stringValue().equals(r.get("displayMode"));
×
313
                    ViewDisplay vd = ViewDisplay.forPresetView(id, view, topLevel, deactivated);
×
314
                    if (vd != null) list.add(vd);
×
315
                }
316
            } catch (IllegalArgumentException ex) {
×
317
                logger.error("Couldn't generate view display object", ex);
×
318
            }
×
319
        }
×
320
        return list;
×
321
    }
322

323
    private QueryRef viewDisplaysRefQueryRef(String refRoot) {
324
        Multimap<String, String> params = ArrayListMultimap.create();
×
325
        params.put("resource", id);
×
326
        params.put("root_np", refRoot);
×
327
        return new QueryRef(QueryApiAccess.GET_VIEW_DISPLAYS_REF, params);
×
328
    }
329

330
    /**
331
     * The root nanopub of the space ref the resource's view displays should be scoped to, or null
332
     * to use the IRI-keyed query (merged across refs). Null by default; {@link Space} overrides it
333
     * to its representative ref so a multi-ref identifier doesn't merge Content-tab displays.
334
     *
335
     * @return the ref's root nanopub, or null
336
     */
337
    protected String getViewDisplayRefRoot() {
338
        return null;
×
339
    }
340

341
    @Override
342
    public abstract String getLabel();
343

344
    @Override
345
    public String toString() {
346
        return id;
×
347
    }
348

349
    /**
350
     * Gets the chain of superspaces from the current space up to the root space.
351
     *
352
     * @return the list of superspaces from the given space to the root space
353
     */
354
    @Override
355
    public List<AbstractResourceWithProfile> getAllSuperSpacesUntilRoot() {
356
        List<AbstractResourceWithProfile> chain = new ArrayList<>();
×
357
        Set<String> visited = new HashSet<>();
×
358
        collectAncestors(space, chain, visited);
×
359
        Collections.reverse(chain);
×
360
        return chain;
×
361
    }
362

363
    private void collectAncestors(Space current, List<AbstractResourceWithProfile> chain, Set<String> visited) {
364
        if (current == null) {
×
365
            return;
×
366
        }
367
        List<Space> parents = SpaceRepository.get().findSuperspaces(current);
×
368
        if (parents == null || parents.isEmpty()) {
×
369
            return;
×
370
        }
371
        Space parent = parents.getFirst();
×
372
        if (parent == null) {
×
373
            return;
×
374
        }
375
        String pid = parent.getId();
×
376
        if (pid == null || !visited.add(pid)) {
×
377
            return;
×
378
        }
379
        chain.add(parent);
×
380
        collectAncestors(parent, chain, visited);
×
381
    }
×
382

383
    /**
384
     * 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.
385
     *
386
     * @param elementId the ID of the element to check for applicability
387
     * @param classes   the set of classes to check for applicability
388
     * @return true if any view display of this resource applies to the given element ID and set of classes, false otherwise
389
     */
390
    public boolean appliesTo(String elementId, Set<IRI> classes) {
391
        triggerDataUpdate();
×
392
        for (ViewDisplay v : getViewDisplays()) {
×
393
            if (v.appliesTo(elementId, classes)) {
×
394
                return true;
×
395
            }
396
        }
×
397
        return false;
×
398
    }
399

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