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

knowledgepixels / nanodash / 23429117751

23 Mar 2026 08:56AM UTC coverage: 16.395% (-0.001%) from 16.396%
23429117751

push

github

tkuhn
fix: show space and maintained resource labels instead of URI suffixes when linked

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

746 of 5613 branches covered (13.29%)

Branch coverage included in aggregate %.

1895 of 10496 relevant lines covered (18.05%)

2.47 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/ReadonlyItem.java
1
package com.knowledgepixels.nanodash.component;
2

3
import com.knowledgepixels.nanodash.LocalUri;
4
import com.knowledgepixels.nanodash.RestrictedChoice;
5
import com.knowledgepixels.nanodash.domain.User;
6
import com.knowledgepixels.nanodash.Utils;
7
import com.knowledgepixels.nanodash.component.StatementItem.RepetitionGroup;
8
import com.knowledgepixels.nanodash.domain.IndividualAgent;
9
import com.knowledgepixels.nanodash.domain.MaintainedResource;
10
import com.knowledgepixels.nanodash.domain.Space;
11
import com.knowledgepixels.nanodash.page.ExplorePage;
12
import com.knowledgepixels.nanodash.repository.MaintainedResourceRepository;
13
import com.knowledgepixels.nanodash.repository.SpaceRepository;
14
import com.knowledgepixels.nanodash.template.ContextType;
15
import com.knowledgepixels.nanodash.template.Template;
16
import com.knowledgepixels.nanodash.template.UnificationException;
17
import net.trustyuri.TrustyUriUtils;
18
import org.apache.commons.codec.Charsets;
19
import org.apache.wicket.behavior.AttributeAppender;
20
import org.apache.wicket.markup.html.basic.Label;
21
import org.apache.wicket.markup.html.link.ExternalLink;
22
import org.apache.wicket.model.IModel;
23
import org.apache.wicket.model.Model;
24
import org.apache.wicket.validation.IValidatable;
25
import org.apache.wicket.validation.IValidator;
26
import org.apache.wicket.validation.Validatable;
27
import org.apache.wicket.validation.ValidationError;
28
import org.eclipse.rdf4j.common.net.ParsedIRI;
29
import org.eclipse.rdf4j.model.IRI;
30
import org.eclipse.rdf4j.model.Literal;
31
import org.eclipse.rdf4j.model.Value;
32
import org.eclipse.rdf4j.model.ValueFactory;
33
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
34
import org.eclipse.rdf4j.model.util.Literals;
35
import org.eclipse.rdf4j.model.vocabulary.XSD;
36
import org.nanopub.Nanopub;
37
import org.nanopub.NanopubUtils;
38
import org.nanopub.SimpleCreatorPattern;
39
import org.nanopub.vocabulary.NTEMPLATE;
40
import org.slf4j.Logger;
41
import org.slf4j.LoggerFactory;
42

43
import java.net.URISyntaxException;
44
import java.net.URLEncoder;
45
import java.util.HashMap;
46
import java.util.Map;
47

48
/**
49
 * ReadonlyItem is a component that displays a read-only item in the form.
50
 */
