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

knowledgepixels / nanodash / 27622721129

16 Jun 2026 01:55PM UTC coverage: 26.963% (+6.3%) from 20.697%
27622721129

Pull #483

github

web-flow
Merge 73a4d0fe1 into 663f14f46
Pull Request #483: Space/resource About pages, ref-aware spaces, and magic query params

1542 of 6717 branches covered (22.96%)

Branch coverage included in aggregate %.

3407 of 11638 relevant lines covered (29.27%)

4.31 hits per line

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

10.67
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.slf4j.Logger;
16
import org.slf4j.LoggerFactory;
17

18
import com.google.common.cache.Cache;
19
import com.google.common.cache.CacheBuilder;
20

21
import java.io.Serializable;
22
import java.util.*;
23
import java.util.concurrent.ConcurrentHashMap;
24
import java.util.concurrent.TimeUnit;
25

26
/**
27
 * A class representing a Resource View.
28
 */
29
public class View implements Serializable {
30

31
    private static final Logger logger = LoggerFactory.getLogger(View.class);
9✔
32
    private static final Set<IRI> supportedViewTypes = Set.of(
21✔
33
            KPXL_TERMS.TABULAR_VIEW,
34
            KPXL_TERMS.LIST_VIEW,
35
            KPXL_TERMS.PLAIN_PARAGRAPH_VIEW,
36
            KPXL_TERMS.NANOPUB_SET_VIEW,
37
            KPXL_TERMS.ITEM_LIST_VIEW
38
    );
39

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

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

57
    private static final Cache<String, View> views = CacheBuilder.newBuilder()
6✔
58
        .maximumSize(5_000)
9✔
59
        .expireAfterAccess(24, TimeUnit.HOURS)
3✔
60
        .build();
6✔
61

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

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

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

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

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

114
    /**
115
     * Get a View by its ID.
116
     *
117
     * @param id            the ID of the View
118
     * @param resolveLatest if true, follow the supersedes chain to load the latest
119
     *                      version of the view; if false, load exactly the given
120
     *                      version without a latest-version lookup. Pass false when
121
     *                      the caller already holds a latest-resolved IRI (e.g. from
122
     *                      the get-view-displays query, which now resolves it
123
     *                      server-side) to avoid a redundant network round-trip.
124
     * @return the View object
125
     */
126
    public static View get(String id, boolean resolveLatest) {
127
        String npId = id.replaceFirst("^(.*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43})[^A-Za-z0-9-_].*$", "$1");
×
128
        if (!resolveLatest) {
×
129
            return getExactVersion(id, npId);
×
130
        }
131
        Pair<Long, View> memo = latestResolvedViews.getIfPresent(id);
×
132
        if (memo != null) {
×
133
            if (System.currentTimeMillis() - memo.getLeft() > REFRESH_RESOLUTION_AFTER_MS) {
×
134
                triggerResolutionRefresh(id, npId);
×
135
            }
136
            return memo.getRight();
×
137
        }
138
        View resolved = resolveLatestVersion(id, npId);
×
139
        if (resolved != null) {
×
140
            latestResolvedViews.put(id, Pair.of(System.currentTimeMillis(), resolved));
×
141
        }
142
        return resolved;
×
143
    }
144

145
    /**
146
     * Resolves a view id to the latest version of its view definition by
147
     * following the supersedes chain, falling back to the exact given version
148
     * if the lookup fails or doesn't yield a single embedded view IRI. This is
149
     * the network-touching part of {@link #get(String)}.
150
     */
151
    private static View resolveLatestVersion(String id, String npId) {
152
        // Automatically selecting latest version of view definition:
153
        // TODO This should be made configurable at some point, so one can make it a fixed version.
154
        try {
155
            String latestNpId = QueryApiAccess.getLatestVersionId(npId);
×
156
            if (!latestNpId.equals(npId)) {
×
157
                Nanopub np = Utils.getAsNanopub(latestNpId);
×
158
                if (np != null) {
×
159
                    Set<String> embeddedIris = NanopubUtils.getEmbeddedIriIds(np);
×
160
                    if (embeddedIris.size() == 1) {
×
161
                        String latestId = embeddedIris.iterator().next();
×
162
                        View cached = views.getIfPresent(latestId);
×
163
                        if (cached == null) {
×
164
                            cached = new View(latestId, np);
×
165
                            views.put(latestId, cached);
×
166
                        }
167
                        return cached;
×
168
                    }
169
                }
170
            }
171
        } catch (Exception ex) {
×
172
            logger.error("Error resolving latest version for view: {}", id, ex);
×
173
        }
×
174
        return getExactVersion(id, npId);
×
175
    }
176

177
    /**
178
     * Loads the view exactly as given, without a latest-version lookup.
179
     */
180
    private static View getExactVersion(String id, String npId) {
181
        Nanopub np = Utils.getAsNanopub(npId);
×
182
        View cached = views.getIfPresent(id);
×
183
        if (cached == null) {
×
184
            try {
185
                cached = new View(id, np);
×
186
                views.put(id, cached);
×
187
            } catch (Exception ex) {
×
188
                logger.error("Couldn't load nanopub for resource: {}", id, ex);
×
189
            }
×
190
        }
191
        return cached;
×
192
    }
193

194
    /**
195
     * Re-resolves a stale memoized latest-version resolution in the background.
196
     * The stale value keeps being served meanwhile; on failure it is re-stamped
197
     * with the current time, so failing lookups are retried at most once per
198
     * {@link #REFRESH_RESOLUTION_AFTER_MS} rather than on every render.
199
     */
200
    private static void triggerResolutionRefresh(String id, String npId) {
201
        if (!refreshingViews.add(id)) return;
×
202
        NanodashThreadPool.submit(() -> {
×
203
            try {
204
                View resolved = resolveLatestVersion(id, npId);
×
205
                if (resolved == null) {
×
206
                    Pair<Long, View> previous = latestResolvedViews.getIfPresent(id);
×
207
                    resolved = (previous == null ? null : previous.getRight());
×
208
                }
209
                if (resolved != null) {
×
210
                    latestResolvedViews.put(id, Pair.of(System.currentTimeMillis(), resolved));
×
211
                }
212
            } finally {
213
                refreshingViews.remove(id);
×
214
            }
215
        });
×
216
    }
×
217

218
    private String id;
219
    private Nanopub nanopub;
220
    private IRI viewKind;
221
    private String label;
222
    private String title = "View";
×
223
    private GrlcQuery query;
224
    private String queryField = "resource";
×
225
    private Integer pageSize;
226
    private Integer displayWidth;
227
    private String structuralPosition;
228
    private List<IRI> viewResultActionList = new ArrayList<>();
×
229
    private List<IRI> viewEntryActionList = new ArrayList<>();
×
230
    private Set<IRI> appliesToClasses = new HashSet<>();
×
231
    private Set<IRI> appliesToNamespaces = new HashSet<>();
×
232
    private Map<IRI, Template> actionTemplateMap = new HashMap<>();
×
233
    private Map<IRI, String> actionTemplateTargetFieldMap = new HashMap<>();
×
234
    private Map<IRI, IRI> actionTemplateTypeMap = new HashMap<>();
×
235
    private Map<IRI, String> actionTemplatePartFieldMap = new HashMap<>();
×
236
    private Map<IRI, List<String>> actionTemplateQueryMappingsMap = new HashMap<>();
×
237
    private Map<IRI, String> labelMap = new HashMap<>();
×
238
    private IRI viewType;
239
    private Map<IRI, Set<IRI>> actionVisibleToMap = new HashMap<>();
×
240

241
    private View(String id, Nanopub nanopub) {
×
242
        this.id = id;
×
243
        this.nanopub = nanopub;
×
244
        List<IRI> actionList = new ArrayList<>();
×
245
        boolean viewTypeFound = false;
×
246
        for (Statement st : nanopub.getAssertion()) {
×
247
            if (st.getSubject().stringValue().equals(id)) {
×
248
                if (st.getPredicate().equals(RDF.TYPE)) {
×
249
                    if (st.getObject().equals(KPXL_TERMS.RESOURCE_VIEW)) {
×
250
                        viewTypeFound = true;
×
251
                    }
252
                    if (st.getObject() instanceof IRI objIri && supportedViewTypes.contains(objIri)) {
×
253
                        viewType = objIri;
×
254
                    }
255
                } else if (st.getPredicate().equals(DCTERMS.IS_VERSION_OF) && st.getObject() instanceof IRI objIri) {
×
256
                    viewKind = objIri;
×
257
                } else if (st.getPredicate().equals(RDFS.LABEL)) {
×
258
                    label = st.getObject().stringValue();
×
259
                } else if (st.getPredicate().equals(DCTERMS.TITLE)) {
×
260
                    title = st.getObject().stringValue();
×
261
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_QUERY)) {
×
262
                    query = GrlcQuery.get(st.getObject().stringValue());
×
263
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_QUERY_TARGET_FIELD)) {
×
264
                    queryField = st.getObject().stringValue();
×
265
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_ACTION) && st.getObject() instanceof IRI objIri) {
×
266
                    actionList.add(objIri);
×
267
                } else if (st.getPredicate().equals(KPXL_TERMS.APPLIES_TO_NAMESPACE) && st.getObject() instanceof IRI objIri) {
×
268
                    appliesToNamespaces.add(objIri);
×
269
                } else if (st.getPredicate().equals(KPXL_TERMS.APPLIES_TO_INSTANCES_OF) && st.getObject() instanceof IRI objIri) {
×
270
                    appliesToClasses.add(objIri);
×
271
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_TARGET_CLASS) && st.getObject() instanceof IRI objIri) {
×
272
                    // Deprecated
273
                    appliesToClasses.add(objIri);
×
274
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_PAGE_SIZE) && st.getObject() instanceof Literal objL) {
×
275
                    try {
276
                        pageSize = Integer.parseInt(objL.stringValue());
×
277
                    } catch (NumberFormatException ex) {
×
278
                        logger.error("Invalid page size value: {}", objL.stringValue(), ex);
×
279
                    }
×
280
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_DISPLAY_WIDTH) && st.getObject() instanceof IRI objIri) {
×
281
                    displayWidth = columnWidths.get(objIri);
×
282
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_STRUCTURAL_POSITION) && st.getObject() instanceof Literal objL) {
×
283
                    structuralPosition = objL.stringValue();
×
284
                }
