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

knowledgepixels / nanodash / 30449053577

29 Jul 2026 11:49AM UTC coverage: 34.201% (-0.2%) from 34.372%
30449053577

Pull #573

github

web-flow
Merge fb645b715 into 3f292c5bd
Pull Request #573: feat(views): add header views (gen:HeaderView)

2460 of 7964 branches covered (30.89%)

Branch coverage included in aggregate %.

4802 of 13269 relevant lines covered (36.19%)

5.59 hits per line

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

0.0
src/main/java/com/knowledgepixels/nanodash/component/ViewList.java
1
package com.knowledgepixels.nanodash.component;
2

3
import com.google.common.collect.ArrayListMultimap;
4
import com.google.common.collect.Multimap;
5
import com.knowledgepixels.nanodash.View;
6
import com.knowledgepixels.nanodash.ViewDisplay;
7
import com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile;
8
import com.knowledgepixels.nanodash.domain.Space;
9
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
10
import org.apache.wicket.Component;
11
import org.apache.wicket.behavior.AttributeAppender;
12
import org.apache.wicket.markup.html.WebMarkupContainer;
13
import org.apache.wicket.markup.html.basic.Label;
14
import org.apache.wicket.markup.html.link.AbstractLink;
15
import org.apache.wicket.markup.html.list.ListItem;
16
import org.apache.wicket.markup.html.list.ListView;
17
import org.apache.wicket.markup.html.panel.Panel;
18
import org.eclipse.rdf4j.model.IRI;
19
import org.nanopub.extra.services.QueryRef;
20
import org.nanopub.extra.services.QueryTemplate;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23

24
import java.util.ArrayList;
25
import java.util.List;
26
import java.util.Set;
27

