• 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/ViewDisplay.java
1
package com.knowledgepixels.nanodash;
2

3
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
4
import org.eclipse.rdf4j.model.IRI;
5
import org.eclipse.rdf4j.model.Literal;
6
import org.eclipse.rdf4j.model.Statement;
7
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
8
import org.eclipse.rdf4j.model.vocabulary.RDF;
9
import org.nanopub.Nanopub;
10
import org.slf4j.Logger;
11
import org.slf4j.LoggerFactory;
12

13
import java.io.Serializable;
14
import java.util.HashSet;
15
import java.util.Set;
16

17
/**
18
 * A class representing the display of a resource view associated with a Space.
19
 */
20
public class ViewDisplay implements Serializable, Comparable<ViewDisplay> {
21

22
    private static final Logger logger = LoggerFactory.getLogger(ViewDisplay.class);
×
23

24
    private String id;
25
    private Nanopub nanopub;
26
    private View view;
27
    private IRI viewIri;
28
    private String title;
29
    private Integer pageSize;
30
    private Integer displayWidth;
31
    private String structuralPosition;
32
    private Set<IRI> types = new HashSet<>();
×
33
    private Set<String> appliesTo = new HashSet<>();
×
34
    private Set<IRI> appliesToClasses = new HashSet<>();
×
35
    private Set<IRI> appliesToNamespaces = new HashSet<>();
×
36
    private IRI resource;
37

38
    /**
39
     * Constructor for ViewDisplay with only a View. This is used for temporary view displays used in profiles as defaults.
40
     *
41
     * @param view the View associated with this ViewDisplay
42
     */
43
    public ViewDisplay(View view) {
×
44
        this.id = null;
×
45
        this.nanopub = view.getNanopub();
×
46
        this.view = view;
×
47
    }
×
48

49
    /**
50
     * Get a View by its ID.
51
     *
52
     * @param id the ID of the View
53
     * @return the View object
54
     */
55
    public static ViewDisplay get(String id) throws IllegalArgumentException {
56
        return get(id, null);
×
57
    }
58

59
    /**
60
     * Get a ViewDisplay by its ID, using a pre-resolved latest view IRI.
61
     *
62
     * <p>The display nanopub is loaded directly, without a per-display latest-version
63
     * lookup: the get-view-displays query already returns only current display
64
     * nanopubs (its {@code npx:invalidates} filter excludes superseded ones, since
65
     * superseding a display also invalidates it under the same signing key). The
66
     * displayed view is likewise loaded directly from {@code latestViewIri}, which the
67
     * query resolved to its latest version server-side. The result: no one-by-one
68
     * latest-version network round-trips when building the view displays for a page.</p>
69
     *
70
     * @param id            the ID of the ViewDisplay (a current display nanopub)
71
     * @param latestViewIri the already latest-resolved IRI of the displayed view (from
72
     *                      the get-view-displays query), or null to resolve the view's
73
     *                      latest version separately
74
     * @return the ViewDisplay object
75
     */
76
    public static ViewDisplay get(String id, String latestViewIri) throws IllegalArgumentException {
77
        try {
78
            Nanopub np = Utils.getAsNanopub(id.replaceFirst("^(.*[^A-Za-z0-9-_])?(RA[A-Za-z0-9-_]{43})[^A-Za-z0-9-_].*$", "$2"));
×
79
            return new ViewDisplay(id, np, latestViewIri);
×
80
        } catch (Exception ex) {
×
81
            logger.error("Couldn't load nanopub for resource: {}", id, ex);
×
82
            throw new IllegalArgumentException("invalid view value " + id);
×
83
        }
84
    }
85

86
    /**
87
     * Builds a view display derived from a preset assignment (issue #302).
88
     *
89
     * <p>Used for the preset-derived rows emitted by the {@code get-view-displays}
90
     * query: instead of a standalone view-display nanopub, the row carries the
91
     * resolved view IRI plus the assignment's activation state. The resulting
92
     * object behaves like a top-level view display for {@code resourceId}, so it
93
     * flows through the same latest-wins / deactivation aggregation in
94
     * {@link com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile} as
95
     * standalone displays.</p>
96
     *
97
     * @param resourceId  the resource the preset is assigned to
98
     * @param viewIri     the resolved view IRI (a {@code gen:hasView} /
99
     *                    {@code gen:hasTopLevelView} target of the preset)
100
     * @param topLevel    whether this came from {@code gen:hasTopLevelView} (shown
101
     *                    at the top level of the resource page) vs. {@code gen:hasView}
102
     *                    (shown at the part level, for parts matching the view's own
103
     *                    class/namespace targeting)
104
     * @param deactivated whether the underlying preset assignment is deactivated
105
     * @return the ViewDisplay, or null if the view could not be resolved
106
     */
107
    public static ViewDisplay forPresetView(String resourceId, String viewIri, boolean topLevel, boolean deactivated) {
108
        // viewIri is already latest-resolved by the get-view-displays query, so load
109
        // it directly without a separate latest-version lookup.
110
        View view = View.get(viewIri, false);
×
111
        if (view == null) {
×
112
            logger.error("Couldn't resolve preset view: {}", viewIri);
×
113
            return null;
×
114
        }
115
        return new ViewDisplay(resourceId, view, topLevel, deactivated);
×
116
    }
117

118
    private ViewDisplay(String resourceId, View view, boolean topLevel, boolean deactivated) {
×
119
        this.id = null;
×
120
        this.nanopub = view.getNanopub();
×
121
        this.view = view;
×
122
        if (topLevel) {
×
123
            // gen:hasTopLevelView: pin to the resource's own page (top level).
124
            this.appliesTo.add(resourceId);
×
125
        }
126
        // else gen:hasView: leave appliesTo empty so applicability falls back to the
127
        // view's own class/namespace targeting -> shown at the part level for matching
128
        // parts, not at the resource's top level.
129
        if (deactivated) {
×
130
            this.types.add(KPXL_TERMS.DEACTIVATED_VIEW_DISPLAY);
×
131
        }
132
    }
×
133

134
    /**
135
     * Constructor for ViewDisplay.
136
     *
137
     * @param id      the ID of the ViewDisplay
138
     * @param nanopub the Nanopub containing the data for this ViewDisplay
139
     */
140
    private ViewDisplay(String id, Nanopub nanopub) {
141
        this(id, nanopub, null);
×
142
    }
×
143

144
    /**
145
     * Constructor for ViewDisplay.
146
     *
147
     * @param id            the ID of the ViewDisplay
148
     * @param nanopub       the Nanopub containing the data for this ViewDisplay
149
     * @param latestViewIri the already latest-resolved IRI of the displayed view, or
150
     *                      null to resolve the view's latest version separately. When
151
     *                      given, the view is loaded directly (no extra round-trip).
152
     */
153
    private ViewDisplay(String id, Nanopub nanopub, String latestViewIri) {
×
154
        this.id = id;
×
155
        this.nanopub = nanopub;
×
156

157
        boolean viewDisplayTypeFound = false;
×
158
        for (Statement st : nanopub.getAssertion()) {
×
159
            if (st.getSubject().stringValue().equals(id)) {
×
160
                if (st.getPredicate().equals(RDF.TYPE)) {
×
161
                    if (st.getObject().equals(KPXL_TERMS.VIEW_DISPLAY) || st.getObject().equals(KPXL_TERMS.DEACTIVATED_VIEW_DISPLAY)) {
×
162
                        viewDisplayTypeFound = true;
×
163
                    }
164
                    if (st.getObject() instanceof IRI objIri && !st.getObject().equals(KPXL_TERMS.VIEW_DISPLAY)) {
×
165
                        types.add(objIri);
×
166
                    }
167
                } else if (st.getPredicate().equals(DCTERMS.TITLE)) {
×
168
                    title = st.getObject().stringValue();
×
169
                } else if (st.getPredicate().equals(KPXL_TERMS.IS_DISPLAY_OF_VIEW) && st.getObject() instanceof IRI objIri) {
×
170
                    if (view != null) {
×
171
                        throw new IllegalArgumentException("View already set: " + objIri);
×
172
                    }
173
                    viewIri = objIri;
×
174
                    view = (latestViewIri != null && !latestViewIri.isEmpty())
×
175
                            ? View.get(latestViewIri, false)
×
176
                            : View.get(objIri.stringValue());
×
177
                } else if (st.getPredicate().equals(KPXL_TERMS.IS_DISPLAY_FOR) && st.getObject() instanceof IRI objIri) {
×
178
                    if (resource != null) {
×
179
                        throw new IllegalArgumentException("Resource already set: " + objIri);
×
180
                    }
181
                    resource = objIri;
×
182
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_PAGE_SIZE) && st.getObject() instanceof Literal objL) {
×
183
                    try {
184
                        pageSize = Integer.parseInt(objL.stringValue());
×
185
                    } catch (NumberFormatException ex) {
×
186
                        logger.error("Invalid page size value: {}", objL.stringValue(), ex);
×
187
                    }
×
188
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_DISPLAY_WIDTH) && st.getObject() instanceof IRI objIri) {
×
189
                    displayWidth = View.columnWidths.get(objIri);
×
190
                } else if (st.getPredicate().equals(KPXL_TERMS.HAS_STRUCTURAL_POSITION) && st.getObject() instanceof Literal objL) {
×
191
                    structuralPosition = objL.stringValue();
×
192
                } else if (st.getPredicate().equals(KPXL_TERMS.APPLIES_TO_NAMESPACE) && st.getObject() instanceof IRI objIri) {
×
193
                    appliesToNamespaces.add(objIri);
×
194
                } else if (st.getPredicate().equals(KPXL_TERMS.APPLIES_TO_INSTANCES_OF) && st.getObject() instanceof IRI objIri) {
×
195
                    appliesToClasses.add(objIri);
×
196
                } else if (st.getPredicate().equals(KPXL_TERMS.APPLIES_TO) && st.getObject() instanceof IRI objIri) {
×
197
                    appliesTo.add(objIri.stringValue());
×
198
                }
199
            }
200
        }
