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

knowledgepixels / nanodash / 30449359054

29 Jul 2026 11:53AM UTC coverage: 34.201% (-0.2%) from 34.372%
30449359054

push

github

web-flow
Merge pull request #573 from knowledgepixels/572-header-views

feat(views): add header views (gen:HeaderView)

2460 of 7964 branches covered (30.89%)

Branch coverage included in aggregate %.

4802 of 13269 relevant lines covered (36.19%)

5.59 hits per line

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

9.07
src/main/java/com/knowledgepixels/nanodash/View.java
1
package com.knowledgepixels.nanodash;
2

3
import com.knowledgepixels.nanodash.template.Template;
4
import com.knowledgepixels.nanodash.template.TemplateData;
5
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
6
import org.apache.commons.lang3.tuple.Pair;
7
import org.eclipse.rdf4j.model.IRI;
8
import org.eclipse.rdf4j.model.Literal;
9
import org.eclipse.rdf4j.model.Statement;
10
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
11
import org.eclipse.rdf4j.model.vocabulary.RDF;
12
import org.eclipse.rdf4j.model.vocabulary.RDFS;
13
import org.nanopub.Nanopub;
14
import org.nanopub.NanopubUtils;
15
import org.nanopub.extra.services.ApiResponse;
16
import org.nanopub.extra.services.QueryRef;
17
import org.slf4j.Logger;
18
import org.slf4j.LoggerFactory;
19

20
import com.google.common.cache.Cache;
21
import com.google.common.cache.CacheBuilder;
22
import com.google.common.collect.ArrayListMultimap;
23
import com.google.common.collect.Multimap;
24

25
import java.io.Serializable;
26
import java.util.*;
27
import java.util.concurrent.ConcurrentHashMap;
28
import java.util.concurrent.TimeUnit;
29

30
/**
31
 * A class representing a Resource View.
32
 */
33
public class View implements Serializable {
34

35
    private static final Logger logger = LoggerFactory.getLogger(View.class);
9✔
36
    private static final Set<IRI> supportedViewTypes = Set.of(
24✔
37
            KPXL_TERMS.TABULAR_VIEW,
38
            KPXL_TERMS.LIST_VIEW,
39
            KPXL_TERMS.PLAIN_PARAGRAPH_VIEW,
40
            KPXL_TERMS.NANOPUB_SET_VIEW,
41
            KPXL_TERMS.ITEM_LIST_VIEW,
42
            KPXL_TERMS.HEADER_VIEW
43
    );
44

45
    static Map<IRI, Integer> columnWidths = new HashMap<>();
12✔
46

47
    static {
48
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_1_OF_12, 1);
18✔
49
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_2_OF_12, 2);
18✔
50
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_3_OF_12, 3);
18✔
51
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_4_OF_12, 4);
18✔
52
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_5_OF_12, 5);
18✔
53
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_6_OF_12, 6);
18✔
54
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_7_OF_12, 7);
18✔
55
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_8_OF_12, 8);
18✔
56
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_9_OF_12, 9);
18✔
57
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_10_OF_12, 10);
18✔
58
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_11_OF_12, 11);
18✔
59
        columnWidths.put(KPXL_TERMS.COLUMN_WIDTH_12_OF_12, 12);
18✔
60
    }
61

62
    private static final Cache<String, View> views = CacheBuilder.newBuilder()
6✔
63
        .maximumSize(5_000)
9✔
64
        .expireAfterAccess(24, TimeUnit.HOURS)
3✔
65
        .build();
6✔
66

67
    /**
68
     * Memo of latest-version resolutions: view id (as passed to {@link #get(String)})
69
     * to the resolution time and the View it resolved to. Entries are served as
70
     * long as they live; one older than {@link #REFRESH_RESOLUTION_AFTER_MS} is
71
     * served stale while a background re-resolution runs (stale-while-revalidate,
72
     * like {@link ApiCache}), so a superseding view nanopub is picked up on a
73
     * later render without {@link #get(String)} ever blocking on the network
74
     * once an entry exists.
75
     */
