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

knowledgepixels / nanodash / 35330653258

18 Sep 2026 09:39AM UTC coverage: 48.287% (-0.01%) from 48.297%
35330653258

push

github

web-flow
Merge pull request #715 from knowledgepixels/fix/deactivate-preset-view-display

fix(views): offer "deactivate view display" for preset-supplied views

4807 of 10711 branches covered (44.88%)

Branch coverage included in aggregate %.

8581 of 17015 relevant lines covered (50.43%)

8.09 hits per line

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

41.8
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);
12✔
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<>();
45✔
33
    private Set<String> appliesTo = new HashSet<>();
45✔
34
    private Set<IRI> appliesToClasses = new HashSet<>();
45✔
35
    private Set<IRI> appliesToNamespaces = new HashSet<>();
45✔
36
    private IRI resource;
37
    private boolean presetDerived = false;
27✔
38

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

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

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

87
    /**
88
     * Builds a view display derived from a preset assignment (issue #302).
89
     *
90
     * <p>Used for the preset-derived rows emitted by the {@code get-view-displays}
91
     * query: instead of a standalone view-display nanopub, the row carries the
92
     * resolved view IRI plus the assignment's activation state. The resulting
93
     * object behaves like a top-level view display for {@code resourceId}, so it
94
     * flows through the same latest-wins / deactivation aggregation in
95
     * {@link com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile} as
96
     * standalone displays.</p>
97
     *
98
     * @param resourceId    the resource the preset is assigned to
99
     * @param viewIri       the view IRI (a {@code gen:hasView} /
100
     *                      {@code gen:hasTopLevelView} target of the preset)
101
     * @param topLevel      whether this came from {@code gen:hasTopLevelView} (shown
102
     *                      at the top level of the resource page) vs. {@code gen:hasView}
103
     *                      (shown at the part level, for parts matching the view's own
104
     *                      class/namespace targeting)
105
     * @param deactivated   whether the underlying preset assignment is deactivated
106
     * @param resolveLatest whether to resolve {@code viewIri} to its latest version
107
     *                      here; pass false when the query already resolved it
108
     *                      server-side, true for the unresolved query variant
109
     * @return the ViewDisplay, or null if the view could not be resolved
110
     */
111
    public static ViewDisplay forPresetView(String resourceId, String viewIri, boolean topLevel, boolean deactivated, boolean resolveLatest) {
112
        View view = View.get(viewIri, resolveLatest);
×
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
        this.presetDerived = true;
×
125
        if (topLevel) {
×
126
            // gen:hasTopLevelView: pin to the resource's own page (top level).
127
            this.appliesTo.add(resourceId);
×
128
        }
129
        // else gen:hasView: leave appliesTo empty so applicability falls back to the
130
        // view's own class/namespace targeting -> shown at the part level for matching
131
        // parts, not at the resource's top level.
132
        if (deactivated) {
×
133
            this.types.add(KPXL_TERMS.DEACTIVATED_VIEW_DISPLAY);
×
134
        }
135
    }
×
136

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

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

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

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

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

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

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

234
    public IRI getViewIri() {
235
        return viewIri;
9✔
236
    }
237

238
    /**
239
     * Whether this display comes from a preset assignment rather than from a view-display
240
     * nanopub of its own (see {@link #forPresetView}). Such a display has no nanopub to edit,
241
     * but it can still be deactivated for the resource like a standalone one.
242
     *
243
     * @return true if derived from a preset assignment
244
     */
245
    public boolean isPresetDerived() {
246
        return presetDerived;
×
247
    }
248

249
    public IRI getViewKindIri() {
250
        IRI kind = view.getViewKindIri();
×
251
        if (kind != null) return kind;
×
252
        return viewIri;
×
253
    }
254

255
    public boolean appliesTo(String resourceId, Set<IRI> classes) {
256
        if (appliesTo.contains(resourceId)) return true;
×
257
        if (appliesToNamespaces.isEmpty() && appliesToClasses.isEmpty()) {
×
258
            if (!appliesTo.isEmpty()) return false;
×
259
            return view.appliesTo(resourceId, classes);
×
260
        } else {
261
            for (IRI namespace : appliesToNamespaces) {
×
262
                if (resourceId.startsWith(namespace.stringValue())) return true;
×
263
            }
×
264
            if (classes != null) {
×
265
                for (IRI c : classes) {
×
266
                    if (appliesToClasses.contains(c)) return true;
×
267
                }
×
268
            }
269
        }
270
        return false;
×
271
    }
272

273
    public boolean appliesToClasses() {
274
        if (appliesToClasses.isEmpty()) {
×
275
            return view.appliesToClasses();
×
276
        } else {
277
            return true;
×
278
        }
279
    }
280

281
    public boolean appliesToClass(IRI targetClass) {
282
        if (appliesToClasses.isEmpty()) {
×
283
            return view.appliesToClasses();
×
284
        } else {
285
            return appliesToClasses.contains(targetClass);
×
286
        }
287
    }
288

289
    /**
290
     * Gets the nanopub ID associated with this ViewDisplay
291
     *
292
     * @return the nanopub ID
293
     */
294
    public IRI getNanopubId() {
295
        if (nanopub == null) {
9!
296
            return null;
6✔
297
        }
298
        return nanopub.getUri();
×
299
    }
300

301
    /**
302
     * Gets the nanopub associated with this ViewDisplay
303
     *
304
     * @return the nanopub, or null if not available
305
     */
306
    public Nanopub getNanopub() {
307
        return nanopub;
×
308
    }
309

310
    public Integer getPageSize() {
311
        if (pageSize != null) return pageSize;
18!
312
        if (view == null) return 10;
×
313
        if (view.getPageSize() != null) return view.getPageSize();
×
314
        return 10;
×
315
    }
316

317
    public Integer getDisplayWidth() {
318
        if (displayWidth != null) return displayWidth;
×
319
        if (view == null) return 12;
×
320
        if (view.getDisplayWidth() != null) return view.getDisplayWidth();
×
321
        return 12;
×
322
    }
323

324
    public ViewDisplay withDisplayWidth(int width) {
325
        this.displayWidth = width;
×
326
        return this;
×
327
    }
328

329
    /**
330
     * Overrides the title shown by the result components. An empty string hides the
331
     * title row entirely (used on the view-results page, where the query form above
332
     * already shows the view's icon+title).
333
     *
334
     * @param title the title to show, or "" to hide it
335
     * @return this view display, for chaining
336
     */
337
    public ViewDisplay withTitle(String title) {
338
        this.title = title;
×
339
        return this;
×
340
    }
341

342
    public String getStructuralPosition() {
343
        if (structuralPosition != null) return structuralPosition;
9!
344
        if (view == null) return "5.5.default";
15!
345
        if (view.getStructuralPosition() != null) return view.getStructuralPosition();
×
346
        return "5.5.default";
×
347
    }
348

349
    public String getTitle() {
350
        if (title != null) return title;
9!
351
        if (view != null) return view.getTitle();
9!
352
        return null;
6✔
353
    }
354

355
    @Override
356
    public int compareTo(ViewDisplay other) {
357
        return this.getStructuralPosition().compareTo(other.getStructuralPosition());
×
358
    }
359

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