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

knowledgepixels / nanodash / 27145358627

08 Jun 2026 02:39PM UTC coverage: 20.682% (-0.3%) from 20.947%
27145358627

push

github

web-flow
Merge pull request #479 from knowledgepixels/feat/about-pages-478

Resource-page tabs, presets, and role-gated view actions (#478, #302)

1052 of 6429 branches covered (16.36%)

Branch coverage included in aggregate %.

2642 of 11432 relevant lines covered (23.11%)

3.31 hits per line

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

0.0
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.eclipse.rdf4j.model.IRI;
7
import org.eclipse.rdf4j.model.Literal;
8
import org.eclipse.rdf4j.model.Statement;
9
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
10
import org.eclipse.rdf4j.model.vocabulary.RDF;
11
import org.eclipse.rdf4j.model.vocabulary.RDFS;
12
import org.nanopub.Nanopub;
13
import org.nanopub.NanopubUtils;
14
import org.slf4j.Logger;
15
import org.slf4j.LoggerFactory;
16

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

20
import java.io.Serializable;
21
import java.util.*;
22
import java.util.concurrent.TimeUnit;
23

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

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

38
    static Map<IRI, Integer> columnWidths = new HashMap<>();
×
39

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

55
    private static final Cache<String, View> views = CacheBuilder.newBuilder()
×
56
        .maximumSize(5_000)
×
57
        .expireAfterAccess(24, TimeUnit.HOURS)
×
58
        .build();
×
59

60
    /**
61
     * Get a View by its ID, resolving to the latest version (following the
62
     * supersedes chain).
63
     *
64
     * @param id the ID of the View
65
     * @return the View object
66
     */
67
    public static View get(String id) {
68
        return get(id, true);
×
69
    }
70

71
    /**
72
     * Get a View by its ID.
73
     *
74
     * @param id            the ID of the View
75
     * @param resolveLatest if true, follow the supersedes chain to load the latest
76
     *                      version of the view; if false, load exactly the given
77
     *                      version without a latest-version lookup. Pass false when
78
     *                      the caller already holds a latest-resolved IRI (e.g. from
79
     *                      the get-view-displays query, which now resolves it
80
     *                      server-side) to avoid a redundant network round-trip.
81
     * @return the View object
82
     */
83
    public static View get(String id, boolean resolveLatest) {
84
        String npId = id.replaceFirst("^(.*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43})[^A-Za-z0-9-_].*$", "$1");
×
85
        if (resolveLatest) {
×
86
            // Automatically selecting latest version of view definition:
87
            // TODO This should be made configurable at some point, so one can make it a fixed version.
88
            try {
89
                String latestNpId = QueryApiAccess.getLatestVersionId(npId);
×
90
                if (!latestNpId.equals(npId)) {
×
91
                    Nanopub np = Utils.getAsNanopub(latestNpId);
×
92
                    if (np != null) {
×
93
                        Set<String> embeddedIris = NanopubUtils.getEmbeddedIriIds(np);
×
94
                        if (embeddedIris.size() == 1) {
×
95
                            String latestId = embeddedIris.iterator().next();
×
96
                            View cached = views.getIfPresent(latestId);
×
97
                            if (cached == null) {
×
98
                                cached = new View(latestId, np);
×
99
                                views.put(latestId, cached);
×
100
                            }
101
                            return cached;
×
102
                        }
103
                    }
104
                }
105
            } catch (Exception ex) {
×
106
                logger.error("Error resolving latest version for view: {}", id, ex);
×
107
            }
×
108
        }
109
        // Fall back to loading the nanopub as given:
110
        Nanopub np = Utils.getAsNanopub(npId);
×
111
        View cached = views.getIfPresent(id);
×
112
        if (cached == null) {
×
113
            try {
114
                cached = new View(id, np);
×
115
                views.put(id, cached);
×
116
            } catch (Exception ex) {
×
117
                logger.error("Couldn't load nanopub for resource: {}", id, ex);
×
118
            }
×
119
        }
120
        return cached;
×
121
    }
122

123
    private String id;
124
    private Nanopub nanopub;
125
    private IRI viewKind;
126
    private String label;
127
    private String title = "View";
×
128
    private GrlcQuery query;
129
    private String queryField = "resource";
×
130
    private Integer pageSize;
131
    private Integer displayWidth;
132
    private String structuralPosition;
133
    private List<IRI> viewResultActionList = new ArrayList<>();
×
134
    private List<IRI> viewEntryActionList = new ArrayList<>();
×
135
    private Set<IRI> appliesToClasses = new HashSet<>();
×
136
    private Set<IRI> appliesToNamespaces = new HashSet<>();
×
137
    private Map<IRI, Template> actionTemplateMap = new HashMap<>();
×
138
    private Map<IRI, String> actionTemplateTargetFieldMap = new HashMap<>();
×
139
    private Map<IRI, IRI> actionTemplateTypeMap = new HashMap<>();
×
140
    private Map<IRI, String> actionTemplatePartFieldMap = new HashMap<>();
×
141
    private Map<IRI, String> actionTemplateQueryMappingMap = new HashMap<>();
×
142
    private Map<IRI, String> labelMap = new HashMap<>();
×
143
    private IRI viewType;
144
    private Map<IRI, Set<IRI>> actionVisibleToMap = new HashMap<>();
×
145

146
    private View(String id, Nanopub nanopub) {
×
147
        this.id = id;
×
148
        this.nanopub = nanopub;
×
149
        List<IRI> actionList = new ArrayList<>();
×
150
        boolean viewTypeFound = false;
×
151
        for (Statement st : nanopub.getAssertion()) {
×
152
            if (st.getSubject().stringValue().equals(id)) {
×
153
                if (st.getPredicate().equals(RDF.TYPE)) {
×
154
                    if (st.getObject().equals(KPXL_TERMS.RESOURCE_VIEW)) {
×
155
                        viewTypeFound = true;
×
156
                    }
157
                    if (st.getObject() instanceof IRI objIri && supportedViewTypes.contains(objIri)) {
×
158
                        viewType = objIri;
×
159
                    }
160
                } else if (st.getPredicate().equals(DCTERMS.IS_VERSION_OF) && st.getObject() instanceof IRI objIri) {
×
161
                    viewKind = objIri;
×
162
                } else if (st.getPredicate().equals(RDFS.LABEL)) {
×
163
                    label = st.getObject().stringValue();
×
164
                } else if (st.getPredicate().equals(DCTERMS.TITLE)) {
×
165
                    title = st.getObject().stringValue();
×
166
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_QUERY)) {
×
167
                    query = GrlcQuery.get(st.getObject().stringValue());
×
168
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_QUERY_TARGET_FIELD)) {
×
169
                    queryField = st.getObject().stringValue();
×
170
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_ACTION) && st.getObject() instanceof IRI objIri) {
×
171
                    actionList.add(objIri);
×
172
                } else if (st.getPredicate().equals(KPXL_TERMS.APPLIES_TO_NAMESPACE) && st.getObject() instanceof IRI objIri) {
×
173
                    appliesToNamespaces.add(objIri);
×
174
                } else if (st.getPredicate().equals(KPXL_TERMS.APPLIES_TO_INSTANCES_OF) && st.getObject() instanceof IRI objIri) {
×
175
                    appliesToClasses.add(objIri);
×
176
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_VIEW_TARGET_CLASS) && st.getObject() instanceof IRI objIri) {
×
177
                    // Deprecated
178
                    appliesToClasses.add(objIri);
×
179
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_PAGE_SIZE) && st.getObject() instanceof Literal objL) {
×
180
                    try {
181
                        pageSize = Integer.parseInt(objL.stringValue());
×
182
                    } catch (NumberFormatException ex) {
×
183
                        logger.error("Invalid page size value: {}", objL.stringValue(), ex);
×
184
                    }
×
185
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_DISPLAY_WIDTH) && st.getObject() instanceof IRI objIri) {
×
186
                    displayWidth = columnWidths.get(objIri);
×
187
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_STRUCTURAL_POSITION) && st.getObject() instanceof Literal objL) {
×
188
                    structuralPosition = objL.stringValue();
×
189
                }