76
    private static final Cache<String, Pair<Long, View>> latestResolvedViews = CacheBuilder.newBuilder()
6✔
77
        .maximumSize(5_000)
9✔
78
        .expireAfterAccess(24, TimeUnit.HOURS)
3✔
79
        .build();
6✔
80

81
    /**
82
     * Age after which a memoized latest-version resolution is re-resolved in the
83
     * background; mirrors the freshness window of
84
     * {@link QueryApiAccess#getLatestVersionId(String)}.
85
     */
86
    private static final long REFRESH_RESOLUTION_AFTER_MS = 1000 * 60;
87

88
    /**
89
     * Ids whose latest-version resolution is currently being refreshed in the
90
     * background, so concurrent renders don't pile up duplicate refreshes.
91
     */
92
    private static final Set<String> refreshingViews = ConcurrentHashMap.newKeySet();
9✔
93

94
    /**
95
     * Indicates whether {@link #get(String)} would currently return without
96
     * network access, i.e. a latest-version resolution is memoized for this id
97
     * (possibly stale, in which case get() serves it while refreshing in the
98
     * background). Used to decide between constructing a view-based panel
99
     * directly and deferring it to a lazy-loading AJAX request.
100
     *
101
     * @param id the ID of the View
102
     * @return true if {@link #get(String)} currently returns without blocking
103
     */
104
    public static boolean isCached(String id) {
105
        return latestResolvedViews.getIfPresent(id) != null;
×
106
    }
107

108
    /**
109
     * Get a View by its ID, resolving to the latest version (following the
110
     * supersedes chain).
111
     *
112
     * @param id the ID of the View
113
     * @return the View object
114
     */
115
    public static View get(String id) {
116
        return get(id, true);
×
117
    }
118

119
    /**
120
     * Get a View by its ID.
121
     *
122
     * @param id            the ID of the View
123
     * @param resolveLatest if true, follow the supersedes chain to load the latest
124
     *                      version of the view; if false, load exactly the given
125
     *                      version without a latest-version lookup. Pass false when
126
     *                      the caller already holds a latest-resolved IRI (e.g. from
127
     *                      the get-view-displays query, which now resolves it
128
     *                      server-side) to avoid a redundant network round-trip.
129
     *                      A version declaring {@code gen:governedBy} still gets the
130
     *                      space-based resolution here even with false: its float is
131
     *                      not supersedes-based, so the caller's server-side
132
     *                      resolution doesn't cover it.
133
     * @return the View object
134
     */
135
    public static View get(String id, boolean resolveLatest) {
136
        String npId = id.replaceFirst("^(.*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43})[^A-Za-z0-9-_].*$", "$1");
×
137
        if (!resolveLatest) {
×
138
            View exact = getExactVersion(id, npId);
×
139
            if (exact == null || exact.getGoverningSpace() == null || exact.getViewKindIri() == null) {
×
140
                return exact;
×
141
            }
142
            // fall through to the memoized latest path, which resolves a governed
143
            // version space-based (never supersedes-based) for this pin
144
        }
145
        Pair<Long, View> memo = latestResolvedViews.getIfPresent(id);
×
146
        if (memo != null) {
×
147
            if (System.currentTimeMillis() - memo.getLeft() > REFRESH_RESOLUTION_AFTER_MS) {
×
148
                triggerResolutionRefresh(id, npId);
×
149
            }
150
            return memo.getRight();
×
151
        }
152
        View resolved = resolveLatestVersion(id, npId);
×
153
        if (resolved != null) {
×
154
            latestResolvedViews.put(id, Pair.of(System.currentTimeMillis(), resolved));
×
155
        }
156
        return resolved;
×
157
    }
158