×
201
        if (!viewDisplayTypeFound) throw new IllegalArgumentException("Not a proper view display nanopub: " + id);
×
202
        if (view == null) throw new IllegalArgumentException("View not found: " + id);
×
203
    }
×
204

205
    public String getId() {
206
        return id;
×
207
    }
208

209
    public boolean hasType(IRI type) {
210
        return types.contains(type);
×
211
    }
212

213
    /**
214
     * Creates a plain minimal view display without attached view object.
215
     *
216
     * @param pageSize the page size of the view display
217
     */
218
    public ViewDisplay(Integer pageSize) {
×
219
        this.pageSize = pageSize;
×
220
    }
×
221

222
    /**
223
     * Gets the View associated with this ViewDisplay.
224
     *
225
     * @return the View
226
     */
227
    public View getView() {
228
        return view;
×
229
    }
230

231
    public IRI getViewIri() {
232
        return viewIri;
×
233
    }
234

235
    public IRI getViewKindIri() {
236
        IRI kind = view.getViewKindIri();
×
237
        if (kind != null) return kind;
×
238
        return viewIri;
×
239
    }
240

241
    public boolean appliesTo(String resourceId, Set<IRI> classes) {
242
        if (appliesTo.contains(resourceId)) return true;
×
243
        if (appliesToNamespaces.isEmpty() && appliesToClasses.isEmpty()) {
×
244
            if (!appliesTo.isEmpty()) return false;
×
245
            return view.appliesTo(resourceId, classes);
×
246
        } else {
247
            for (IRI namespace : appliesToNamespaces) {
×
248
                if (resourceId.startsWith(namespace.stringValue())) return true;
×
249
            }
×
250
            if (classes != null) {
×
251
                for (IRI c : classes) {
×
252
                    if (appliesToClasses.contains(c)) return true;
×
253
                }
×
254
            }
255
        }
256
        return false;
×
257
    }