190
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE)) {
×
191
                Template template = TemplateData.get().getTemplate(st.getObject().stringValue());
×
192
                actionTemplateMap.put((IRI) st.getSubject(), template);
×
193
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE_TARGET_FIELD)) {
×
194
                putUnlessVoid(actionTemplateTargetFieldMap, (IRI) st.getSubject(), st.getObject().stringValue());
×
195
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE_PART_FIELD)) {
×
196
                putUnlessVoid(actionTemplatePartFieldMap, (IRI) st.getSubject(), st.getObject().stringValue());
×
197
            } else if (st.getPredicate().equals(KPXL_TERMS.HAS_ACTION_TEMPLATE_QUERY_MAPPING)) {
×
198
                putUnlessVoid(actionTemplateQueryMappingMap, (IRI) st.getSubject(), st.getObject().stringValue());
×
199
            } else if (st.getPredicate().equals(KPXL_TERMS.IS_VISIBLE_TO) && st.getObject() instanceof IRI objIri) {
×
200
                // Per-action visibility: gen:isVisibleTo on an action node restricts
201
                // that action button to viewers holding the given role tier or
202
                // specific role. See docs/role-specific-views.md.
203
                actionVisibleToMap.computeIfAbsent((IRI) st.getSubject(), k -> new HashSet<>()).add(objIri);
×
204
            } else if (st.getPredicate().equals(RDFS.LABEL)) {
×
205
                labelMap.put((IRI) st.getSubject(), st.getObject().stringValue());
×
206
            } else if (st.getPredicate().equals(RDF.TYPE)) {
×
207
                if (st.getObject().equals(KPXL_TERMS.VIEW_ACTION) || st.getObject().equals(KPXL_TERMS.VIEW_ENTRY_ACTION)) {
×
208
                    actionTemplateTypeMap.put((IRI) st.getSubject(), (IRI) st.getObject());
×
209
                }
210
            }