159
    /**
160
     * Resolves a view id to the latest version of its view definition, falling
161
     * back to the exact given version if the lookup fails or doesn't yield a
162
     * single embedded view IRI. This is the network-touching part of
163
     * {@link #get(String)}. A version that declares {@code gen:governedBy}
164
     * resolves space-based (authority-scoped latest-wins within its
165
     * {@code (kind, space)} pair); one that doesn't follows the supersedes
166
     * chain as before. See docs/views-and-presets-as-maintained-resources.md.
167
     */
168
    private static View resolveLatestVersion(String id, String npId) {
169
        View pinned = getExactVersion(id, npId);
×
170
        if (pinned != null && pinned.getGoverningSpace() != null && pinned.getViewKindIri() != null) {
×
171
            return resolveGovernedVersion(pinned);
×
172
        }
173
        // Automatically selecting latest version of view definition:
174
        // TODO This should be made configurable at some point, so one can make it a fixed version.
175
        try {
176
            String latestNpId = QueryApiAccess.getLatestVersionId(npId);
×
177
            if (!latestNpId.equals(npId)) {
×
178
                Nanopub np = Utils.getAsNanopub(latestNpId);
×
179
                if (np != null) {
×
180
                    Set<String> embeddedIris = NanopubUtils.getEmbeddedIriIds(np);
×
181
                    if (embeddedIris.size() == 1) {
×
182
                        String latestId = embeddedIris.iterator().next();
×
183
                        View cached = views.getIfPresent(latestId);
×
184
                        if (cached == null) {
×
185
                            cached = new View(latestId, np);
×
186
                            views.put(latestId, cached);
×
187
                        }
188
                        return cached;
×
189
                    }
190
                }
191
            }
192
        } catch (Exception ex) {
×
193
            logger.error("Error resolving latest version for view: {}", id, ex);
×
194
        }
×
195
        return pinned;
×
196
    }
197

198
    /**
199
     * Resolves the latest space-governed version of the pinned view's
200
     * {@code (kind, space)} pair: the newest version declaring the same kind and
201
     * governing space, signed by a current member+ of that space, with the kind
202
     * validated as maintained by the space — all checked server-side by the
203
     * {@link QueryApiAccess#GET_LATEST_GOVERNED_VERSION} query. The pin is the
204
     * floor: on an empty result (or any failure) the pinned version stands,
205
     * un-revalidated.
206
     */
207
    private static View resolveGovernedVersion(View pinned) {
208
        try {
209
            Multimap<String, String> params = ArrayListMultimap.create();
×
210
            params.put("kind", pinned.getViewKindIri().stringValue());
×
211
            params.put("space", pinned.getGoverningSpace().stringValue());
×
212
            ApiResponse resp = ApiCache.retrieveResponseSync(new QueryRef(QueryApiAccess.GET_LATEST_GOVERNED_VERSION, params), false);
×
213
            if (resp != null && !resp.getData().isEmpty()) {
×
214
                String latestId = resp.getData().get(0).get("version");
×
215
                if (latestId != null && !latestId.isEmpty() && !latestId.equals(pinned.getId())) {
×
216
                    String latestNpId = latestId.replaceFirst("^(.*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43})[^A-Za-z0-9-_].*$", "$1");
×
217
                    View resolved = getExactVersion(latestId, latestNpId);
×
218
                    if (resolved != null) return resolved;
×
219
                }
220
            }
221
        } catch (Exception ex) {
×
222
            logger.error("Error resolving governed version for view: {}", pinned.getId(), ex);
×
223
        }
×
224
        return pinned;
×
225
    }
226

227
    /**
228
     * Loads the view exactly as given, without a latest-version lookup.
229
     */
230
    private static View getExactVersion(String id, String npId) {
231
        Nanopub np = Utils.getAsNanopub(npId);
×
232
        View cached = views.getIfPresent(id);
×
233
        if (cached == null) {
×
234
            try {
235
                cached = new View(id, np);
×
236
                views.put(id, cached);
×
237
            } catch (Exception ex) {
×
238
                logger.error("Couldn't load nanopub for resource: {}", id, ex);
×
239
            }
×
240
        }
241
        return cached;
×
242
    }