258

259
    public boolean appliesToClasses() {
260
        if (appliesToClasses.isEmpty()) {
×
261
            return view.appliesToClasses();
×
262
        } else {
263
            return true;
×
264
        }
265
    }
266

267
    public boolean appliesToClass(IRI targetClass) {
268
        if (appliesToClasses.isEmpty()) {
×
269
            return view.appliesToClasses();
×
270
        } else {
271
            return appliesToClasses.contains(targetClass);
×
272
        }
273
    }
274

275
    /**
276
     * Gets the nanopub ID associated with this ViewDisplay
277
     *
278
     * @return the nanopub ID
279
     */
280
    public IRI getNanopubId() {
281
        if (nanopub == null) {
×
282
            return null;
×
283
        }
284
        return nanopub.getUri();
×
285
    }
286

287
    /**
288
     * Gets the nanopub associated with this ViewDisplay
289
     *
290
     * @return the nanopub, or null if not available
291
     */
292
    public Nanopub getNanopub() {
293
        return nanopub;
×
294
    }
295

296
    public Integer getPageSize() {
297
        if (pageSize != null) return pageSize;
×
298
        if (view == null) return 10;
×
299
        if (view.getPageSize() != null) return view.getPageSize();
×
300
        return 10;
×
301
    }
302

303
    public Integer getDisplayWidth() {
304
        if (displayWidth != null) return displayWidth;
×
305
        if (view == null) return 12;
×
306
        if (view.getDisplayWidth() != null) return view.getDisplayWidth();
×
307
        return 12;
×
308
    }
309

310
    public ViewDisplay withDisplayWidth(int width) {
311
        this.displayWidth = width;
×
312
        return this;
×
313
    }
314

315
    public String getStructuralPosition() {
316
        if (structuralPosition != null) return structuralPosition;
×
317
        if (view == null) return "5.5.default";
×
318
        if (view.getStructuralPosition() != null) return view.getStructuralPosition();
×
319
        return "5.5.default";
×
320
    }
321

322
    public String getTitle() {
323
        if (title != null) return title;
×
324
        if (view != null) return view.getTitle();
×
325
        return null;
×
326
    }
327

328
    @Override
329
    public int compareTo(ViewDisplay other) {
330
        return this.getStructuralPosition().compareTo(other.getStructuralPosition());
×
331
    }
332

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