211
        }
×
212
        for (IRI actionIri : actionList) {
×
213
            if (actionTemplateTypeMap.containsKey(actionIri) && actionTemplateTypeMap.get(actionIri).equals(KPXL_TERMS.VIEW_ENTRY_ACTION)) {
×
214
                viewEntryActionList.add(actionIri);
×
215
            } else {
216
                viewResultActionList.add(actionIri);
×
217
            }
218
        }
×
219
        if (!viewTypeFound) throw new IllegalArgumentException("Not a proper resource view nanopub: " + id);
×
220
        if (query == null) throw new IllegalArgumentException("Query not found: " + id);
×
221
    }
×
222

223
    /**
224
     * Stores an action-field value unless it is the {@code "void"} sentinel.
225
     * View-creation templates can't leave a statement optional inside a repeated
226
     * action group, so views carry every action field, with {@code "void"} for the
227
     * not-applicable ones (its presence is what lets Nanodash repopulate the action
228
     * group when superseding a view). It is treated here as absent — so e.g. a
229
     * "void" part field never becomes a bogus {@code param_void}.
230
     */
231
    private static void putUnlessVoid(Map<IRI, String> map, IRI key, String value) {
232
        if (value != null && !value.equals("void")) {
×
233
            map.put(key, value);
×
234
        }
235
    }
×
236

237
    /**
238
     * Gets the ID of the View.
239
     *
240
     * @return the ID of the View
241
     */
242
    public String getId() {
243
        return id;
×
244
    }
245

246
    /**
247
     * Gets the Nanopub defining this View.
248
     *
249
     * @return the Nanopub defining this View
250
     */
251
    public Nanopub getNanopub() {
252
        return nanopub;
×
253
    }
254

255
    public IRI getViewKindIri() {
256
        return viewKind;
×
257
    }
258

259
    /**
260
     * Gets the label of the View.
261
     *
262
     * @return the label of the View
263
     */
264
    public String getLabel() {
265
        return label;
×
266
    }
267

268
    /**
269
     * Gets the title of the View.
270
     *
271
     * @return the title of the View
272
     */
273
    public String getTitle() {
274
        return title;
×
275
    }
276

277
    /**
278
     * Gets the GrlcQuery associated with the View.
279
     *
280
     * @return the GrlcQuery associated with the View
281
     */
282
    public GrlcQuery getQuery() {
283
        return query;
×
284
    }
285

286
    /**
287
     * Gets the query field of the View.
288
     *
289
     * @return the query field
290
     */
291
    public String getQueryField() {
292
        return queryField;
×
293
    }