51
public class ReadonlyItem extends AbstractContextComponent {
52

53
    private static final int LONG_LITERAL_LENGTH = 100;
54
    private static final Logger logger = LoggerFactory.getLogger(ReadonlyItem.class);
×
55
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
×
56

57
    private IModel<String> model;
58
    private String prefix;
59
    private ExternalLink linkComp;
60
    private Label extraComp, languageComp, datatypeComp;
61
    private IModel<String> extraModel, languageModel, datatypeModel;
62
    private IRI iri;
63
    private RestrictedChoice restrictedChoice;
64
    private Label showMoreLabelLiteral, showMoreLabelHTML;
65
    private final Template template;
66

67
    /**
68
     * Constructor for ReadonlyItem.
69
     *
70
     * @param id              the component id
71
     * @param parentId        the parent id (e.g., "subj", "obj")
72
     * @param iriP            the IRI of the item
73
     * @param statementPartId the statement part ID
74
     * @param rg              the repetition group
75
     */
76
    public ReadonlyItem(String id, String parentId, final IRI iriP, IRI statementPartId, final RepetitionGroup rg) {
77
        super(id, rg.getContext());
×
78
        this.iri = iriP;
×
79
        template = context.getTemplate();
×
80
        model = (IModel<String>) context.getComponentModels().get(iri);
×
81
        if (model == null) {
×
82
            model = Model.of("");
×
83
            context.getComponentModels().put(iri, model);
×
84
        }
85
        String postfix = Utils.getUriPostfix(iri);
×
86
        if (context.hasParam(postfix)) {
×
87
            model.setObject(context.getParam(postfix));
×
88
        }
89

90
        final Map<String, String> foafNameMap;
91
        if (context.getExistingNanopub() == null) {
×
92
            foafNameMap = new HashMap<>();
×
93
        } else {
94
            foafNameMap = Utils.getFoafNameMap(context.getExistingNanopub());
×
95
        }
96

97
        prefix = template.getPrefix(iri);
×
98
        if (prefix == null) prefix = "";
×
99
        if (template.isRestrictedChoicePlaceholder(iri)) {
×
100
            restrictedChoice = new RestrictedChoice(iri, context);
×
101
        }
102
        add(new Label("prefix", new Model<String>() {
×
103

104
            @Override
105
            public String getObject() {
106
                String prefixLabel = template.getPrefixLabel(iri);
×
107
                String v = getFullValue();
×
108
                if (prefixLabel == null || IndividualAgent.isUser(v) || foafNameMap.containsKey(v)) {
×
109
                    return "";
×
110
                } else {
111
                    if (!prefixLabel.isEmpty() && parentId.equals("subj") && !prefixLabel.matches("https?://.*")) {
×
112
                        // Capitalize first letter of label if at subject position:
113
                        prefixLabel = prefixLabel.substring(0, 1).toUpperCase() + prefixLabel.substring(1);
×
114
                    }
115
                    return prefixLabel;
×
116
                }
117
            }
118

119
        }));
120

121
        linkComp = new ExternalLink("link", new Model<String>() {
×
122

123
            @Override
124
            public String getObject() {
125
                String obj = getFullValue();
×
126
                if (obj == null) return "";
×
127
                if (obj.equals(LocalUri.of("nanopub").stringValue())) {
×
128
                    if (context.getExistingNanopub() != null) {
×
129
                        obj = context.getExistingNanopub().getUri().stringValue();
×
130
                        return ExplorePage.MOUNT_PATH + "?id=" + URLEncoder.encode(obj, Charsets.UTF_8);
×
131
                    } else {
132
                        return "";
×
133
                    }
134
                } else if (obj.equals(LocalUri.of("assertion").stringValue())) {
×
135
                    if (context.getExistingNanopub() != null) {
×
136
                        obj = context.getExistingNanopub().getAssertionUri().stringValue();
×
137
                        return ExplorePage.MOUNT_PATH + "?id=" + URLEncoder.encode(obj, Charsets.UTF_8);
×
138
                    } else {
139
                        return "";
×
140
                    }
141
                } else if (obj.matches("https?://.+")) {
×
142
                    return NanodashLink.getPageUrl(obj);
×
143
                } else {
144
                    return "";
×
145
                }
146
            }
147

148
        }, new Model<String>() {
×
149

150
            @Override
151
            public String getObject() {
152
                String obj = getFullValue();
×
153
                if (obj != null && obj.matches("https?://.+")) {
×
154
                    IRI objIri = vf.createIRI(obj);
×
155
                    if (iri.equals(NTEMPLATE.CREATOR_PLACEHOLDER)) {
×
156
                        // TODO We might want to introduce a "(you)" flag here at some point
157
                        return User.getShortDisplayName(objIri);
×
158
                    } else if (isAssertionValue(objIri)) {
×
159
                        if (context.getType() == ContextType.ASSERTION) {
×
160
                            return "this assertion";
×
161
                        } else {
162
                            return "the assertion above";
×
163
                        }
164
                    } else if (isNanopubValue(objIri)) {
×
165
                        return "this nanopublication";
×
166
                    } else if (IndividualAgent.isUser(obj)) {
×
167
                        return User.getShortDisplayName(objIri);
×
168
                    } else if (foafNameMap.containsKey(obj)) {
×
169
                        return foafNameMap.get(obj);
×
170
                    }
171
                    Space space = SpaceRepository.get().findById(obj);
×
172
                    if (space != null) return space.getLabel();
×
173
                    space = SpaceRepository.get().findByAltId(obj);
×
174
                    if (space != null) return space.getLabel();
×
175
                    MaintainedResource mr = MaintainedResourceRepository.get().findById(obj);
×
176
                    if (mr != null) return mr.getLabel();
×
177
                    return getLabelString(objIri);
×
178
                }
179
                return obj;
×
180
            }
181

182
        });
183
        if (template.isIntroducedResource(iri) || template.isEmbeddedResource(iri)) {
×
184
            linkComp.add(AttributeAppender.append("class", "introduced"));
×
185
        }
186
        add(linkComp);
×
187
        add(new Label("description", new Model<String>() {
×
188

189
            @Override
190
            public String getObject() {
191
                String obj = getFullValue();
×
192
                if (obj != null && obj.matches("https?://.+")) {
×
193
                    IRI objIri = vf.createIRI(obj);
×
194
                    if (isAssertionValue(objIri)) {
×
195
                        return "This is the identifier for the assertion of this nanopublication.";
×
196
                    } else if (isNanopubValue(objIri)) {
×
197
                        return "This is the identifier for this whole nanopublication.";
×
198
                    } else if (context.isReadOnly() && obj.startsWith(context.getExistingNanopub().getUri().stringValue())) {
×
199
                        return "This is a local identifier minted within the nanopublication.";
×
200
                    }
201
                    String labelString = getLabelString(objIri);
×
202
                    String description = "";
×
203
                    if (labelString.contains(" - ")) description = labelString.replaceFirst("^.* - ", "");
×
204
                    return description;
×
205
                } else if (obj != null && obj.startsWith("\"")) {
×
206
                    return "(this is a literal)";
×
207
                }
208
                return "";
×
209
            }
210

211
        }));
212
        Model<String> uriModel = new Model<String>() {
×
213

214
            @Override
215
            public String getObject() {
216
                String obj = getFullValue();
×
217
                if (obj != null && obj.startsWith("\"")) return "";
×
218
                if (isAssertionValue(obj)) {
×
219
                    return getAssertionValue();
×
220
                } else if (isNanopubValue(obj)) {
×
221
                    return getNanopubValue();
×
222
                }
223
                return obj;
×
224
            }
225

226
        };
227
        add(Utils.getUriLink("uri", uriModel));
×
228
        extraModel = Model.of("");
×
229
        extraComp = new Label("extra", extraModel);
×
230
        extraComp.setVisible(false);
×
231
        add(extraComp);
×
232
        languageModel = Model.of("");
×
233
        languageComp = new Label("language", languageModel);
×
234
        languageComp.setVisible(false);
×
235
        add(languageComp);
×
236
        datatypeModel = Model.of("");
×
237
        datatypeComp = new Label("datatype", datatypeModel);
×
238
        datatypeComp.setVisible(false);
×
239
        add(datatypeComp);
×
240

241
        showMoreLabelLiteral = new Label("show-more-literal", "");
×
242
        add(showMoreLabelLiteral);
×
243
        showMoreLabelLiteral.setVisible(false);
×
244

245
        showMoreLabelHTML = new Label("show-more-html", "");
×
246
        add(showMoreLabelHTML);
×
247
        showMoreLabelHTML.setVisible(false);
×
248
    }
×
249

250
    /**
251
     * {@inheritDoc}
252
     */
253
    @Override
254
    public void fillFinished() {
255
        String obj = getFullValue();
×
256
        if (obj != null) {
×
257
            if (isAssertionValue(obj)) {
×
258
                linkComp.add(new AttributeAppender("class", "this-assertion"));
×
259
            } else if (isNanopubValue(obj)) {
×
260
                linkComp.add(new AttributeAppender("class", "this-nanopub"));
×
261
            } else if (context.getExistingNanopub() != null) {
×
262
                Nanopub np = context.getExistingNanopub();
×
263
                if (NanopubUtils.getIntroducedIriIds(np).contains(obj) || NanopubUtils.getEmbeddedIriIds(np).contains(obj)) {
×
264
                    linkComp.add(AttributeAppender.append("class", "introduced"));
×
265
                }
266
            }
267
        }
268
    }
×
269

270
    /**
271
     * {@inheritDoc}
272
     */
273
    @Override
274
    public void finalizeValues() {
275
    }
×
276

277
    private String getLabelString(IRI iri) {
278
        if (template.getLabel(iri) != null) {
×
279
            return template.getLabel(iri).replaceFirst(" - .*$", "");
×
280
        } else if (context.getLabel(iri) != null) {
×
281
            return context.getLabel(iri).replaceFirst(" - .*$", "");
×
282
        } else {
283
            return Utils.getShortNameFromURI(iri.stringValue());
×
284
        }
285
    }
286

287
    /**
288
     * {@inheritDoc}
289
     */
290
    @Override
291
    public void removeFromContext() {
292
        // Nothing to be done here.
293
    }
×
294

295
    private String getFullValue() {
296
        String s = model.getObject();
×
297
        if (s == null) return null;
×
298
        if (template.isAutoEscapePlaceholder(iri)) {
×
299
            s = Utils.urlEncode(s);
×
300
        }
301
        if (!prefix.isEmpty()) {
×
302
            s = prefix + s;
×
303
        }
304
        return s;
×
305
    }
306

307
    private boolean isNanopubValue(Object obj) {
308
        if (obj == null) return false;
×
309
        if (obj.toString().equals(LocalUri.of("nanopub").stringValue())) return true;
×
310
        if (context.getExistingNanopub() == null) return false;
×
311
        return obj.toString().equals(context.getExistingNanopub().getUri().stringValue());
×
312
    }
313

314
    private String getNanopubValue() {
315
        if (context.getExistingNanopub() != null) {
×
316
            return context.getExistingNanopub().getUri().stringValue();
×
317
        } else {
318
            return LocalUri.of("nanopub").stringValue();
×
319
        }
320
    }
321

322
    private boolean isAssertionValue(Object obj) {
323
        if (obj == null) return false;
×
324
        if (obj.toString().equals(LocalUri.of("assertion").stringValue())) return true;
×
325
        if (context.getExistingNanopub() == null) return false;
×
326
        return obj.toString().equals(context.getExistingNanopub().getAssertionUri().stringValue());
×
327
    }
328

329
    private String getAssertionValue() {
330
        if (context.getExistingNanopub() != null) {
×
331
            return context.getExistingNanopub().getAssertionUri().stringValue();
×
332
        } else {
333
            return LocalUri.of("assertion").stringValue();
×
334
        }
335
    }
336

337
    /**
338
     * {@inheritDoc}
339
     */
340
    @Override
341
    public boolean isUnifiableWith(Value v) {
342
        if (v == null) return true;
×
343
        if (v instanceof IRI) {
×
344
            String vs = v.stringValue();
×
345
            if (vs.equals(LocalUri.of("nanopub").stringValue())) {
×
346
                vs = getNanopubValue();
×
347
            } else if (vs.equals(LocalUri.of("assertion").stringValue())) {
×
348
                vs = getAssertionValue();
×
349
            }
350
            if (vs.startsWith(prefix)) vs = vs.substring(prefix.length());
×
351
//                        if (Utils.isLocalURI(vs)) vs = vs.replaceFirst("^" + LocalUri.PREFIX, "");
352
            if (template.isAutoEscapePlaceholder(iri)) {
×
353
                vs = Utils.urlDecode(vs);
×
354
            }
355
            Validatable<String> validatable = new Validatable<>(vs);
×
356
//                        if (template.isLocalResource(iri) && !Utils.isUriPostfix(vs)) {
357
//                                vs = Utils.getUriPostfix(vs);
358
//                        }
359
            new Validator().validate(validatable);
×
360
            if (!validatable.isValid()) {
×
361
                return false;
×
362
            }
363
            if (model.getObject().isEmpty()) {
×
364
                return true;
×
365
            }
366
            return vs.equals(model.getObject());
×
367
        } else if (v instanceof Literal vL) {
×
368
            if (template.getRegex(iri) != null && !v.stringValue().matches(template.getRegex(iri))) {
×
369
                return false;
×
370
            }
371
            String languagetag = template.getLanguageTag(iri);
×
372
            IRI datatype = template.getDatatype(iri);
×
373
            if (languagetag != null) {
×
374
                if (vL.getLanguage().isEmpty() || !Literals.normalizeLanguageTag(vL.getLanguage().get()).equals(languagetag)) {
×
375
                    return false;
×
376
                }
377
            } else if (datatype != null) {
×
378
                if (!vL.getDatatype().equals(datatype)) {
×
379
                    return false;
×
380
                }
381
            }
382
            if (linkComp.getDefaultModelObject() == null || linkComp.getDefaultModelObject().toString().isEmpty()) {
×
383
                return true;
×
384
            }
385
            return linkComp.getDefaultModelObject().equals("\"" + v.stringValue() + "\"");
×
386
        }
387
        return false;
×
388
    }
389

390
    /**
391
     * {@inheritDoc}
392
     */
393
    @Override
394
    public void unifyWith(Value v) throws UnificationException {
395
        if (v == null) return;
×
396
        String vs = v.stringValue();
×
397
        if (!isUnifiableWith(v)) {
×
398
            logger.error("Cannot unify {}", v);
×
399
            throw new UnificationException(vs);
×
400
        }
401
        if (v instanceof IRI) {
×
402
            if (vs.equals(LocalUri.of("nanopub").stringValue())) {
×
403
                vs = getNanopubValue();
×
404
            } else if (vs.equals(LocalUri.of("assertion").stringValue())) {
×
405
                vs = getAssertionValue();
×
406
            }
407
            if (!prefix.isEmpty() && vs.startsWith(prefix)) {
×
408
                vs = vs.substring(prefix.length());
×
409
                // With read-only items, we don't need preliminary local identifiers:
410
//                        } else if (Utils.isLocalURI(vs)) {
411
//                                vs = vs.replaceFirst("^" + LocalUri.PREFIX, "");
412
//                        } else if (template.isLocalResource(iri) && !Utils.isUriPostfix(vs)) {
413
//                                vs = Utils.getUriPostfix(vs);
414
            }
415
            if (template.isAutoEscapePlaceholder(iri)) {
×
416
                vs = Utils.urlDecode(vs);
×
417
            }
418
            model.setObject(vs);
×
419
        } else if (v instanceof Literal vL) {
×
420
            if (vs.length() >= LONG_LITERAL_LENGTH) {
×
421
                linkComp.add(AttributeAppender.append("class", "long-literal collapsed"));
×
422
                showMoreLabelLiteral.setVisible(true);
×
423
            }
424
            if (vL.getLanguage().isPresent()) {
×
425
                model.setObject("\"" + vs + "\"");
×
426
                languageModel.setObject("(" + Literals.normalizeLanguageTag(vL.getLanguage().get()) + ")");
×
427
                languageComp.setVisible(true);
×
428
            } else if (!vL.getDatatype().equals(XSD.STRING)) {
×
429
                model.setObject("\"" + vs + "\"");
×
430
                datatypeModel.setObject("(" + vL.getDatatype().stringValue().replace(XSD.NAMESPACE, "xsd:") + ")");
×
431
                datatypeComp.setVisible(true);
×
432
            } else {
433
                model.setObject("\"" + vs + "\"");
×
434
            }
435
            if (Utils.looksLikeHtml(vs)) {
×
436
                linkComp.setVisible(false);
×
437
                extraModel.setObject(Utils.sanitizeHtml(vs));
×
438
                extraComp.setEscapeModelStrings(false);
×
439
                extraComp.setVisible(true);
×
440
                showMoreLabelLiteral.setVisible(false);
×
441
                showMoreLabelHTML.setVisible(true);
×
442
            }
443
        }
444
    }
×
445

446
    /**
447
     * Validator class for validating the input.
448
     */
449
    protected class Validator extends InvalidityHighlighting implements IValidator<String> {
450

451
        /**
452
         * Default constructor for Validator.
453
         */
454
        public Validator() {
×
455
        }
×
456

457
        @Override
458
        public void validate(IValidatable<String> s) {
459
            String sv = s.getValue();
×
460
            String p = prefix;
×
461
            if (template.isAutoEscapePlaceholder(iri)) {
×
462
                sv = Utils.urlEncode(sv);
×
463
            }
464
            if (sv.matches("https?://.+")) {
×
465
                p = "";
×
466
            } else if (sv.contains(":")) {
×
467
                s.error(new ValidationError("Colon character is not allowed in postfix"));
×
468
            }
469
            String iriString = p + sv;
×
470
            if (iriString.matches("[^:# ]+")) {
×
471
                p = LocalUri.PREFIX;
×
472
                iriString = p + sv;
×
473
            }
474
            try {
475
                ParsedIRI piri = new ParsedIRI(iriString);
×
476
                if (!piri.isAbsolute()) {
×
477
                    s.error(new ValidationError("IRI not well-formed"));
×
478
                }
479
                if (p.isEmpty() && !Utils.isLocalURI(sv) && !sv.matches("https?://.+")) {
×
480
                    s.error(new ValidationError("Only http(s):// IRIs are allowed here"));
×
481
                }
482
            } catch (URISyntaxException ex) {
×
483
                s.error(new ValidationError("IRI not well-formed"));
×
484
            }
×
485
            String regex = template.getRegex(iri);
×
486
            if (regex != null) {
×
487
                if (!sv.matches(regex)) {
×
488
                    s.error(new ValidationError("Value '" + sv + "' doesn't match the pattern '" + regex + "'"));
×
489
                }
490
            }
491
            if (template.isRestrictedChoicePlaceholder(iri)) {
×
492
                if (!restrictedChoice.getPossibleValues().contains(iriString) && !restrictedChoice.hasPossibleRefValues()) {
×
493
                    // not checking the possible ref values can overgenerate, but normally works
494
                    s.error(new ValidationError("Invalid choice"));
×
495
                }
496
            }
497
            if (template.isExternalUriPlaceholder(iri)) {
×
498
                if (!iriString.matches("https?://.+")) {
×
499
                    s.error(new ValidationError("Not an external IRI"));
×
500
                }
501
            }
502
            if (template.isTrustyUriPlaceholder(iri)) {
×
503
                if (!TrustyUriUtils.isPotentialTrustyUri(iriString)) {
×
504
                    s.error(new ValidationError("Not a trusty URI"));
×
505
                }
506
            }
507
            if (iri.equals(NTEMPLATE.CREATOR_PLACEHOLDER) && context.getExistingNanopub() != null) {
×
508
                boolean found = false;
×
509
                for (IRI creator : SimpleCreatorPattern.getCreators(context.getExistingNanopub())) {
×
510
                    if (creator.stringValue().equals(iriString)) {
×
511
                        found = true;
×
512
                        break;
×
513
                    }
514
                }
×
515
                if (!found) {
×
516
                    s.error(new ValidationError("Not a creator of nanopub"));
×
517
                }
518
            }
519
        }
×
520

521
    }
522

523
    /**
524
     * <p>toString.</p>
525
     *
526
     * @return a {@link java.lang.String} object
527
     */
528
    public String toString() {
529
        return "[read-only IRI item: " + iri + "]";
×
530
    }
531

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