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

knowledgepixels / nanodash / 26455055595

26 May 2026 02:38PM UTC coverage: 20.427% (-0.3%) from 20.748%
26455055595

Pull #468

github

web-flow
Merge 0354b3eb9 into 65b0c8452
Pull Request #468: Source space data from nanopub-query spaces repo

1005 of 6260 branches covered (16.05%)

Branch coverage included in aggregate %.

2600 of 11388 relevant lines covered (22.83%)

3.27 hits per line

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

15.65
src/main/java/com/knowledgepixels/nanodash/page/DownloadRdfPage.java
1
package com.knowledgepixels.nanodash.page;
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.QueryApiAccess;
7
import com.knowledgepixels.nanodash.SpaceMemberRoleRef;
8
import com.knowledgepixels.nanodash.Utils;
9
import com.knowledgepixels.nanodash.View;
10
import com.knowledgepixels.nanodash.ViewDisplay;
11
import com.knowledgepixels.nanodash.component.QueryParamField;
12
import com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile;
13
import com.knowledgepixels.nanodash.domain.IndividualAgent;
14
import com.knowledgepixels.nanodash.domain.MaintainedResource;
15
import com.knowledgepixels.nanodash.domain.Space;
16
import com.knowledgepixels.nanodash.domain.User;
17
import com.knowledgepixels.nanodash.repository.MaintainedResourceRepository;
18
import com.knowledgepixels.nanodash.repository.SpaceRepository;
19
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
20
import org.apache.wicket.markup.html.WebPage;
21
import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
22
import org.apache.wicket.request.mapper.parameter.PageParameters;
23
import org.apache.wicket.request.resource.ContentDisposition;
24
import org.apache.wicket.util.resource.AbstractResourceStreamWriter;
25
import org.eclipse.rdf4j.model.IRI;
26
import org.eclipse.rdf4j.model.Statement;
27
import org.eclipse.rdf4j.model.vocabulary.RDF;
28
import org.eclipse.rdf4j.rio.RDFFormat;
29
import org.eclipse.rdf4j.rio.RDFWriter;
30
import org.eclipse.rdf4j.rio.Rio;
31
import org.nanopub.Nanopub;
32
import org.nanopub.NanopubUtils;
33
import org.nanopub.NanopubWithNs;
34
import org.nanopub.extra.services.ApiResponse;
35
import org.nanopub.extra.services.ApiResponseEntry;
36
import org.nanopub.extra.services.QueryRef;
37
import org.nanopub.extra.setting.IntroNanopub;
38
import org.slf4j.Logger;
39
import org.slf4j.LoggerFactory;
40

41
import java.io.IOException;
42
import java.io.OutputStream;
43
import java.util.*;
44

45
/**
46
 * Page that serves a bulk RDF download of all nanopublications shown on a given page.
47
 * Supports TriG, TriX, JSON-LD, and N-Quads formats.
48
 *
49
 * Parameters:
50
 * - type: "user", "space", "resource", or "part"
51
 * - id: the resource identifier
52
 * - context: (required for type=part) the context resource ID
53
 * - format: "trig" (default), "trix", "jsonld", or "nq"
54
 *
55
 * NOTE: Currently limited to a maximum of 1000 nanopubs. Pagination is not yet implemented.
56
 */