28
public class ViewList extends Panel {
29

30
    private static final Logger logger = LoggerFactory.getLogger(ViewList.class);
×
31

32
    // The ref (root definition) this list is pinned to (?root=), used to gate each view's
33
    // action visibility against the claimant being viewed rather than the resource's
34
    // representative ref. Null = representative ref. Read at render time by the inner
35
    // ListView, so it may be set after the delegating constructor. See docs/space-ref-identity.md.
36
    private String refRoot;
37

38
    public ViewList(String markupId, AbstractResourceWithProfile resourceWithProfile) {
39
        this(markupId, resourceWithProfile, null, null, null, null, null, true, null);
×
40
    }
×
41

42
    public ViewList(String markupId, AbstractResourceWithProfile resourceWithProfile, String partId, String nanopubId, Set<IRI> partClasses) {
43
        this(markupId, resourceWithProfile, partId, nanopubId, partClasses, null, null, true, null);
×
44
    }
×
45

46
    public ViewList(String markupId, AbstractResourceWithProfile resourceWithProfile, String partId, String nanopubId, Set<IRI> partClasses, AbstractResourceWithProfile footerResource, List<AbstractLink> footerAdminButtons) {
47
        this(markupId, resourceWithProfile, partId, nanopubId, partClasses, footerResource, footerAdminButtons, true, null);
×
48
    }
×
49

50
    public ViewList(String markupId, AbstractResourceWithProfile resourceWithProfile, String partId, String nanopubId, Set<IRI> partClasses, AbstractResourceWithProfile footerResource, List<AbstractLink> footerAdminButtons, boolean showEmptyNotice) {
51
        this(markupId, resourceWithProfile, partId, nanopubId, partClasses, footerResource, footerAdminButtons, showEmptyNotice, null);
×
52
    }
×
53

54
    public ViewList(String markupId, AbstractResourceWithProfile resourceWithProfile, List<ViewDisplay> explicitViewDisplays) {
55
        this(markupId, resourceWithProfile, null, null, null, null, null, false, explicitViewDisplays);
×
56
    }
×
57

58
    /**
59
     * As {@link #ViewList(String, AbstractResourceWithProfile, List)} but pinned to a specific
60
     * ref (root definition), so each view's action visibility is gated against that claimant's
61
     * authority. Used for {@code ?root=}-pinned space pages. See docs/space-ref-identity.md.
62
     */
63
    public ViewList(String markupId, AbstractResourceWithProfile resourceWithProfile, List<ViewDisplay> explicitViewDisplays, String refRoot) {
64
        this(markupId, resourceWithProfile, null, null, null, null, null, false, explicitViewDisplays);
×
65
        this.refRoot = refRoot;
×
66
    }
×
67

68
    public ViewList(String markupId, AbstractResourceWithProfile resourceWithProfile, List<ViewDisplay> explicitViewDisplays, AbstractResourceWithProfile footerResource, List<AbstractLink> footerAdminButtons) {
69
        this(markupId, resourceWithProfile, null, null, null, footerResource, footerAdminButtons, false, explicitViewDisplays);
×
70
    }
×
71

72
    private ViewList(String markupId, AbstractResourceWithProfile resourceWithProfile, String partId, String nanopubId, Set<IRI> partClasses, AbstractResourceWithProfile footerResource, List<AbstractLink> footerAdminButtons, boolean showEmptyNotice, List<ViewDisplay> explicitViewDisplays) {
73
        super(markupId);
×
74

75
        final String id = (partId == null ? resourceWithProfile.getId() : partId);
×
76
        final String npId = (nanopubId == null ? resourceWithProfile.getNanopubId() : nanopubId);
×
77
        final List<ViewDisplay> viewDisplays;
78
        if (explicitViewDisplays != null) {
×
79
            viewDisplays = explicitViewDisplays;
×
80
        } else if (partId == null) {
×
81
            viewDisplays = resourceWithProfile.getTopLevelViewDisplays();
×
82
        } else {
83
            viewDisplays = resourceWithProfile.getPartLevelViewDisplays(partId, partClasses);
×
84
        }
85

86
        // Group viewDisplays by the first segment of their structural position (e.g. "4" from "4.4.1.papers")
87
        List<List<ViewDisplay>> groups = new ArrayList<>();
×
88
        String currentGroupKey = null;
×
89
        List<ViewDisplay> currentGroup = null;
×
90
        for (ViewDisplay vd : viewDisplays) {
×
91
            String pos = vd.getStructuralPosition();
×
92
            int firstDot = pos.indexOf('.');
×
93
            String key = firstDot > 0 ? pos.substring(0, firstDot) : pos;
×
94
            if (!key.equals(currentGroupKey)) {
×
95
                currentGroup = new ArrayList<>();
×
96
                groups.add(currentGroup);
×
97
                currentGroupKey = key;
×
98
            }
99
            currentGroup.add(vd);
×
100
        }
×
101

102
        add(new ListView<List<ViewDisplay>>("groups", groups) {
×
103
            @Override
104
            protected void populateItem(ListItem<List<ViewDisplay>> groupItem) {
105
                List<ViewDisplay> group = groupItem.getModelObject();
×
106
                groupItem.add(new ListView<ViewDisplay>("views", group) {
×
107
                    @Override
108
                    protected void populateItem(ListItem<ViewDisplay> item) {
109
                        // This populate runs at render time (onBeforeRender) and is the only
110
                        // view-rendering path without a guard; every other one (the QueryResult
111
                        // builders, populateComponent, per-cell populateItem) already degrades a
112
                        // failure to an inline error. Without this catch, a single view whose
113
                        // build/render dereferences a null takes down the whole page render.
114
                        try {
115
                            View view = item.getModelObject().getView();
×
116
                            if (KPXL_TERMS.HEADER_VIEW.equals(view.getViewType())) {
×
117
                                // Header views have no query (issue #572), so they skip all the
118
                                // query machinery below and render directly.
119
                                Component header = new HeaderViewPanel("view", item.getModelObject(), resourceWithProfile, id, resourceWithProfile.getId(), refRoot);
×
120
                                header.add(new AttributeAppender("class", " col-" + item.getModelObject().getDisplayWidth() + " section-header"));
×
121
                                item.add(header);
×
122
                                return;
×
123
                            }
124
                            Multimap<String, String> queryRefParams = ArrayListMultimap.create();
×
125
                            for (String p : view.getQuery().getPlaceholdersList()) {
×
126
                                String paramName = QueryTemplate.getParamName(p);
×
127
                                if (paramName.equals(view.getQueryField())) {
×
128
                                    queryRefParams.put(view.getQueryField(), id);
×
129
                                    if (QueryTemplate.isMultiPlaceholder(p) && resourceWithProfile instanceof Space space) {
×
130
                                        // TODO Support this also for maintained resources and users.
131
                                        for (String altId : space.getAltIDs()) {
×
132
                                            queryRefParams.put(view.getQueryField(), altId);
×
133
                                        }
×
134
                                    }
135
                                } else if (paramName.equals(view.getQueryField() + "Namespace") && resourceWithProfile.getNamespace() != null) {
×
136
                                    queryRefParams.put(view.getQueryField() + "Namespace", resourceWithProfile.getNamespace());
×
137
                                } else if (paramName.equals(view.getQueryField() + "Np")) {
×
138
                                    if (!QueryTemplate.isOptionalPlaceholder(p) && npId == null) {
×
139
                                        queryRefParams.put(view.getQueryField() + "Np", "x:");
×
140
                                    } else {
141
                                        queryRefParams.put(view.getQueryField() + "Np", npId);
×
142
                                    }
143
                                } else if (paramName.equals("context")) {
×
144
                                    // Auto-fill the page's context (the resource maintaining the shown
145
                                    // element) so queries like get-part-info can render as content-tab
146
                                    // view displays; on a resource's own page this is the resource itself.
147
                                    queryRefParams.put("context", resourceWithProfile.getId());
×
148
                                } else if (paramName.equals("root_np")) {
×
149
                                    // Auto-fill the ref scope (root nanopub) from the page's effective ref,
150
                                    // the same way the resource IRI above is filled, so a content-tab view
151
                                    // whose query opts into ref-scoping is scoped without the panel threading
152
                                    // it. Left empty when no ref is known (an optional placeholder tolerates
153
                                    // the empty VALUES; the ref-scoped query then yields its no-ref result).
154
                                    if (refRoot != null && !refRoot.isEmpty()) {
×
155
                                        queryRefParams.put("root_np", refRoot);
×
156
                                    }
157
                                } else if (!QueryTemplate.isOptionalPlaceholder(p)) {
×
158
                                    // For a query-form view, an unmatched placeholder is not an
159
                                    // error: it becomes a form field the user fills in.
160
                                    if (!view.hasQueryForm()) {
×
161
                                        item.add(new Label("view", "<span class=\"negative\">Error: Query has non-optional parameter</span>").setEscapeModelStrings(false));
×
162
                                        logger.error("Error: Query has non-optional parameter: {} {}", view.getQuery().getQueryId(), p);
×
163
                                        return;
×
164
                                    }
165
                                }
166
                            }
×
167
                            if (view.hasQueryForm()) {
×
168
                                Component form = new QueryFormPanel("view", item.getModelObject(), queryRefParams, null, resourceWithProfile);
×
169
                                form.add(new AttributeAppender("class", " col-" + item.getModelObject().getDisplayWidth()));
×
170
                                item.add(form);
×
171
                                return;
×
172
                            }
173
                            QueryRef queryRef = new QueryRef(view.getQuery().getQueryId(), queryRefParams);
×
174
                            Component resultComponent = QueryResultComponentFactory.build("view", queryRef, item.getModelObject(),
×
175
                                    resourceWithProfile, id, resourceWithProfile.getId(), refRoot);
×
176
                            if (resultComponent != null) {
×
177
                                item.add(resultComponent);
×
178
                            } else if (view.getViewType() != null && View.getSupportedViewTypes().contains(view.getViewType())) {
×
179
                                item.add(new Label("view", "<span class=\"negative\">View type \"" + view.getViewType().stringValue() + "\" is supported but its view is not implemented yet</span>").setEscapeModelStrings(false));
×
180
                                logger.error("View type \"{}\" is supported but its view is not implemented yet", view.getViewType().stringValue());
×
181
                            } else {
182
                                item.add(new Label("view", "<span class=\"negative\">Unsupported view type</span>").setEscapeModelStrings(false));
×
183
                                logger.error("Unsupported view type.");
×
184
                            }
185
                        } catch (Exception ex) {
×
186
                            logger.error("Failed to render view display", ex);
×
187
                            // Guard against a partial add before the failure so we never add a
188
                            // second component with the same id.
189
                            if (item.get("view") == null) {
×
190
                                item.add(new Label("view", "<span class=\"negative\">Error rendering this view</span>").setEscapeModelStrings(false));
×
191
                            }
192
                        }
×
193
                    }
×
194
                });
195
            }
×
196
        });
197

198
        add(new WebMarkupContainer("emptynotice").setVisible(showEmptyNotice && viewDisplays.isEmpty()));
×
199

200
        WebMarkupContainer footerSection = new WebMarkupContainer("footer-section");
×
201
        if (footerAdminButtons != null) {
×
202
            footerSection.add(new ButtonList("footer-buttons",
×
203
                    footerResource != null ? footerResource : resourceWithProfile,
×
204
                    null, null, footerAdminButtons));
205
        } else {
206
            footerSection.setVisible(false);
×
207
            footerSection.add(new Label("footer-buttons").setVisible(false));
×
208
        }
209
        add(footerSection);
×
210

211
        add(new WebMarkupContainer("page-footer").setVisible(false));
×
212
    }
×
213

214
    public void setPageFooter(Component footer) {
215
        replace(footer);
×
216
    }
×
217

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