285
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE)) {
×
286
                Template template = TemplateData.get().getTemplate(st.getObject().stringValue());
×
287
                actionTemplateMap.put((IRI) st.getSubject(), template);
×
288
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE_TARGET_FIELD)) {
×
289
                putUnlessVoid(actionTemplateTargetFieldMap, (IRI) st.getSubject(), st.getObject().stringValue());
×
290
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE_PART_FIELD)) {
×
291
                putUnlessVoid(actionTemplatePartFieldMap, (IRI) st.getSubject(), st.getObject().stringValue());
×
292
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE_QUERY_MAPPING)) {
×
293
                // Repeatable: an action may declare several query mappings (e.g. derive
294
                // maps both the row np and the local pubkey). "void" means "none".
295
                String mapping = st.getObject().stringValue();
×
296
                if (!"void".equals(mapping)) {
×
297
                    actionTemplateQueryMappingsMap.computeIfAbsent((IRI) st.getSubject(), k -> new ArrayList<>()).add(mapping);
×
298
                }
299
            } else if (st.getPredicate().equals(KPXL_TERMS.IS_VISIBLE_TO) && st.getObject() instanceof IRI objIri) {
×
300
                // Per-action visibility: gen:isVisibleTo on an action node restricts
301
                // that action button to viewers holding the given role tier or
302
                // specific role. See docs/role-specific-views.md.
303
                actionVisibleToMap.computeIfAbsent((IRI) st.getSubject(), k -> new HashSet<>()).add(objIri);
×
304
            } else if (st.getPredicate().equals(RDFS.LABEL)) {
×
305
                labelMap.put((IRI) st.getSubject(), st.getObject().stringValue());
×
306
            } else if (st.getPredicate().equals(RDF.TYPE)) {
×
307
                if (st.getObject().equals(KPXL_TERMS.VIEW_ACTION) || st.getObject().equals(KPXL_TERMS.VIEW_ENTRY_ACTION)) {
×
308
                    actionTemplateTypeMap.put((IRI) st.getSubject(), (IRI) st.getObject());
×
309
                }
310
            }