243

244
    /**
245
     * Re-resolves a stale memoized latest-version resolution in the background.
246
     * The stale value keeps being served meanwhile; on failure it is re-stamped
247
     * with the current time, so failing lookups are retried at most once per
248
     * {@link #REFRESH_RESOLUTION_AFTER_MS} rather than on every render.
249
     */
250
    private static void triggerResolutionRefresh(String id, String npId) {
251
        if (!refreshingViews.add(id)) return;
×
252
        NanodashThreadPool.submit(() -> {
×
253
            try {
254
                View resolved = resolveLatestVersion(id, npId);
×
255
                if (resolved == null) {
×
256
                    Pair<Long, View> previous = latestResolvedViews.getIfPresent(id);
×
257
                    resolved = (previous == null ? null : previous.getRight());
×
258
                }
259
                if (resolved != null) {
×
260
                    latestResolvedViews.put(id, Pair.of(System.currentTimeMillis(), resolved));
×
261
                }
262
            } finally {
263
                refreshingViews.remove(id);
×
264
            }
265
        });
×
266
    }
×
267

268
    private String id;
269
    private Nanopub nanopub;
270
    private IRI viewKind;
271
    private IRI governingSpace;
272
    private String label;
273
    private String title = "View";
×
274
    private String description;
275
    private GrlcQuery query;
276
    private String queryField = "resource";
×
277
    private Integer pageSize;
278
    private Integer displayWidth;
279
    private String structuralPosition;
280
    private List<IRI> viewResultActionList = new ArrayList<>();
×
281
    private List<IRI> viewEntryActionList = new ArrayList<>();
×
282
    private Set<IRI> appliesToClasses = new HashSet<>();
×
283
    private Set<IRI> appliesToNamespaces = new HashSet<>();
×
284
    private Map<IRI, Template> actionTemplateMap = new HashMap<>();
×
285
    private Map<IRI, String> actionTemplateTargetFieldMap = new HashMap<>();
×
286
    private Map<IRI, IRI> actionTemplateTypeMap = new HashMap<>();
×
287
    private Map<IRI, String> actionTemplatePartFieldMap = new HashMap<>();
×
288
    private Map<IRI, List<String>> actionTemplateQueryMappingsMap = new HashMap<>();
×
289
    private Map<IRI, String> labelMap = new HashMap<>();
×
290
    private IRI viewType;
291
    private boolean queryForm = false;
×
292
    private Map<IRI, Set<IRI>> actionVisibleToMap = new HashMap<>();
×
293

