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

knowledgepixels / nanodash / 27145358627

08 Jun 2026 02:39PM UTC coverage: 20.682% (-0.3%) from 20.947%
27145358627

push

github

web-flow
Merge pull request #479 from knowledgepixels/feat/about-pages-478

Resource-page tabs, presets, and role-gated view actions (#478, #302)

1052 of 6429 branches covered (16.36%)

Branch coverage included in aggregate %.

2642 of 11432 relevant lines covered (23.11%)

3.31 hits per line

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

15.47
src/main/java/com/knowledgepixels/nanodash/domain/AbstractResourceWithProfile.java
1
package com.knowledgepixels.nanodash.domain;
2

3
import com.knowledgepixels.nanodash.ApiCache;
4
import com.knowledgepixels.nanodash.NanodashThreadPool;
5
import com.knowledgepixels.nanodash.QueryApiAccess;
6
import com.knowledgepixels.nanodash.ViewDisplay;
7
import com.knowledgepixels.nanodash.repository.SpaceRepository;
8
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
9
import org.eclipse.rdf4j.model.IRI;
10
import org.nanopub.Nanopub;
11
import org.nanopub.extra.services.ApiResponseEntry;
12
import org.nanopub.extra.services.QueryRef;
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.ConcurrentHashMap;
19
import java.util.concurrent.Future;
20

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

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

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

31
    private final String id;
32
    private Space space;
33
    private ResourceWithProfile data = new ResourceWithProfile();
15✔
34
    private volatile boolean dataInitialized = false;
9✔
35
    private volatile boolean dataNeedsUpdate = true;
9✔
36
    private volatile Long runUpdateAfter = null;
9✔
37

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

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

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

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

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

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

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

113
    @Override
114
    public String getId() {
115
        return id;
9✔
116
    }
117

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

132
                    ResourceWithProfile newData = new ResourceWithProfile();
×
133

134
                    // The query returns both standalone view displays (bound ?display) and
135
                    // preset-supplied views (issue #302: unbound ?display, the assignment's
136
                    // preset expanded into its views server-side), already ordered by date
137
                    // (latest first) so the per-view-kind latest-wins / deactivation
138
                    // aggregation in getViewDisplays() resolves overrides between presets and
139
                    // standalone displays correctly, in either direction.
140
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_VIEW_DISPLAYS, "resource", id), true).getData()) {
×
141
                        try {
142
                            String display = r.get("display");
×
143
                            if (display != null && !display.isEmpty()) {
×
144
                                // The query resolves ?view to its latest version server-side, so
145
                                // pass it through to avoid a separate per-view latest-version lookup.
146
                                newData.viewDisplays.add(ViewDisplay.get(display, r.get("view")));
×
147
                            } else {
148
                                String view = r.get("view");
×
149
                                if (view == null || view.isEmpty()) continue;
×
150
                                boolean topLevel = KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY.stringValue().equals(r.get("displayType"));
×
151
                                boolean deactivated = KPXL_TERMS.DEACTIVATED_PRESET_ASSIGNMENT.stringValue().equals(r.get("displayMode"));
×
152
                                ViewDisplay vd = ViewDisplay.forPresetView(id, view, topLevel, deactivated);
×
153
                                if (vd != null) newData.viewDisplays.add(vd);
×
154
                            }
155
                        } catch (IllegalArgumentException ex) {
×
156
                            logger.error("Couldn't generate view display object", ex);
×
157
                        }
×
158
                    }
×
159
                    data = newData;
×
160
                    dataInitialized = true;
×
161
                } catch (Exception ex) {
×
162
                    logger.error("Error while trying to update data for resource {}", id, ex);
×
163
                    dataNeedsUpdate = true;
×
164
                }
×
165
            });
×
166
        }
167
        return null;
×
168
    }
169

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

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

189
    @Override
190
    public Long getRunUpdateAfter() {
191
        return runUpdateAfter;
×
192
    }
