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

knowledgepixels / nanodash / 23133579782

16 Mar 2026 08:00AM UTC coverage: 15.975% (+0.2%) from 15.811%
23133579782

Pull #402

github

web-flow
Merge 2ffdda823 into 39c6ac11c
Pull Request #402: Fix unbounded memory growth and resource exhaustion

717 of 5521 branches covered (12.99%)

Branch coverage included in aggregate %.

1811 of 10304 relevant lines covered (17.58%)

2.39 hits per line

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

17.32
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
    protected static final long DATA_REFRESH_TTL_MS = 60_000;
32

33
    private final String id;
34
    private Space space;
35
    private ResourceWithProfile data = new ResourceWithProfile();
15✔
36
    private volatile boolean dataInitialized = false;
9✔
37
    private volatile boolean dataNeedsUpdate = true;
9✔
38
    private volatile Long runUpdateAfter = null;
9✔
39
    private volatile long lastDataUpdate = 0;
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);
15✔
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 && dataInitialized && System.currentTimeMillis() - lastDataUpdate > DATA_REFRESH_TTL_MS) {
×
124
            dataNeedsUpdate = true;
×
125
            dataInitialized = false;
×
126
        }
127
        if (dataNeedsUpdate) {
×
128
            logger.info("Data needs update for resource {}, starting update thread", id);
×
129
            dataNeedsUpdate = false;
×
130
            return NanodashThreadPool.submit(() -> {
×
131
                try {
132
                    if (runUpdateAfter != null) {
×
133
                        while (System.currentTimeMillis() < runUpdateAfter) {
×
134
                            Thread.sleep(100);
×
135
                        }
136
                        runUpdateAfter = null;
×
137
                    }
138

139
                    ResourceWithProfile newData = new ResourceWithProfile();
×
140

141
                    for (ApiResponseEntry r : ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_VIEW_DISPLAYS, "resource", id), true).getData()) {
×
142
                        if (space != null && !space.isAdminPubkey(r.get("pubkey"))) {
×
143
                            continue;
×
144
                        }
145
                        try {
146
                            newData.viewDisplays.add(ViewDisplay.get(r.get("display")));
×
147
                        } catch (IllegalArgumentException ex) {
×
148
                            logger.error("Couldn't generate view display object", ex);
×
149
                        }
×
150
                    }
×
151
                    data = newData;
×
152
                    dataInitialized = true;
×
153
                    lastDataUpdate = System.currentTimeMillis();
×
154
                } catch (Exception ex) {
×
155
                    logger.error("Error while trying to update data for resource {}", id, ex);
×
156
                    dataNeedsUpdate = true;
×
157
                }
×
158
            });
×
159
        }
160
        return null;
×
161
    }
162

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

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

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

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

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

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

198
    public abstract String getNamespace();
199

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

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

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

217
    @Override
218
    public List<ViewDisplay> getTopLevelViewDisplays() {
219
        return getViewDisplays(true, getId(), null);
×
220
    }
221

222
    @Override
223
    public List<ViewDisplay> getPartLevelViewDisplays(String resourceId, Set<IRI> classes) {
224
        return getViewDisplays(false, resourceId, classes);
×
225
    }
226

227
    private List<ViewDisplay> getViewDisplays(boolean toplevel, String resourceId, Set<IRI> classes) {
228
        triggerDataUpdate();
×
229
        List<ViewDisplay> viewDisplays = new ArrayList<>();
×
230
        Set<IRI> viewKinds = new HashSet<>();
×
231

232
        // Results are sorted by date (most recent first); only the most recent per view-kind is considered
233
        for (ViewDisplay vd : getViewDisplays()) {
×
234
            IRI kind = vd.getViewKindIri();
×
235
            if (kind != null) {
×
236
                if (viewKinds.contains(kind)) {
×
237
                    continue;
×
238
                }
239
                viewKinds.add(kind);
×
240
            }
241

242
            if (vd.hasType(KPXL_TERMS.DEACTIVATED_VIEW_DISPLAY)) {
×
243
                continue;
×
244
            }
245

246
            if (!toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
247
                // Deprecated
248
                // do nothing
249
            } else if (vd.appliesTo(resourceId, classes)) {
×
250
                viewDisplays.add(vd);
×
251
            } else if (toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
252
                // Deprecated
253
                viewDisplays.add(vd);
×
254
            }
255
        }
×
256

257
        Collections.sort(viewDisplays);
×
258
        return viewDisplays;
×
259
    }
260

261
    @Override
262
    public abstract String getLabel();
263

264
    @Override
265
    public String toString() {
266
        return id;
×
267
    }
268

269
    /**
270
     * Gets the chain of superspaces from the current space up to the root space.
271
     *
272
     * @return the list of superspaces from the given space to the root space
273
     */
274
    @Override
275
    public List<AbstractResourceWithProfile> getAllSuperSpacesUntilRoot() {
276
        List<AbstractResourceWithProfile> chain = new ArrayList<>();
×
277
        Set<String> visited = new HashSet<>();
×
278
        collectAncestors(space, chain, visited);
×
279
        Collections.reverse(chain);
×
280
        return chain;
×
281
    }
282

283
    private void collectAncestors(Space current, List<AbstractResourceWithProfile> chain, Set<String> visited) {
284
        if (current == null) {
×
285
            return;
×
286
        }
287
        List<Space> parents = SpaceRepository.get().findSuperspaces(current);
×
288
        if (parents == null || parents.isEmpty()) {
×
289
            return;
×
290
        }
291
        Space parent = parents.getFirst();
×
292
        if (parent == null) {
×
293
            return;
×
294
        }
295
        String pid = parent.getId();
×
296
        if (pid == null || !visited.add(pid)) {
×
297
            return;
×
298
        }
299
        chain.add(parent);
×
300
        collectAncestors(parent, chain, visited);
×
301
    }
×
302

303
    /**
304
     * 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.
305
     *
306
     * @param elementId the ID of the element to check for applicability
307
     * @param classes   the set of classes to check for applicability
308
     * @return true if any view display of this resource applies to the given element ID and set of classes, false otherwise
309
     */
310
    public boolean appliesTo(String elementId, Set<IRI> classes) {
311
        triggerDataUpdate();
×
312
        for (ViewDisplay v : getViewDisplays()) {
×
313
            if (v.appliesTo(elementId, classes)) {
×
314
                return true;
×
315
            }
316
        }
×
317
        return false;
×
318
    }
319

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