294
    private View(String id, Nanopub nanopub) {
×
295
        this.id = id;
×
296
        this.nanopub = nanopub;
×
297
        List<IRI> actionList = new ArrayList<>();
×
298
        boolean viewTypeFound = false;
×
299
        for (Statement st : nanopub.getAssertion()) {
×
300
            if (st.getSubject().stringValue().equals(id)) {
×
301
                if (st.getPredicate().equals(RDF.TYPE)) {
×
302
                    if (st.getObject().equals(KPXL_TERMS.RESOURCE_VIEW)) {
×
303
                        viewTypeFound = true;
×
304
                    }
305
                    if (st.getObject() instanceof IRI objIri && supportedViewTypes.contains(objIri)) {
×
306
                        viewType = objIri;
×
307
                    }
308
                    if (st.getObject().equals(KPXL_TERMS.QUERY_FORM_VIEW)) {
×
309
                        queryForm = true;
×
310
                    }
311
                } else if (st.getPredicate().equals(DCTERMS.IS_VERSION_OF) && st.getObject() instanceof IRI objIri) {
×
312
                    viewKind = objIri;
×
313
                } else if (st.getPredicate().equals(KPXL_TERMS.GOVERNED_BY) && st.getObject() instanceof IRI objIri) {
×
314
                    governingSpace = objIri;
×
315
                } else if (st.getPredicate().equals(RDFS.LABEL)) {
×
316
                    label = st.getObject().stringValue();
×
317
                } else if (st.getPredicate().equals(DCTERMS.TITLE)) {
×
318
                    title = st.getObject().stringValue();
×
319
                } else if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
×
320
                    description = st.getObject().stringValue();
×
321
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_QUERY)) {
×
322
                    query = GrlcQuery.get(st.getObject().stringValue());
×
323
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_QUERY_TARGET_FIELD)) {
×
324
                    queryField = st.getObject().stringValue();
×
325
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_ACTION) && st.getObject() instanceof IRI objIri) {
×
326
                    actionList.add(objIri);
×
327
                } else if (st.getPredicate().equals(KPXL_TERMS.APPLIES_TO_NAMESPACE) && st.getObject() instanceof IRI objIri) {
×
328
                    appliesToNamespaces.add(objIri);
×
329
                } else if (st.getPredicate().equals(KPXL_TERMS.APPLIES_TO_INSTANCES_OF) && st.getObject() instanceof IRI objIri) {
×
330
                    appliesToClasses.add(objIri);
×
331
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_TARGET_CLASS) && st.getObject() instanceof IRI objIri) {
×
332
                    // Deprecated
333
                    appliesToClasses.add(objIri);
×
334
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_PAGE_SIZE) && st.getObject() instanceof Literal objL) {
×
335
                    try {
336
                        pageSize = Integer.parseInt(objL.stringValue());
×
337
                    } catch (NumberFormatException ex) {
×
338
                        logger.error("Invalid page size value: {}", objL.stringValue(), ex);
×
339
                    }
×
340
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_DISPLAY_WIDTH) && st.getObject() instanceof IRI objIri) {
×
341
                    displayWidth = columnWidths.get(objIri);
×
342
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_STRUCTURAL_POSITION) && st.getObject() instanceof Literal objL) {
×
343
                    structuralPosition = objL.stringValue();
×
344
                }
345
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE)) {
×
346
                Template template = TemplateData.get().getTemplate(st.getObject().stringValue());
×
347
                actionTemplateMap.put((IRI) st.getSubject(), template);
×
348
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE_TARGET_FIELD)) {
×
349
                putUnlessVoid(actionTemplateTargetFieldMap, (IRI) st.getSubject(), st.getObject().stringValue());
×
350
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE_PART_FIELD)) {
×
351
                putUnlessVoid(actionTemplatePartFieldMap, (IRI) st.getSubject(), st.getObject().stringValue());
×
352
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE_QUERY_MAPPING)) {
×
353
                // Repeatable: an action may declare several query mappings (e.g. derive
354
                // maps both the row np and the local pubkey). "void" means "none".
355
                String mapping = st.getObject().stringValue();
×
356
                if (!"void".equals(mapping)) {
×
357
                    actionTemplateQueryMappingsMap.computeIfAbsent((IRI) st.getSubject(), k -> new ArrayList<>()).add(mapping);
×
358
                }
359
            } else if (st.getPredicate().equals(KPXL_TERMS.IS_VISIBLE_TO) && st.getObject() instanceof IRI objIri) {
×
360
                // Per-action visibility: gen:isVisibleTo on an action node restricts
361
                // that action button to viewers holding the given role tier or
362
                // specific role. See docs/role-specific-views.md.
363
                actionVisibleToMap.computeIfAbsent((IRI) st.getSubject(), k -> new HashSet<>()).add(objIri);
×
364
            } else if (st.getPredicate().equals(RDFS.LABEL)) {
×
365
                labelMap.put((IRI) st.getSubject(), st.getObject().stringValue());
×
366
            } else if (st.getPredicate().equals(RDF.TYPE)) {
×
367
                if (st.getObject().equals(KPXL_TERMS.VIEW_ACTION) || st.getObject().equals(KPXL_TERMS.VIEW_ENTRY_ACTION)) {
×
368
                    actionTemplateTypeMap.put((IRI) st.getSubject(), (IRI) st.getObject());
×
369
                }
370
            }