311
        }
×
312
        for (IRI actionIri : actionList) {
×
313
            if (actionTemplateTypeMap.containsKey(actionIri) && actionTemplateTypeMap.get(actionIri).equals(KPXL_TERMS.VIEW_ENTRY_ACTION)) {
×
314
                viewEntryActionList.add(actionIri);
×
315
            } else {
316
                viewResultActionList.add(actionIri);
×
317
            }
318
        }
×
319
        if (!viewTypeFound) throw new IllegalArgumentException("Not a proper resource view nanopub: " + id);
×
320
        if (query == null) throw new IllegalArgumentException("Query not found: " + id);
×
321
    }
×
322

323
    /**
324
     * Stores an action-field value unless it is the {@code "void"} sentinel.
325
     * View-creation templates can't leave a statement optional inside a repeated
326
     * action group, so views carry every action field, with {@code "void"} for the
327
     * not-applicable ones (its presence is what lets Nanodash repopulate the action
328
     * group when superseding a view). It is treated here as absent — so e.g. a
329
     * "void" part field never becomes a bogus {@code param_void}.
330
     */
331
    private static void putUnlessVoid(Map<IRI, String> map, IRI key, String value) {
332
        if (value != null && !value.equals("void")) {
×
333
            map.put(key, value);
×
334
        }
335
    }
