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

knowledgepixels / nanodash / 27128215434

08 Jun 2026 09:24AM UTC coverage: 20.467% (-0.5%) from 20.924%
27128215434

Pull #479

github

web-flow
Merge 1d0c37666 into 5556185a4
Pull Request #479: About pages for spaces, maintained resources, and users (#478)

1024 of 6405 branches covered (15.99%)

Branch coverage included in aggregate %.

2624 of 11419 relevant lines covered (22.98%)

3.29 hits per line

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

14.29
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.NanodashSession;
5
import com.knowledgepixels.nanodash.NanodashThreadPool;
6
import com.knowledgepixels.nanodash.QueryApiAccess;
7
import com.knowledgepixels.nanodash.ViewDisplay;
8
import com.knowledgepixels.nanodash.repository.SpaceRepository;
9
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
10
import org.eclipse.rdf4j.model.IRI;
11
import org.nanopub.Nanopub;
12
import org.nanopub.extra.services.ApiResponseEntry;
13
import org.nanopub.extra.services.QueryRef;
14
import org.slf4j.Logger;
15
import org.slf4j.LoggerFactory;
16

17
import java.io.Serializable;
18
import java.util.*;
19
import java.util.concurrent.ConcurrentHashMap;
20
import java.util.concurrent.Future;
21

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

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

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

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

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

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

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

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

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

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

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

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

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

133
                    ResourceWithProfile newData = new ResourceWithProfile();
×
134

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

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

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

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

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

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

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

206
    public abstract String getNamespace();
207

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

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

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

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

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

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

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

253
        // Role-based visibility (docs/role-specific-views.md): a display restricted
254
        // via gen:isVisibleTo is shown only to viewers holding the required role
255
        // tier or specific role in the governing space. For a space-less resource
256
        // (e.g. a user page) a restricted display is shown only to the page owner.
257
        Space governingSpace = (this instanceof Space s) ? s : getSpace();
×
258
        IRI viewer = NanodashSession.getCurrentUserIriOrNull();
×
259
        boolean viewerIsOwner = viewer != null && (this instanceof IndividualAgent ia) && ia.isCurrentUser();
×
260

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

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

275
            // Drop displays this viewer is not entitled to see. Done after the
276
            // per-view-kind latest-wins pick above, so a hidden latest display
277
            // does not fall back to an older, more-visible version of the same kind.
278
            if (!vd.isVisibleTo(viewer, governingSpace, viewerIsOwner)) {
×
279
                continue;
×
280
            }
281

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

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

297
    @Override
298
    public abstract String getLabel();
299

300
    @Override
301
    public String toString() {
302
        return id;
×
303
    }
304

305
    /**
306
     * Gets the chain of superspaces from the current space up to the root space.
307
     *
308
     * @return the list of superspaces from the given space to the root space
309
     */
310
    @Override
311
    public List<AbstractResourceWithProfile> getAllSuperSpacesUntilRoot() {
312
        List<AbstractResourceWithProfile> chain = new ArrayList<>();
×
313
        Set<String> visited = new HashSet<>();
×
314
        collectAncestors(space, chain, visited);
×
315
        Collections.reverse(chain);
×
316
        return chain;
×
317
    }
318

319
    private void collectAncestors(Space current, List<AbstractResourceWithProfile> chain, Set<String> visited) {
320
        if (current == null) {
×
321
            return;
×
322
        }
323
        List<Space> parents = SpaceRepository.get().findSuperspaces(current);
×
324
        if (parents == null || parents.isEmpty()) {
×
325
            return;
×
326
        }
327
        Space parent = parents.getFirst();
×
328
        if (parent == null) {
×
329
            return;
×
330
        }
331
        String pid = parent.getId();
×
332
        if (pid == null || !visited.add(pid)) {
×
333
            return;
×
334
        }
335
        chain.add(parent);
×
336
        collectAncestors(parent, chain, visited);
×
337
    }
×
338

339
    /**
340
     * 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.
341
     *
342
     * @param elementId the ID of the element to check for applicability
343
     * @param classes   the set of classes to check for applicability
344
     * @return true if any view display of this resource applies to the given element ID and set of classes, false otherwise
345
     */
346
    public boolean appliesTo(String elementId, Set<IRI> classes) {
347
        triggerDataUpdate();
×
348
        for (ViewDisplay v : getViewDisplays()) {
×
349
            if (v.appliesTo(elementId, classes)) {
×
350
                return true;
×
351
            }
352
        }
×
353
        return false;
×
354
    }
355

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