294

295
    /**
296
     * Returns the preferred page size.
297
     *
298
     * @return page size (0 = everything on first page)
299
     */
300
    public Integer getPageSize() {
301
        return pageSize;
×
302
    }
303

304
    public Integer getDisplayWidth() {
305
        return displayWidth;
×
306
    }
307

308
    public String getStructuralPosition() {
309
        return structuralPosition;
×
310
    }
311

312
    /**
313
     * Gets the visibility restriction declared on a given action node via
314
     * {@code gen:isVisibleTo}: the set of role-tier or specific-role IRIs a viewer
315
     * must hold for that action button to be shown. An empty set means the action
316
     * is visible to everyone (subject to the existing button-list routing).
317
     *
318
     * @param actionIri the action IRI (a result or entry action of this view)
319
     * @return the set of {@code gen:isVisibleTo} IRIs for that action (never null)
320
     */
321
    public Set<IRI> getActionVisibleTo(IRI actionIri) {
322
        return actionVisibleToMap.getOrDefault(actionIri, Collections.emptySet());
×
323
    }
324

325
    /**
326
     * Gets the list of action IRIs associated with the View.
327
     *
328
     * @return the list of action IRIs
329
     */
330
    public List<IRI> getViewResultActionList() {
331
        return viewResultActionList;
×
332
    }
333

334
    public List<IRI> getViewEntryActionList() {
335
        return viewEntryActionList;
×
336
    }
337

338
    /**
339
     * Gets the Template for a given action IRI.
340
     *
341
     * @param actionIri the action IRI
342
     * @return the Template for the action IRI
343
     */
344
    public Template getTemplateForAction(IRI actionIri) {
345
        return actionTemplateMap.get(actionIri);
×
346
    }
347

348
    /**
349
     * Gets the template field for a given action IRI.
350
     *
351
     * @param actionIri the action IRI
352
     * @return the template field for the action IRI
353
     */
354
    public String getTemplateTargetFieldForAction(IRI actionIri) {
355
        return actionTemplateTargetFieldMap.get(actionIri);
×
356
    }
357

358
    public String getTemplatePartFieldForAction(IRI actionIri) {
359
        return actionTemplatePartFieldMap.get(actionIri);
×
360
    }
361

362
    public String getTemplateQueryMapping(IRI actionIri) {
363
        return actionTemplateQueryMappingMap.get(actionIri);
×
364
    }
365

366
    /**
367
     * Gets the label for a given action IRI.
368
     *
369
     * @param actionIri the action IRI
370
     * @return the label for the action IRI
371
     */
372
    public String getLabelForAction(IRI actionIri) {
373
        return labelMap.get(actionIri);
×
374
    }
375

376
    public boolean appliesTo(String resourceId, Set<IRI> classes) {
377
        for (IRI namespace : appliesToNamespaces) {
×
378
            if (resourceId.startsWith(namespace.stringValue())) return true;
×
379
        }
×
380
        if (classes != null) {
×
381
            for (IRI c : classes) {
×
382
                if (appliesToClasses.contains(c)) return true;
×
383
            }
×
384
        }
385
        return false;
×
386
    }
387

388
    /**
389
     * Checks if the View has target classes.
390
     *
391
     * @return true if the View has target classes, false otherwise
392
     */
393
    public boolean appliesToClasses() {
394
        return !appliesToClasses.isEmpty();
×
395
    }
396

397
    /**
398
     * Checks if the View has a specific target class.
399
     *
400
     * @param targetClass the target class IRI
401
     * @return true if the View has the target class, false otherwise
402
     */
403
    public boolean appliesToClass(IRI targetClass) {
404
        return appliesToClasses.contains(targetClass);
×
405
    }
406

407
    @Override
408
    public String toString() {
409
        return id;
×
410
    }
411

412
    /**
413
     * Gets the view type of the View.
414
     *
415
     * @return the view type mode IRI
416
     */
417
    public IRI getViewType() {
418
        return viewType;
×
419
    }
420

421
    /**
422
     * Get the supported view types.
423
     *
424
     * @return a set of supported view type IRIs
425
     */
426
    public static Set<IRI> getSupportedViewTypes() {
427
        return supportedViewTypes;
×
428
    }
429

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