193

194
    @Override
195
    public Space getSpace() {
196
        return space;
×
197
    }
198

199
    @Override
200
    public abstract String getNanopubId();
201

202
    @Override
203
    public abstract Nanopub getNanopub();
204

205
    public abstract String getNamespace();
206

207
    @Override
208
    public void setDataNeedsUpdate() {
209
        dataNeedsUpdate = true;
9✔
210
    }
3✔
211

212
    @Override
213
    public boolean isDataInitialized() {
214
        triggerDataUpdate();
×
215
        return dataInitialized;
×
216
    }
217

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

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

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

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

247
    private List<ViewDisplay> getViewDisplays(boolean toplevel, String resourceId, Set<IRI> classes) {
248
        triggerDataUpdate();
×
249
        List<ViewDisplay> viewDisplays = new ArrayList<>();
×
250
        Set<IRI> viewKinds = new HashSet<>();
×
251

252
        // Results are sorted by date (most recent first); only the most recent per view-kind is considered
253
        for (ViewDisplay vd : getViewDisplays()) {
×
254
            IRI kind = vd.getViewKindIri();
×
255
            if (kind != null) {
×
256
                if (viewKinds.contains(kind)) {
×
257
                    continue;
×
258
                }
259
                viewKinds.add(kind);
×
260
            }
261

262
            if (vd.hasType(KPXL_TERMS.DEACTIVATED_VIEW_DISPLAY)) {
×
263
                continue;
×
264
            }
265

266
            if (!toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
267
                // Deprecated
268
                // do nothing
269
            } else if (vd.appliesTo(resourceId, classes)) {
×
270
                viewDisplays.add(vd);
×
271
            } else if (toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
272
                // Deprecated
273
                viewDisplays.add(vd);
×
274
            }
275
        }
×
276

277
        Collections.sort(viewDisplays);
×
278
        return viewDisplays;
×
279
    }
280

281
    @Override
282
    public abstract String getLabel();
283

284
    @Override
285
    public String toString() {
286
        return id;
×
287
    }
288

289
    /**
290
     * Gets the chain of superspaces from the current space up to the root space.
291
     *
292
     * @return the list of superspaces from the given space to the root space
293
     */
294
    @Override
295
    public List<AbstractResourceWithProfile> getAllSuperSpacesUntilRoot() {
296
        List<AbstractResourceWithProfile> chain = new ArrayList<>();
×
297
        Set<String> visited = new HashSet<>();
×
298
        collectAncestors(space, chain, visited);
×
299
        Collections.reverse(chain);
×
300
        return chain;
×
301
    }
302

303
    private void collectAncestors(Space current, List<AbstractResourceWithProfile> chain, Set<String> visited) {
304
        if (current == null) {
×
305
            return;
×
306
        }
307
        List<Space> parents = SpaceRepository.get().findSuperspaces(current);
×
308
        if (parents == null || parents.isEmpty()) {
×
309
            return;
×
310
        }
311
        Space parent = parents.getFirst();
×
312
        if (parent == null) {
×
313
            return;
×
314
        }
315
        String pid = parent.getId();
×
316
        if (pid == null || !visited.add(pid)) {
×
317
            return;
×
318
        }
319
        chain.add(parent);
×
320
        collectAncestors(parent, chain, visited);
×
321
    }
×
322

323
    /**
324
     * 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.
325
     *
326
     * @param elementId the ID of the element to check for applicability
327
     * @param classes   the set of classes to check for applicability
328
     * @return true if any view display of this resource applies to the given element ID and set of classes, false otherwise
329
     */
330
    public boolean appliesTo(String elementId, Set<IRI> classes) {
331
        triggerDataUpdate();
×
332
        for (ViewDisplay v : getViewDisplays()) {
×
333
            if (v.appliesTo(elementId, classes)) {
×
334
                return true;
×
335
            }
336
        }
×
337
        return false;
×
338
    }
339

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