371
        }
×
372
        for (IRI actionIri : actionList) {
×
373
            if (actionTemplateTypeMap.containsKey(actionIri) && actionTemplateTypeMap.get(actionIri).equals(KPXL_TERMS.VIEW_ENTRY_ACTION)) {
×
374
                viewEntryActionList.add(actionIri);
×
375
            } else {
376
                viewResultActionList.add(actionIri);
×
377
            }
378
        }
×
379
        if (!viewTypeFound) throw new IllegalArgumentException("Not a proper resource view nanopub: " + id);
×
380
        // Header views are the one display type without a query (issue #572).
381
        if (query == null && !KPXL_TERMS.HEADER_VIEW.equals(viewType)) throw new IllegalArgumentException("Query not found: " + id);
×
382
    }
×
383

384
    /**
385
     * Stores an action-field value unless it is the {@code "void"} sentinel.
386
     * View-creation templates can't leave a statement optional inside a repeated
387
     * action group, so views carry every action field, with {@code "void"} for the
388
     * not-applicable ones (its presence is what lets Nanodash repopulate the action
389
     * group when superseding a view). It is treated here as absent — so e.g. a
390
     * "void" part field never becomes a bogus {@code param_void}.
391
     */
392
    private static void putUnlessVoid(Map<IRI, String> map, IRI key, String value) {
393
        if (value != null && !value.equals("void")) {
×
394
            map.put(key, value);
×
395
        }
396
    }
×
397

398
    /**
399
     * Gets the ID of the View.
400
     *
401
     * @return the ID of the View
402
     */
403
    public String getId() {
404
        return id;
×
405
    }
406

407
    /**
408
     * Gets the Nanopub defining this View.
409
     *
410
     * @return the Nanopub defining this View
411
     */
412
    public Nanopub getNanopub() {
413
        return nanopub;
×
414
    }
415

416
    public IRI getViewKindIri() {
417
        return viewKind;
×
418
    }
419

420
    /**
421
     * Gets the space governing this view version's {@code (kind, space)} pair via
422
     * {@code gen:governedBy}, or null if the version doesn't opt into space
423
     * governance (in which case latest-version resolution stays supersedes-based).
424
     *
425
     * @return the governing space IRI, or null
426
     */
427
    public IRI getGoverningSpace() {
428
        return governingSpace;
×
429
    }
430

431
    /**
432
     * Gets the label of the View.
433
     *
434
     * @return the label of the View
435
     */
436
    public String getLabel() {
437
        return label;
×
438
    }
439

440
    /**
441
     * Gets the title of the View.
442
     *
443
     * @return the title of the View
444
     */
445
    public String getTitle() {
446
        return title;
×
447
    }
448

449
    /**
450
     * Gets the description of the View ({@code dct:description}), shown below the
451
     * title for header views.
452
     *
453
     * @return the description, or null if none is declared
454
     */
455
    public String getDescription() {
456
        return description;
×
457
    }
458

459
    /**
460
     * Gets the GrlcQuery associated with the View.
461
     *
462
     * @return the GrlcQuery associated with the View, or null for a header view
463
     */
464
    public GrlcQuery getQuery() {
465
        return query;
×
466
    }
467

468
    /**
469
     * Gets the query field of the View.
470
     *
471
     * @return the query field
472
     */
473
    public String getQueryField() {
474
        return queryField;
×
475
    }
476

477
    /**
478
     * Returns the preferred page size.
479
     *
480
     * @return page size (0 = everything on first page)
481
     */
482
    public Integer getPageSize() {
483
        return pageSize;
×
484
    }
485