57
public class DownloadRdfPage extends WebPage {
58

59
    private static final Logger logger = LoggerFactory.getLogger(DownloadRdfPage.class);
9✔
60

61
    public static final String MOUNT_PATH = "/download-rdf";
62

63
    /**
64
     * Maximum number of nanopubs to include in a single download.
65
     * TODO: Implement pagination or streaming for larger downloads.
66
     */
67
    private static final int MAX_NANOPUBS = 1000;
68

69
    private static final Map<String, RDFFormat> FORMAT_MAP = Map.of(
48✔
70
            "trig", RDFFormat.TRIG,
71
            "trix", RDFFormat.TRIX,
72
            "jsonld", RDFFormat.JSONLD,
73
            "nq", RDFFormat.NQUADS,
74
            "turtle", RDFFormat.TURTLE,
75
            "nt", RDFFormat.NTRIPLES,
76
            "rdfxml", RDFFormat.RDFXML
77
    );
78

79
    private static final Map<String, String> EXTENSION_MAP = Map.of(
51✔
80
            "trig", ".trig",
81
            "trix", ".xml",
82
            "jsonld", ".jsonld",
83
            "nq", ".nq",
84
            "turtle", ".ttl",
85
            "nt", ".nt",
86
            "rdfxml", ".rdf"
87
    );
88

89
    public DownloadRdfPage(final PageParameters parameters) {
90
        super(parameters);
×
91

92
        String type = parameters.get("type").toString();
×
93
        String id = parameters.get("id").toString();
×
94
        String format = parameters.get("format").toString("trig");
×
95
        boolean asText = !parameters.get("txt").isNull();
×
96
        boolean assertionsOnly = !parameters.get("assertions").isNull();
×
97

98
        if (type == null) {
×
99
            throw new IllegalArgumentException("Parameter 'type' is required");
×
100
        }
101
        if (id == null && !"list".equals(type)) {
×
102
            throw new IllegalArgumentException("Parameter 'id' is required for type: " + type);
×
103
        }
104

105
        RDFFormat rdfFormat = FORMAT_MAP.get(format);
×
106
        if (rdfFormat == null) {
×
107
            throw new IllegalArgumentException("Unsupported format: " + format);
×
108
        }
109

110
        // Resolve the resource and collect nanopubs
111
        List<Nanopub> nanopubs = collectNanopubs(type, id, parameters);
×
112

113
        if (nanopubs.size() >= MAX_NANOPUBS) {
×
114
            logger.warn("Download for {} {} reached the maximum of {} nanopubs. Results may be incomplete.",
×
115
                    type, id, MAX_NANOPUBS);
×
116
        }
117

118
        logger.info("Serving RDF download: {} nanopubs in {} format ({}) for {} {}",
×
119
                nanopubs.size(), format, assertionsOnly ? "assertions only" : "full", type, id);
×
120

121
        // Build filename from the resource label or ID
122
        String extension = EXTENSION_MAP.get(format) + (asText ? ".txt" : "");
×
123
        String prefix = assertionsOnly ? "assertions_" : "";
×
124
        String filename;
125
        if (id != null) {
×
126
            String safeId = id.replaceAll("[^a-zA-Z0-9_-]", "_");
×
127
            if (safeId.length() > 60) safeId = safeId.substring(safeId.length() - 60);
×
128
            filename = prefix + type + "_" + safeId + extension;
×
129
        } else {
×
130
            filename = prefix + type + extension;
×
131
        }
132

133
        // When txt parameter is present, serve as text/plain so it always displays in browser
134
        String contentType = asText ? "text/plain; charset=utf-8" : rdfFormat.getDefaultMIMEType() + "; charset=utf-8";
×
135

136
        AbstractResourceStreamWriter stream = new AbstractResourceStreamWriter() {
×
137
            @Override
138
            public void write(OutputStream output) throws IOException {
139
                if (assertionsOnly) {
×
140
                    writeAssertions(output, nanopubs, rdfFormat);
×
141
                } else if (rdfFormat == RDFFormat.JSONLD) {
×
142
                    writeJsonLdArray(output, nanopubs);
×
143
                } else {
144
                    for (Nanopub np : nanopubs) {
×
145
                        try {
146
                            NanopubUtils.writeToStream(np, output, rdfFormat);
×
147
                            output.write('\n');
×
148
                        } catch (Exception ex) {
×
149
                            if (ex.toString().contains("ClientAbortException") || ex.toString().contains("Broken pipe")) {
×
150
                                logger.debug("Client disconnected during RDF download");
×
151
                                break;
×
152
                            }
153
                            logger.error("Error serializing nanopub {}: {}", np.getUri(), ex.getMessage());
×
154
                        }
×
155
                    }
×
156
                }
157
            }
×
158

159
            @Override
160
            public String getContentType() {
161
                return contentType;
×
162
            }
163
        };
164

165
        ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(stream, filename);
×
166
        handler.setContentDisposition(ContentDisposition.INLINE);
×
167
        handler.setCacheDuration(java.time.Duration.ZERO);
×
168
        getRequestCycle().scheduleRequestHandlerAfterCurrent(handler);
×
169
    }
×
170

171
    /**
172
     * Collects all nanopubs for the given page type and resource.
173
     * Includes declarations, approved intros, roles, role assignments, and view query results.
174
     * Does not include the view display definition nanopubs themselves.
175
     */
176
    private List<Nanopub> collectNanopubs(String type, String id, PageParameters parameters) {
177
        Map<String, Nanopub> collected = new LinkedHashMap<>();
×
178

179
        AbstractResourceWithProfile resource;
180
        String partId = null;
×
181
        Set<IRI> partClasses = null;
×
182
        String nanopubRef = null;
×
183

184
        switch (type) {
×
185
            case "user" -> {
186
                resource = IndividualAgent.get(id);
×
187
                if (resource == null) {
×
188
                    throw new IllegalArgumentException("User not found: " + id);
×
189
                }
190
                collectUserNanopubs(collected, id);
×
191
            }
×
192
            case "space" -> {
193
                resource = SpaceRepository.get().findById(id);
×
194
                if (resource == null) {
×
195
                    throw new IllegalArgumentException("Space not found: " + id);
×
196
                }
197
                // Space-specific data is waited for inside collectSpaceNanopubs via
198
                // space.getUsers() which calls ensureInitialized() and blocks until ready.
199
                collectSpaceNanopubs(collected, (Space) resource);
×
200
            }
×
201
            case "resource" -> {
202
                resource = MaintainedResourceRepository.get().findById(id);
×
203
                if (resource == null) {
×
204
                    throw new IllegalArgumentException("Resource not found: " + id);
×
205
                }
206
                collectResourceNanopubs(collected, (MaintainedResource) resource);
×
207
            }
×
208
            case "part" -> {
209
                String contextId = parameters.get("context").toString();
×
210
                if (contextId == null) {
×
211
                    throw new IllegalArgumentException("Parameter 'context' is required for type=part");
×
212
                }
213
                resource = resolveContextResource(contextId);
×
214
                partId = id;
×
215
                partClasses = resolvePartClasses(id, contextId, resource);
×
216
                nanopubRef = resolvePartNanopubRef(id, contextId, resource);
×
217
                // Include the part definition nanopub
218
                String partNpId = resolvePartNanopubId(partId, contextId, resource);
×
219
                if (partNpId != null) {
×
220
                    fetchAndAdd(collected, partNpId);
×
221
                }
222
            }
×
223
            case "list" -> {
224
                collectListNanopubs(collected, parameters);
×
225
                return new ArrayList<>(collected.values());
×
226
            }
227
            case "np" -> {
228
                fetchAndAdd(collected, id);
×
229
                return new ArrayList<>(collected.values());
×
230
            }
231
            default -> throw new IllegalArgumentException("Unknown type: " + type);
×
232
        }
233

234
        // Collect nanopubs from view query results (but not the view display definitions themselves)
235
        collectViewQueryResults(collected, resource, partId, partClasses, nanopubRef);
×
236

237
        return new ArrayList<>(collected.values());
×
238
    }
239

240
    /**
241
     * Collects all approved introduction nanopubs for a user.
242
     */
243
    private void collectUserNanopubs(Map<String, Nanopub> collected, String id) {
244
        IRI userIri = Utils.vf.createIRI(id);
×
245
        for (IntroNanopub intro : User.getIntroNanopubs(userIri)) {
×
246
            if (collected.size() >= MAX_NANOPUBS) break;
×
247
            if (User.isApproved(intro)) {
×
248
                addNanopub(collected, intro.getNanopub());
×
249
            }
250
        }
×
251
    }
×
252

253
    /**
254
     * Collects nanopubs for a space: declaration, role definitions, user role assignments,
255
     * sub-space declarations, and maintained resource declarations.
256
     */
257
    private void collectSpaceNanopubs(Map<String, Nanopub> collected, Space space) {
258
        // Space declaration
259
        if (space.getNanopub() != null) {
×
260
            addNanopub(collected, space.getNanopub());
×
261
        }
262

263
        // Ensure space data is loaded (getUsers calls ensureInitialized which blocks until ready)
264
        List<IRI> users = space.getUsers();
×
265

266
        // Role definition nanopubs (must be accessed after ensureInitialized)
267
        for (SpaceMemberRoleRef roleRef : space.getRoles()) {
×
268
            if (collected.size() >= MAX_NANOPUBS) break;
×
269
            fetchAndAdd(collected, roleRef.getNanopubUri());
×
270
        }
×
271

272
        // User role assignment nanopubs
273
        for (IRI userId : users) {
×
274
            if (collected.size() >= MAX_NANOPUBS) break;
×
275
            for (SpaceMemberRoleRef memberRole : space.getMemberRoles(userId)) {
×
276
                if (collected.size() >= MAX_NANOPUBS) break;
×
277
                fetchAndAdd(collected, memberRole.getNanopubUri());
×
278
            }
×
279
        }
×
280

281
        // Sub-space declarations
282
        for (Space subspace : SpaceRepository.get().findSubspaces(space)) {
×
283
            if (collected.size() >= MAX_NANOPUBS) break;
×
284
            if (subspace.getNanopub() != null) {
×
285
                addNanopub(collected, subspace.getNanopub());
×
286
            }
287
        }
×
288

289
        // Maintained resource declarations
290
        MaintainedResourceRepository.get().ensureLoaded();
×
291
        for (MaintainedResource resource : MaintainedResourceRepository.get().findResourcesBySpace(space)) {
×
292
            if (collected.size() >= MAX_NANOPUBS) break;
×
293
            if (resource.getNanopub() != null) {
×
294
                addNanopub(collected, resource.getNanopub());
×
295
            }
296
        }
×
297
    }
×
298

299
    /**
300
     * Collects the declaration nanopub for a maintained resource.
301
     */
302
    private void collectResourceNanopubs(Map<String, Nanopub> collected, MaintainedResource resource) {
303
        if (resource.getNanopub() != null) {
9✔
304
            addNanopub(collected, resource.getNanopub());
15✔
305
        }
306
    }
3✔
307

308
    /**
309
     * Collects nanopubs from the filtered nanopublication list (mirrors ListPage query logic).
310
     */
311
    private void collectListNanopubs(Map<String, Nanopub> collected, PageParameters parameters) {
312
        Multimap<String, String> queryParams = ArrayListMultimap.create();
×
313

314
        String typeParam = parameters.get("filtertype").toString();
×
315
        if (typeParam != null) {
×
316
            queryParams.put("types", typeParam);
×
317
        }
318

319
        String userIdParam = parameters.get("userid").toString();
×
320
        if (userIdParam != null) {
×
321
            IRI userIri = Utils.vf.createIRI(userIdParam);
×
322
            for (String pubkey : User.getPubkeyhashes(userIri, null)) {
×
323
                queryParams.put("np_pubkeys", pubkey);
×
324
            }
×
325
        }
326

327
        String startTime = parameters.get("starttime").toString();
×
328
        if (startTime != null) {
×
329
            queryParams.put("np_starttime", startTime);
×
330
        }
331

332
        String endTime = parameters.get("endtime").toString();
×
333
        if (endTime != null) {
×
334
            queryParams.put("np_endtime", endTime);
×
335
        }
336

337
        View filteredNanopubsView = View.get("https://w3id.org/np/RAAxsnXxYLev1_STgHnb2Y-oNRE3DRERXXDoJbELHSnzA/filtered-nanopubs-view");
×
338
        QueryRef queryRef = new QueryRef(filteredNanopubsView.getQuery().getQueryId(), queryParams);
×
339

340
        ApiResponse response = retrieveResponseWithWait(queryRef);
×
341
        if (response == null) return;
×
342

343
        for (ApiResponseEntry entry : response.getData()) {
×
344
            if (collected.size() >= MAX_NANOPUBS) break;
×
345
            String npUri = entry.get("np");
×
346
            if (npUri != null) {
×
347
                fetchAndAdd(collected, npUri);
×
348
            }
349
        }
×
350
    }
×
351

352
    /**
353
     * Executes view display queries and collects nanopubs from results.
354
     * Does not include the view display definition nanopubs themselves.
355
     * Fetches view displays directly from the API to avoid depending on async state.
356
     */
357
    private void collectViewQueryResults(Map<String, Nanopub> collected, AbstractResourceWithProfile resource,
358
            String partId, Set<IRI> partClasses, String nanopubRef) {
359
        List<ViewDisplay> viewDisplays = fetchViewDisplays(resource, partId, partClasses);
×
360

361
        String targetId = partId != null ? partId : resource.getId();
×
362
        String targetNpId = nanopubRef != null ? nanopubRef : resource.getNanopubId();
×
363

364
        for (ViewDisplay vd : viewDisplays) {
×
365
            if (collected.size() >= MAX_NANOPUBS) break;
×
366

367
            // Build query parameters (mirrors ViewList logic)
368
            QueryRef queryRef = buildQueryRef(vd, resource, targetId, targetNpId);
×
369
            if (queryRef == null) continue;
×
370

371
            // Retrieve synchronously, retrying while another thread is fetching the same query
372
            try {
373
                ApiResponse response = retrieveResponseWithWait(queryRef);
×
374
                if (response == null) continue;
×
375

376
                for (ApiResponseEntry entry : response.getData()) {
×
377
                    if (collected.size() >= MAX_NANOPUBS) break;
×
378

379
                    // Single-valued "np" column
380
                    String npUri = entry.get("np");
×
381
                    if (npUri != null) {
×
382
                        fetchAndAdd(collected, npUri);
×
383
                    }
384

385
                    // Multi-valued "np_multi_iri" column (space-separated URIs)
386
                    String npMulti = entry.get("np_multi_iri");
×
387
                    if (npMulti != null) {
×
388
                        for (String uri : npMulti.split("\\s+")) {
×
389
                            if (collected.size() >= MAX_NANOPUBS) break;
×
390
                            if (!uri.isBlank()) {
×
391
                                fetchAndAdd(collected, uri);
×
392
                            }
393
                        }
394
                    }
395
                }
×
396
            } catch (Exception ex) {
×
397
                logger.error("Error executing query for view display {}: {}", vd.getId(), ex.getMessage());
×
398
            }
×
399
        }
×
400
    }
×
401

402
    /**
403
     * Builds a QueryRef for a view display, mirroring the parameter logic from ViewList.
404
     */
405
    private QueryRef buildQueryRef(ViewDisplay vd, AbstractResourceWithProfile resource, String targetId, String targetNpId) {
406
        View view = vd.getView();
×
407
        if (view == null || view.getQuery() == null) return null;
×
408

409
        Multimap<String, String> queryRefParams = ArrayListMultimap.create();
×
410
        for (String p : view.getQuery().getPlaceholdersList()) {
×
411
            String paramName = QueryParamField.getParamName(p);
×
412
            if (paramName.equals(view.getQueryField())) {
×
413
                queryRefParams.put(view.getQueryField(), targetId);
×
414
                if (QueryParamField.isMultiPlaceholder(p) && resource instanceof Space space) {
×
415
                    for (String altId : space.getAltIDs()) {
×
416
                        queryRefParams.put(view.getQueryField(), altId);
×
417
                    }
×
418
                }
419
            } else if (paramName.equals(view.getQueryField() + "Namespace") && resource.getNamespace() != null) {
×
420
                queryRefParams.put(view.getQueryField() + "Namespace", resource.getNamespace());
×
421
            } else if (paramName.equals(view.getQueryField() + "Np")) {
×
422
                if (!QueryParamField.isOptional(p) && targetNpId == null) {
×
423
                    queryRefParams.put(view.getQueryField() + "Np", "x:");
×
424
                } else {
425
                    queryRefParams.put(view.getQueryField() + "Np", targetNpId);
×
426
                }
427
            } else if (paramName.equals("user_pubkey") && QueryParamField.isMultiPlaceholder(p) && resource instanceof Space space) {
×
428
                // TODO Push this per-view pubkey filter server-side (a published
429
                // grlc query that gates by user-of-space) so nanodash doesn't
430
                // have to expand the placeholder client-side; see
431
                // Space.getUserPubkeyHashes.
432
                for (String hash : space.getUserPubkeyHashes()) {
×
433
                    queryRefParams.put("user_pubkey", hash);
×
434
                }
×
435
            } else if (paramName.equals("admin_pubkey") && QueryParamField.isMultiPlaceholder(p) && resource instanceof Space space) {
×
436
                // TODO Same as above for admin-of-space filtering.
437
                for (String hash : space.getAdminPubkeyHashes()) {
×
438
                    queryRefParams.put("admin_pubkey", hash);
×
439
                }
×
440
            } else if (!QueryParamField.isOptional(p)) {
×
441
                logger.error("Query has non-optional parameter that cannot be filled: {} {}", view.getQuery().getQueryId(), p);
×
442
                return null;
×
443
            }
444
        }
×
445
        return new QueryRef(view.getQuery().getQueryId(), queryRefParams);
×
446
    }
447

448
    /**
449
     * Writes multiple nanopubs as a JSON array of JSON-LD objects.
450
     */
451
    private void writeJsonLdArray(OutputStream output, List<Nanopub> nanopubs) throws IOException {
452
        output.write('[');
9✔
453
        output.write('\n');
9✔
454
        boolean first = true;
6✔
455
        for (Nanopub np : nanopubs) {
30✔
456
            try {
457
                if (!first) {
6✔
458
                    output.write(',');
9✔
459
                    output.write('\n');
9✔
460
                }
461
                NanopubUtils.writeToStream(np, output, RDFFormat.JSONLD);
12✔
462
                first = false;
6✔
463
            } catch (Exception ex) {
×
464
                if (ex.toString().contains("ClientAbortException") || ex.toString().contains("Broken pipe")) {
×
465
                    logger.debug("Client disconnected during JSON-LD download");
×
466
                    break;
×
467
                }
468
                logger.error("Error serializing nanopub {}: {}", np.getUri(), ex.getMessage());
×
469
            }
3✔
470
        }
3✔
471
        output.write('\n');
9✔
472
        output.write(']');
9✔
473
        output.write('\n');
9✔
474
    }
3✔
475

476
    /**
477
     * Writes only the assertion statements from all nanopubs as a single RDF document.
478
     * Collects all used IRIs first, then declares only the matching namespace prefixes.
479
     */
480
    private void writeAssertions(OutputStream output, List<Nanopub> nanopubs, RDFFormat format) throws IOException {
481
        // Collect all available namespace prefixes from default + nanopub-specific
482
        Map<String, String> allNamespaces = new LinkedHashMap<>();
12✔
483
        for (var entry : NanopubUtils.getDefaultNamespaces()) {
30✔
484
            allNamespaces.put(entry.getLeft(), entry.getRight());
27✔
485
        }
3✔
486
        for (Nanopub np : nanopubs) {
30✔
487
            if (np instanceof NanopubWithNs npNs) {
18!
488
                npNs.getNs().forEach((prefix, ns) -> {
15✔
489
                    // Skip nanopub-internal prefixes (they refer to the nanopub's own URI space)
490
                    if (!"this".equals(prefix) && !"sub".equals(prefix)) {
×
491
                        allNamespaces.put(prefix, ns);
×
492
                    }
493
                });
×
494
            }
495
        }
3✔
496

497
        // First pass: collect all IRIs used across all assertions
498
        Set<String> usedIris = new HashSet<>();
12✔
499
        for (Nanopub np : nanopubs) {
30✔
500
            for (Statement st : np.getAssertion()) {
33✔
501
                collectIris(usedIris, st);
12✔
502
            }
3✔
503
        }
3✔
504

505
        RDFWriter writer = Rio.createWriter(format, output);
12✔
506
        writer.startRDF();
6✔
507

508
        // Declare only namespaces that match at least one used IRI
509
        for (var entry : allNamespaces.entrySet()) {
33✔
510
            String ns = entry.getValue();
12✔
511
            for (String iri : usedIris) {
30✔
512
                if (iri.startsWith(ns)) {
12!
513
                    writer.handleNamespace(entry.getKey(), ns);
×
514
                    break;
×
515
                }
516
            }
3✔
517
        }
3✔
518

519
        // Second pass: write all assertion statements
520
        for (Nanopub np : nanopubs) {
30✔
521
            for (Statement st : np.getAssertion()) {
33✔
522
                writer.handleStatement(st);
9✔
523
            }
3✔
524
        }
3✔
525
        writer.endRDF();
6✔
526
    }
3✔
527

528
    private void collectIris(Set<String> iris, Statement st) {
529
        if (st.getSubject() instanceof IRI iri) iris.add(iri.stringValue());
42!
530
        iris.add(st.getPredicate().stringValue());
18✔
531
        if (st.getObject() instanceof IRI iri) iris.add(iri.stringValue());
18!
532
    }
3✔
533

534
    private void addNanopub(Map<String, Nanopub> collected, Nanopub np) {
535
        String uri = np.getUri().stringValue();
12✔
536
        if (!collected.containsKey(uri)) {
12✔
537
            collected.put(uri, np);
15✔
538
        }
539
    }
3✔
540

541
    private void fetchAndAdd(Map<String, Nanopub> collected, String npUri) {
542
        Nanopub np = Utils.getAsNanopub(npUri);
×
543
        if (np != null) {
×
544
            addNanopub(collected, np);
×
545
        }
546
    }
×
547

548
    /**
549
     * Retrieves a query response, retrying while another thread is fetching the same query.
550
     * Returns null only if the query genuinely has no cached result and no fetch is in progress.
551
     */
552
    private ApiResponse retrieveResponseWithWait(QueryRef queryRef) {
553
        int waited = 0;
6✔
554
        while (waited < 30_000) {
9!
555
            ApiResponse response = ApiCache.retrieveResponseSync(queryRef, false);
12✔
556
            if (response != null) return response;
12✔
557
            if (!ApiCache.isRunning(queryRef)) return null;
15✔
558
            try {
559
                Thread.sleep(200);
6✔
560
                waited += 200;
3✔
561
            } catch (InterruptedException ex) {
×
562
                Thread.currentThread().interrupt();
×
563
                break;
×
564
            }
3✔
565
        }
3✔
566
        return null;
×
567
    }
568

569
    /**
570
     * Fetches view displays synchronously from the API, bypassing async resource state.
571
     * Mirrors the logic of AbstractResourceWithProfile.triggerDataUpdate() and getTopLevelViewDisplays().
572
     */
573
    private List<ViewDisplay> fetchViewDisplays(AbstractResourceWithProfile resource, String partId, Set<IRI> partClasses) {
574
        // For spaces, ensure core data is loaded first (needed for isAdminPubkey check)
575
        if (resource instanceof Space space) {
×
576
            space.getUsers(); // triggers ensureInitialized
×
577
        }
578

579
        ApiResponse response = ApiCache.retrieveResponseSync(
×
580
                new QueryRef(QueryApiAccess.GET_VIEW_DISPLAYS, "resource", resource.getId()), false);
×
581
        if (response == null) {
×
582
            logger.warn("No view display response for resource {}", resource.getId());
×
583
            return Collections.emptyList();
×
584
        }
585

586
        // Build raw view display list (same logic as AbstractResourceWithProfile.triggerDataUpdate)
587
        List<ViewDisplay> allDisplays = new ArrayList<>();
×
588
        for (ApiResponseEntry r : response.getData()) {
×
589
            if (resource.getSpace() != null && !resource.getSpace().isAdminPubkey(r.get("pubkey"))) {
×
590
                continue;
×
591
            }
592
            try {
593
                allDisplays.add(ViewDisplay.get(r.get("display")));
×
594
            } catch (IllegalArgumentException ex) {
×
595
                logger.error("Couldn't generate view display object", ex);
×
596
            }
×
597
        }
×
598

599
        // Filter (same logic as AbstractResourceWithProfile.getViewDisplays)
600
        String resourceId = partId != null ? partId : resource.getId();
×
601
        boolean toplevel = (partId == null);
×
602

603
        List<ViewDisplay> filtered = new ArrayList<>();
×
604
        Set<IRI> viewKinds = new HashSet<>();
×
605
        for (ViewDisplay vd : allDisplays) {
×
606
            IRI kind = vd.getViewKindIri();
×
607
            if (kind != null) {
×
608
                if (viewKinds.contains(kind)) continue;
×
609
                viewKinds.add(kind);
×
610
            }
611
            if (vd.hasType(KPXL_TERMS.DEACTIVATED_VIEW_DISPLAY)) continue;
×
612

613
            if (!toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
614
                // skip (deprecated top-level-only display in part context)
615
            } else if (vd.appliesTo(resourceId, partClasses)) {
×
616
                filtered.add(vd);
×
617
            } else if (toplevel && vd.hasType(KPXL_TERMS.TOP_LEVEL_VIEW_DISPLAY)) {
×
618
                filtered.add(vd); // deprecated fallback
×
619
            }
620
        }
×
621
        Collections.sort(filtered);
×
622
        return filtered;
×
623
    }
624

625
    /**
626
     * Resolves the context resource for a part page (same logic as ResourcePartPage).
627
     */
628
    private AbstractResourceWithProfile resolveContextResource(String contextId) {
629
        AbstractResourceWithProfile resource = MaintainedResourceRepository.get().findById(contextId);
×
630
        if (resource != null) return resource;
×
631

632
        if (SpaceRepository.get().findById(contextId) != null) {
×
633
            return SpaceRepository.get().findById(contextId);
×
634
        }
635
        if (IndividualAgent.isUser(contextId)) {
×
636
            return IndividualAgent.get(contextId);
×
637
        }
638
        throw new IllegalArgumentException("Not a resource, space, or user: " + contextId);
×
639
    }
640

641
    /**
642
     * Resolves the classes of a part (mirrors ResourcePartPage logic).
643
     */
644
    private Set<IRI> resolvePartClasses(String partId, String contextId, AbstractResourceWithProfile resource) {
645
        Set<IRI> classes = new HashSet<>();
×
646
        String nanopubId = resolvePartNanopubId(partId, contextId, resource);
×
647
        if (nanopubId != null) {
×
648
            Nanopub nanopub = Utils.getAsNanopub(nanopubId);
×
649
            if (nanopub != null) {
×
650
                for (Statement st : nanopub.getAssertion()) {
×
651
                    if (st.getSubject().stringValue().equals(partId) && st.getPredicate().equals(RDF.TYPE) && st.getObject() instanceof IRI objIri) {
×
652
                        classes.add(objIri);
×
653
                    }
654
                }
×
655
            }
656
        }
657
        return classes;
×
658
    }
659

660
    /**
661
     * Resolves the nanopub ref for a part (used as query param), returning "x:" if not found.
662
     */
663
    private String resolvePartNanopubRef(String partId, String contextId, AbstractResourceWithProfile resource) {
664
        String npId = resolvePartNanopubId(partId, contextId, resource);
×
665
        return npId != null ? npId : "x:";
×
666
    }
667

668
    /**
669
     * Looks up the nanopub ID for a part's term definition (mirrors ResourcePartPage logic).
670
     */
671
    private String resolvePartNanopubId(String partId, String contextId, AbstractResourceWithProfile resource) {
672
        QueryRef getDefQuery = new QueryRef(QueryApiAccess.GET_TERM_DEFINITIONS, "term", partId);
×
673
        if (resource.getSpace() != null) {
×
674
            for (IRI userIri : resource.getSpace().getUsers()) {
×
675
                for (String pubkey : User.getUserData().getPubkeyHashes(userIri, true)) {
×
676
                    getDefQuery.getParams().put("pubkey", pubkey);
×
677
                }
×
678
            }
×
679
        } else {
680
            for (String pubkey : User.getUserData().getPubkeyHashes(Utils.vf.createIRI(contextId), true)) {
×
681
                getDefQuery.getParams().put("pubkey", pubkey);
×
682
            }
×
683
        }
684
        ApiResponse resp = ApiCache.retrieveResponseSync(getDefQuery, false);
×
685
        if (resp != null && !resp.getData().isEmpty()) {
×
686
            return resp.getData().iterator().next().get("np");
×
687
        }
688
        return null;
×
689
    }
690

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