×
336

337
    /**
338
     * Gets the ID of the View.
339
     *
340
     * @return the ID of the View
341
     */
342
    public String getId() {
343
        return id;
×
344
    }
345

346
    /**
347
     * Gets the Nanopub defining this View.
348
     *
349
     * @return the Nanopub defining this View
350
     */
351
    public Nanopub getNanopub() {
352
        return nanopub;
×
353
    }
354

355
    public IRI getViewKindIri() {
356
        return viewKind;
×
357
    }
358

359
    /**
360
     * Gets the label of the View.
361
     *
362
     * @return the label of the View
363
     */
364
    public String getLabel() {
365
        return label;
×
366
    }
367

368
    /**
369
     * Gets the title of the View.
370
     *
371
     * @return the title of the View
372
     */
373
    public String getTitle() {
374
        return title;
×
375
    }
376

377
    /**
378
     * Gets the GrlcQuery associated with the View.
379
     *
380
     * @return the GrlcQuery associated with the View
381
     */
382
    public GrlcQuery getQuery() {
383
        return query;
×
384
    }
385

386
    /**
387
     * Gets the query field of the View.
388
     *
389
     * @return the query field
390
     */
391
    public String getQueryField() {
392
        return queryField;
×
393
    }
394

395
    /**
396
     * Returns the preferred page size.
397
     *
398
     * @return page size (0 = everything on first page)
399
     */
400
    public Integer getPageSize() {
401
        return pageSize;
×
402
    }
403

404
    public Integer getDisplayWidth() {
405
        return displayWidth;
×
406
    }
407

408
    public String getStructuralPosition() {
409
        return structuralPosition;
×
410
    }
411

412
    /**
413
     * Gets the visibility restriction declared on a given action node via
414
     * {@code gen:isVisibleTo}: the set of role-tier or specific-role IRIs a viewer
415
     * must hold for that action button to be shown. An empty set means the action
416
     * is visible to everyone (subject to the existing button-list routing).
417
     *
418
     * @param actionIri the action IRI (a result or entry action of this view)
419
     * @return the set of {@code gen:isVisibleTo} IRIs for that action (never null)
420
     */
421
    public Set<IRI> getActionVisibleTo(IRI actionIri) {
422
        return actionVisibleToMap.getOrDefault(actionIri, Collections.emptySet());
×
423
    }
424

425
    /**
426
     * Gets the list of action IRIs associated with the View.
427
     *
428
     * @return the list of action IRIs
429
     */
430
    public List<IRI> getViewResultActionList() {
431
        return viewResultActionList;
×
432
    }
433

434
    public List<IRI> getViewEntryActionList() {
435
        return viewEntryActionList;
×
436
    }
437

438
    /**
439
     * Gets the Template for a given action IRI.
440
     *
441
     * @param actionIri the action IRI
442
     * @return the Template for the action IRI
443
     */
444
    public Template getTemplateForAction(IRI actionIri) {
445
        return actionTemplateMap.get(actionIri);
×
446
    }
447

448
    /**
449
     * Gets the template field for a given action IRI.
450
     *
451
     * @param actionIri the action IRI
452
     * @return the template field for the action IRI
453
     */
454
    public String getTemplateTargetFieldForAction(IRI actionIri) {
455
        return actionTemplateTargetFieldMap.get(actionIri);
×
456
    }
457

458
    public String getTemplatePartFieldForAction(IRI actionIri) {
459
        return actionTemplatePartFieldMap.get(actionIri);
×
460
    }
461

462
    /**
463
     * Gets the query mappings declared for an action: each is {@code "col:target"},
464
     * mapping result column {@code col} to template parameter {@code param_target}
465
     * — or, when {@code target} begins with {@code @}, to the raw URL parameter
466
     * {@code target} (without the {@code param_} prefix), used for fill-mode keys
467
     * such as {@code @derive-a} / {@code @supersede}. An entry action applies all
468
     * of these per row; see docs/magic-query-params.md.
469
     *
470
     * @param actionIri the action IRI
471
     * @return the list of mappings (never null; empty if none)
472
     */
473
    public List<String> getTemplateQueryMappings(IRI actionIri) {
474
        List<String> result = new ArrayList<>();
×
475
        for (String literal : actionTemplateQueryMappingsMap.getOrDefault(actionIri, Collections.emptyList())) {
×
476
            result.addAll(parseMappingLiteral(literal));
×
477
        }
×
478
        return result;
×
479
    }