486
    public Integer getDisplayWidth() {
487
        return displayWidth;
×
488
    }
489

490
    public String getStructuralPosition() {
491
        return structuralPosition;
×
492
    }
493

494
    /**
495
     * Gets the visibility restriction declared on a given action node via
496
     * {@code gen:isVisibleTo}: the set of role-tier or specific-role IRIs a viewer
497
     * must hold for that action button to be shown. An empty set means the action
498
     * is visible to everyone (subject to the existing button-list routing).
499
     *
500
     * @param actionIri the action IRI (a result or entry action of this view)
501
     * @return the set of {@code gen:isVisibleTo} IRIs for that action (never null)
502
     */
503
    public Set<IRI> getActionVisibleTo(IRI actionIri) {
504
        return actionVisibleToMap.getOrDefault(actionIri, Collections.emptySet());
×
505
    }
506

507
    /**
508
     * Gets the list of action IRIs associated with the View.
509
     *
510
     * @return the list of action IRIs
511
     */
512
    public List<IRI> getViewResultActionList() {
513
        return viewResultActionList;
×
514
    }
515

516
    public List<IRI> getViewEntryActionList() {
517
        return viewEntryActionList;
×
518
    }
519

520
    /**
521
     * Gets the Template for a given action IRI.
522
     *
523
     * @param actionIri the action IRI
524
     * @return the Template for the action IRI
525
     */
526
    public Template getTemplateForAction(IRI actionIri) {
527
        return actionTemplateMap.get(actionIri);
×
528
    }
529

530
    /**
531
     * Gets the template field for a given action IRI.
532
     *
533
     * @param actionIri the action IRI
534
     * @return the template field for the action IRI
535
     */
536
    public String getTemplateTargetFieldForAction(IRI actionIri) {
537
        return actionTemplateTargetFieldMap.get(actionIri);
×
538
    }
539

540
    public String getTemplatePartFieldForAction(IRI actionIri) {
541
        return actionTemplatePartFieldMap.get(actionIri);
×
542
    }
543

544
    /**
545
     * Gets the query mappings declared for an action: each is {@code "col:target"},
546
     * mapping result column {@code col} to template parameter {@code param_target}
547
     * — or, when {@code target} begins with {@code @}, to the raw URL parameter
548
     * {@code target} (without the {@code param_} prefix), used for fill-mode keys
549
     * such as {@code @derive-a} / {@code @supersede}. An entry action applies all
550
     * of these per row; see docs/magic-query-params.md.
551
     *
552
     * @param actionIri the action IRI
553
     * @return the list of mappings (never null; empty if none)
554
     */
555
    public List<String> getTemplateQueryMappings(IRI actionIri) {
556
        List<String> result = new ArrayList<>();
×
557
        for (String literal : actionTemplateQueryMappingsMap.getOrDefault(actionIri, Collections.emptyList())) {
×
558
            result.addAll(parseMappingLiteral(literal));
×
559
        }
×
560
        return result;
×
561
    }
562

563
    /**
564
     * Splits a query-mapping literal into individual {@code "col:target"} mappings
565
     * on whitespace. Multiple mappings share a single literal because a
566
     * view-creation template cannot repeat a statement inside its repeated action
567
     * group — e.g. {@code "np:nanopubToBeRetracted"} or
568
     * {@code "derive_target:@derive-a local_pubkey:public-key__.1"}.
569
     *
570
     * @param literal the mapping literal (may be null/blank/"void")
571
     * @return the individual mappings (never null; empty if none)
572
     */
573
    public static List<String> parseMappingLiteral(String literal) {
574
        List<String> mappings = new ArrayList<>();
12✔
575
        if (literal == null || literal.isBlank()) return mappings;
21✔
576
        for (String m : literal.trim().split("\\s+")) {
57✔
577
            if (!m.isEmpty() && !"void".equals(m)) mappings.add(m);
33!
578
        }
579
        return mappings;
6✔
580
    }
