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

knowledgepixels / nanodash / 27127668106

08 Jun 2026 09:14AM UTC coverage: 20.467% (-0.5%) from 20.924%
27127668106

Pull #479

github

web-flow
Merge f10bc9fe9 into 5556185a4
Pull Request #479: About pages for spaces, maintained resources, and users (#478)

1024 of 6405 branches covered (15.99%)

Branch coverage included in aggregate %.

2624 of 11419 relevant lines covered (22.98%)

3.29 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.domain.Space;
4
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
5
import org.eclipse.rdf4j.model.IRI;
6
import org.eclipse.rdf4j.model.Literal;
7
import org.eclipse.rdf4j.model.Statement;
8
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
9
import org.eclipse.rdf4j.model.vocabulary.RDF;
10
import org.nanopub.Nanopub;
11
import org.slf4j.Logger;
12
import org.slf4j.LoggerFactory;
13

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

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

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

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

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

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

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

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

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

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

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

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

209
    public String getId() {
210
        return id;
×
211
    }
212

213
    public boolean hasType(IRI type) {
214
        return types.contains(type);
×
215
    }
216

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

226
    /**
227
     * Gets the View associated with this ViewDisplay.
228
     *
229
     * @return the View
230
     */
231
    public View getView() {
232
        return view;
×
233
    }
234

235
    public IRI getViewIri() {
236
        return viewIri;
×
237
    }
238

239
    public IRI getViewKindIri() {
240
        IRI kind = view.getViewKindIri();
×
241
        if (kind != null) return kind;
×
242
        return viewIri;
×
243
    }
244

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

263
    public boolean appliesToClasses() {
264
        if (appliesToClasses.isEmpty()) {
×
265
            return view.appliesToClasses();
×
266
        } else {
267
            return true;
×
268
        }
269
    }
270

271
    public boolean appliesToClass(IRI targetClass) {
272
        if (appliesToClasses.isEmpty()) {
×
273
            return view.appliesToClasses();
×
274
        } else {
275
            return appliesToClasses.contains(targetClass);
×
276
        }
277
    }
278

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

291
    /**
292
     * Gets the nanopub associated with this ViewDisplay
293
     *
294
     * @return the nanopub, or null if not available
295
     */
296
    public Nanopub getNanopub() {
297
        return nanopub;
×
298
    }
299

300
    public Integer getPageSize() {
301
        if (pageSize != null) return pageSize;
×
302
        if (view == null) return 10;
×
303
        if (view.getPageSize() != null) return view.getPageSize();
×
304
        return 10;
×
305
    }
306

307
    public Integer getDisplayWidth() {
308
        if (displayWidth != null) return displayWidth;
×
309
        if (view == null) return 12;
×
310
        if (view.getDisplayWidth() != null) return view.getDisplayWidth();
×
311
        return 12;
×
312
    }
313

314
    public ViewDisplay withDisplayWidth(int width) {
315
        this.displayWidth = width;
×
316
        return this;
×
317
    }
318

319
    public String getStructuralPosition() {
320
        if (structuralPosition != null) return structuralPosition;
×
321
        if (view == null) return "5.5.default";
×
322
        if (view.getStructuralPosition() != null) return view.getStructuralPosition();
×
323
        return "5.5.default";
×
324
    }
325

326
    public String getTitle() {
327
        if (title != null) return title;
×
328
        if (view != null) return view.getTitle();
×
329
        return null;
×
330
    }
331

332
    /**
333
     * Whether this view display should be shown to the given viewer, per its
334
     * {@code gen:isVisibleTo} restriction (falling back to the displayed view's
335
     * default when the display declares none). An empty restriction means
336
     * visible to everyone.
337
     *
338
     * <p>A role-tier IRI matches when the viewer's highest role tier in the
339
     * governing space meets or exceeds it (admin {@literal >} maintainer
340
     * {@literal >} member {@literal >} observer); a specific role IRI matches
341
     * when the viewer holds exactly that role. Multiple restrictions are OR-ed.
342
     * There is no admin override for specific roles — an admin who needs such a
343
     * view self-assigns the role.</p>
344
     *
345
     * @param viewer         the viewer's agent IRI, or null if logged out
346
     * @param governingSpace the space whose roles govern visibility, or null
347
     *                       (e.g. a user page) — then a restricted display is
348
     *                       shown only to the page owner
349
     * @param viewerIsOwner  whether the viewer owns the resource this display is
350
     *                       for (used only when there is no governing space)
351
     * @return true if the display should be shown
352
     */
353
    public boolean isVisibleTo(IRI viewer, Space governingSpace, boolean viewerIsOwner) {
354
        Set<IRI> reqs = (visibleTo.isEmpty() && view != null) ? view.getVisibleTo() : visibleTo;
×
355
        if (reqs == null || reqs.isEmpty()) return true;
×
356
        if (governingSpace == null) return viewerIsOwner;
×
357
        if (viewer == null) return false;
×
358
        for (IRI req : reqs) {
×
359
            if (SpaceMemberRole.isTier(req)) {
×
360
                if (governingSpace.userTier(viewer) >= SpaceMemberRole.tierRank(req)) return true;
×
361
            } else if (governingSpace.viewerHoldsRole(viewer, req)) {
×
362
                return true;
×
363
            }
364
        }
×
365
        return false;
×
366
    }
367

368
    @Override
369
    public int compareTo(ViewDisplay other) {
370
        return this.getStructuralPosition().compareTo(other.getStructuralPosition());
×
371
    }
372

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