480

481
    /**
482
     * Splits a query-mapping literal into individual {@code "col:target"} mappings
483
     * on whitespace. Multiple mappings share a single literal because a
484
     * view-creation template cannot repeat a statement inside its repeated action
485
     * group — e.g. {@code "np:nanopubToBeRetracted"} or
486
     * {@code "derive_target:@derive-a local_pubkey:public-key__.1"}.
487
     *
488
     * @param literal the mapping literal (may be null/blank/"void")
489
     * @return the individual mappings (never null; empty if none)
490
     */
491
    public static List<String> parseMappingLiteral(String literal) {
492
        List<String> mappings = new ArrayList<>();
12✔
493
        if (literal == null || literal.isBlank()) return mappings;
21✔
494
        for (String m : literal.trim().split("\\s+")) {
57✔
495
            if (!m.isEmpty() && !"void".equals(m)) mappings.add(m);
33!
496
        }
497
        return mappings;
6✔
498
    }
499

500
    /**
501
     * Gets the set of query result columns that serve only as <em>sources</em> for
502
     * this view's action query mappings (the {@code col} part of each
503
     * {@code "col:target"} mapping, across all actions). These columns carry
504
     * action data — conditional targets, the local-key bundle — not row content, so
505
     * the result builders skip them when rendering visible columns. A column that
506
     * happens to be both a display column and a mapping source would also be hidden;
507
     * map a duplicated/aliased column instead if you need to show one.
508
     *
509
     * @return the set of mapping-source column names (never null)
510
     */
511
    public Set<String> getActionMappingSourceColumns() {
512
        Set<String> columns = new HashSet<>();
×
513
        for (IRI actionIri : actionTemplateQueryMappingsMap.keySet()) {
×
514
            for (String mapping : getTemplateQueryMappings(actionIri)) {
×
515
                int idx = mapping.indexOf(':');
×
516
                if (idx > 0) columns.add(mapping.substring(0, idx));
×
517
            }
×
518
        }
×
519
        return columns;
×
520
    }
521

522
    /**
523
     * Gets the first query mapping for an action, or null. Kept for result-action
524
     * callers that pass a single {@code values-from-query-mapping}.
525
     *
526
     * @param actionIri the action IRI
527
     * @return the first mapping, or null
528
     */
529
    public String getTemplateQueryMapping(IRI actionIri) {
530
        List<String> mappings = actionTemplateQueryMappingsMap.get(actionIri);
×
531
        return (mappings == null || mappings.isEmpty()) ? null : mappings.get(0);
×
532
    }
533

534
    /**
535
     * Gets the label for a given action IRI.
536
     *
537
     * @param actionIri the action IRI
538
     * @return the label for the action IRI
539
     */
540
    public String getLabelForAction(IRI actionIri) {
541
        return labelMap.get(actionIri);
×
542
    }
543

544
    public boolean appliesTo(String resourceId, Set<IRI> classes) {
545
        for (IRI namespace : appliesToNamespaces) {
×
546
            if (resourceId.startsWith(namespace.stringValue())) return true;
×
547
        }
×
548
        if (classes != null) {
×
549
            for (IRI c : classes) {
×
550
                if (appliesToClasses.contains(c)) return true;
×
551
            }
×
552
        }
553
        return false;
×
554
    }
555

556
    /**
557
     * Checks if the View has target classes.
558
     *
559
     * @return true if the View has target classes, false otherwise
560
     */
561
    public boolean appliesToClasses() {
562
        return !appliesToClasses.isEmpty();
×
563
    }
564

565
    /**
566
     * Checks if the View has a specific target class.
567
     *
568
     * @param targetClass the target class IRI
569
     * @return true if the View has the target class, false otherwise
570
     */
571
    public boolean appliesToClass(IRI targetClass) {
572
        return appliesToClasses.contains(targetClass);
×
573
    }
574

575
    @Override
576
    public String toString() {
577
        return id;
×
578
    }
579

580
    /**
581
     * Gets the view type of the View.
582
     *
583
     * @return the view type mode IRI
584
     */
585
    public IRI getViewType() {
586
        return viewType;
×
587
    }
588

589
    /**
590
     * Get the supported view types.
591
     *
592
     * @return a set of supported view type IRIs
593
     */
594
    public static Set<IRI> getSupportedViewTypes() {
595
        return supportedViewTypes;
×
596
    }
597

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