581

582
    /**
583
     * Gets the set of query result columns that serve only as <em>sources</em> for
584
     * this view's action query mappings (the {@code col} part of each
585
     * {@code "col:target"} mapping, across all actions). These columns carry
586
     * action data — conditional targets, the local-key bundle — not row content, so
587
     * the result builders skip them when rendering visible columns. A column that
588
     * happens to be both a display column and a mapping source would also be hidden;
589
     * map a duplicated/aliased column instead if you need to show one.
590
     *
591
     * @return the set of mapping-source column names (never null)
592
     */
593
    public Set<String> getActionMappingSourceColumns() {
594
        Set<String> columns = new HashSet<>();
×
595
        for (IRI actionIri : actionTemplateQueryMappingsMap.keySet()) {
×
596
            for (String mapping : getTemplateQueryMappings(actionIri)) {
×
597
                int idx = mapping.indexOf(':');
×
598
                if (idx > 0) columns.add(mapping.substring(0, idx));
×
599
            }
×
600
        }
×
601
        return columns;
×
602
    }
603

604
    /**
605
     * Gets the first query mapping for an action, or null. Kept for result-action
606
     * callers that pass a single {@code values-from-query-mapping}.
607
     *
608
     * @param actionIri the action IRI
609
     * @return the first mapping, or null
610
     */
611
    public String getTemplateQueryMapping(IRI actionIri) {
612
        List<String> mappings = actionTemplateQueryMappingsMap.get(actionIri);
×
613
        return (mappings == null || mappings.isEmpty()) ? null : mappings.get(0);
×
614
    }
615

616
    /**
617
     * Gets the label for a given action IRI.
618
     *
619
     * @param actionIri the action IRI
620
     * @return the label for the action IRI
621
     */
622
    public String getLabelForAction(IRI actionIri) {
623
        return labelMap.get(actionIri);
×
624
    }
625

626
    public boolean appliesTo(String resourceId, Set<IRI> classes) {
627
        for (IRI namespace : appliesToNamespaces) {
×
628
            if (resourceId.startsWith(namespace.stringValue())) return true;
×
629
        }
×
630
        if (classes != null) {
×
631
            for (IRI c : classes) {
×
632
                if (appliesToClasses.contains(c)) return true;
×
633
            }
×
634
        }
635
        return false;
×
636
    }
637

638
    /**
639
     * Checks if the View has target classes.
640
     *
641
     * @return true if the View has target classes, false otherwise
642
     */
643
    public boolean appliesToClasses() {
644
        return !appliesToClasses.isEmpty();
×
645
    }
646

647
    /**
648
     * Checks if the View has a specific target class.
649
     *
650
     * @param targetClass the target class IRI
651
     * @return true if the View has the target class, false otherwise
652
     */
653
    public boolean appliesToClass(IRI targetClass) {
654
        return appliesToClasses.contains(targetClass);
×
655
    }
656

657
    @Override
658
    public String toString() {
659
        return id;
×
660
    }
661

662
    /**
663
     * Gets the view type of the View.
664
     *
665
     * @return the view type mode IRI
666
     */
667
    public IRI getViewType() {
668
        return viewType;
×
669
    }
670

671
    /**
672
     * Whether this view is additionally typed {@code gen:QueryFormView}: on a resource
673
     * page it renders as a form for the query placeholders not auto-filled from the
674
     * page context, whose submission leads to the full results page. Orthogonal to
675
     * {@link #getViewType()}, which then determines how those results render.
676
     *
677
     * @return true if this is a query-form view
678
     */
679
    public boolean hasQueryForm() {
680
        return queryForm;
×
681
    }
682

683
    /**
684
     * Get the supported view types.
685
     *
686
     * @return a set of supported view type IRIs
687
     */
688
    public static Set<IRI> getSupportedViewTypes() {
689
        return supportedViewTypes;
×
690
    }
691

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