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

knowledgepixels / nanodash / 30350726166

28 Jul 2026 10:25AM UTC coverage: 33.442% (+3.1%) from 30.298%
30350726166

push

github

web-flow
Merge pull request #567 from knowledgepixels/feat/reject-literals-in-subject-predicate

Report and block templates with literals in subject/predicate position

2337 of 7695 branches covered (30.37%)

Branch coverage included in aggregate %.

4532 of 12845 relevant lines covered (35.28%)

5.43 hits per line

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

80.49
src/main/java/com/knowledgepixels/nanodash/template/Template.java
1
package com.knowledgepixels.nanodash.template;
2

3
import com.knowledgepixels.nanodash.LookupApis;
4
import com.knowledgepixels.nanodash.Utils;
5
import net.trustyuri.TrustyUriUtils;
6
import org.eclipse.rdf4j.common.exception.RDF4JException;
7
import org.eclipse.rdf4j.model.*;
8
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
9
import org.eclipse.rdf4j.model.util.Literals;
10
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
11
import org.eclipse.rdf4j.model.vocabulary.RDF;
12
import org.eclipse.rdf4j.model.vocabulary.RDFS;
13
import org.eclipse.rdf4j.model.vocabulary.SHACL;
14
import org.nanopub.Nanopub;
15
import org.nanopub.NanopubUtils;
16
import org.nanopub.vocabulary.NTEMPLATE;
17
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
18
import org.slf4j.Logger;
19
import org.slf4j.LoggerFactory;
20

21
import java.io.Serializable;
22
import java.util.*;
23

24
/**
25
 * Represents a template for creating nanopublications.
26
 */
27
public class Template implements Serializable {
28

29
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
6✔
30
    private static final Logger logger = LoggerFactory.getLogger(Template.class);
9✔
31

32
    /**
33
     * Default target namespace for templates.
34
     */
35
    public static final String DEFAULT_TARGET_NAMESPACE = "https://w3id.org/np/";
36

37
    // TODO Move these to the other ntemplate vocabulary terms in nanopub-java:
38
    private static final IRI ADVANCED_STATEMENT = vf.createIRI("https://w3id.org/np/o/ntemplate/AdvancedStatement");
12✔
39

40
    /**
41
     * Type of a literal placeholder whose language tag is selected by the user at fill time.
42
     */
43
    public static final IRI LANGUAGE_TAGGED_LITERAL_PLACEHOLDER = vf.createIRI("https://w3id.org/np/o/ntemplate/LanguageTaggedLiteralPlaceholder");
12✔
44

45
    /**
46
     * Predicate restricting the language tags offered by a language-tag picker.
47
     */
48
    public static final IRI POSSIBLE_LANGUAGE_TAG = vf.createIRI("https://w3id.org/np/o/ntemplate/possibleLanguageTag");
15✔
49

50
    private final Nanopub nanopub;
51
    private String label;
52
    private String description;
53

54
    // TODO: Make all these maps more generic and the code simpler:
55
    private IRI templateIri;
56
    private boolean embeddedIdentity = false;
9✔
57
    private IRI templateKindIri;
58
    private IRI governingSpace;
59
    private Map<IRI, List<IRI>> typeMap = new HashMap<>();
15✔
60
    private Map<IRI, List<Value>> possibleValueMap = new HashMap<>();
15✔
61
    private Map<IRI, List<IRI>> possibleValuesToLoadMap = new HashMap<>();
15✔
62
    private Map<IRI, List<String>> apiMap = new HashMap<>();
15✔
63
    private Map<IRI, String> labelMap = new HashMap<>();
15✔
64
    private Map<IRI, IRI> datatypeMap = new HashMap<>();
15✔
65
    private Map<IRI, String> languageTagMap = new HashMap<>();
15✔
66
    private Map<IRI, List<String>> possibleLanguageTagMap = new HashMap<>();
15✔
67
    private Map<IRI, String> prefixMap = new HashMap<>();
15✔
68
    private Map<IRI, String> prefixLabelMap = new HashMap<>();
15✔
69
    private Map<IRI, String> regexMap = new HashMap<>();
15✔
70
    private Map<IRI, List<IRI>> statementMap = new HashMap<>();
15✔
71
    private Map<IRI, IRI> statementSubjects = new HashMap<>();
15✔
72
    private Map<IRI, IRI> statementPredicates = new HashMap<>();
15✔
73
    private Map<IRI, Value> statementObjects = new HashMap<>();
15✔
74
    // Subject/predicate values that are not IRIs (i.e. literals), which is invalid RDF but
75
    // does occur in published templates. Kept apart from the maps above so the rest of the
76
    // code keeps seeing IRIs, and reported by getStatementErrors().
77
    private Map<IRI, Value> nonIriStatementSubjects = new HashMap<>();
15✔
78
    private Map<IRI, Value> nonIriStatementPredicates = new HashMap<>();
15✔
79
    private Map<IRI, Integer> statementOrder = new HashMap<>();
15✔
80
    private IRI defaultProvenance;
81
    private List<IRI> requiredPubInfoElements = new ArrayList<>();
15✔
82
    private String tag = null;
9✔
83
    private Map<IRI, Value> defaultValues = new HashMap<>();
15✔
84
    private String targetNamespace = null;
9✔
85
    private String nanopubLabelPattern;
86
    private List<IRI> targetNanopubTypes = new ArrayList<>();
15✔
87

88
    /**
89
     * Creates a Template object from a template id.
90
     *
91
     * @param templateId the id of the template, which is the URI of a nanopublication that contains the template definition.
92
     * @throws RDF4JException             if there is an error retrieving the nanopublication.
93
     * @throws MalformedTemplateException if the template is malformed or not a valid nanopub template.
94
     */
95
    Template(String templateId) throws RDF4JException, MalformedTemplateException {
96
        this(Utils.getNanopub(templateId));
12✔
97
    }
3✔
98

99
    /**
100
     * Creates a Template object from a Nanopub object.
101
     *
102
     * @param np the Nanopub object containing the template definition.
103
     * @throws MalformedTemplateException if the template is malformed or not a valid nanopub template.
104
     */
105
    Template(Nanopub np) throws MalformedTemplateException {
6✔
106
        nanopub = np;
9✔
107
        processTemplate(nanopub);
12✔
108
    }
3✔
109

110
    /**
111
     * Checks if the template is unlisted.
112
     *
113
     * @return true if the template is unlisted, false otherwise.
114
     */
115
    public boolean isUnlisted() {
116
        List<IRI> types = typeMap.get(templateIri);
21✔
117
        return types != null && types.contains(NTEMPLATE.UNLISTED_TEMPLATE);
30!
118
    }
119

120
    /**
121
     * Returns the Nanopub object representing the template.
122
     *
123
     * @return the Nanopub object of the template.
124
     */
125
    public Nanopub getNanopub() {
126
        return nanopub;
9✔
127
    }
128

129
    /**
130
     * Returns the ID of the template: for a template with embedded identity (the
131
     * template node is a regular embedded IRI in the assertion), that embedded IRI;
132
     * for a legacy template (the template node is the assertion graph URI), the URI
133
     * of the nanopublication.
134
     *
135
     * @return the ID of the template as a string.
136
     */
137
    public String getId() {
138
        if (embeddedIdentity) return templateIri.stringValue();
21✔
139
        return nanopub.getUri().toString();
15✔
140
    }
141

142
    /**
143
     * Checks whether the given ID refers to this template, in either form: the
144
     * template's canonical ID ({@link #getId()}) or its nanopublication URI. Use
145
     * this instead of comparing against {@link #getId()} when the ID to compare
146
     * comes from outside (page parameters, published references), as those may
147
     * carry either form.
148
     *
149
     * @param id the ID to check
150
     * @return true if the ID refers to this template
151
     */
152
    public boolean hasId(String id) {
153
        return id != null && (id.equals(getId()) || id.equals(nanopub.getUri().stringValue()));
54!
154
    }
155

156
    /**
157
     * Returns the template kind IRI, i.e. the stable identifier this template
158
     * version declares itself a version of ({@code dct:isVersionOf} on the template
159
     * node), or null if it doesn't declare one.
160
     *
161
     * @return the template kind IRI, or null
162
     */
163
    public IRI getTemplateKindIri() {
164
        return templateKindIri;
9✔
165
    }
166

167
    /**
168
     * Returns the space governing this template version ({@code gen:governedBy} on
169
     * the template node), or null if it doesn't declare one. A governed version's
170
     * latest-resolution floats space-based within its (kind, space) pair instead of
171
     * following the supersedes chain; see docs/template-identity-and-governance.md.
172
     *
173
     * @return the governing space IRI, or null
174
     */
175
    public IRI getGoverningSpace() {
176
        return governingSpace;
9✔
177
    }
178

179
    /**
180
     * Returns the label of the template.
181
     *
182
     * @return the label of the template, or a default label if not set.
183
     */
184
    public String getLabel() {
185
        if (label == null) {
9!
186
            return "Template " + TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue()).substring(0, 10);
×
187
        }
188
        return label;
9✔
189
    }
190

191
    /**
192
     * Returns the description of the template.
193
     *
194
     * @return the description of the template, or an empty string if not set.
195
     */
196
    public String getDescription() {
197
        return description;
9✔
198
    }
199

200
    /**
201
     * Returns the IRI of the template.
202
     *
203
     * @param iri the IRI to transform.
204
     * @return the transformed IRI, or the original IRI if no transformation is needed.
205
     */
206
    public String getLabel(IRI iri) {
207
        iri = transform(iri);
12✔
208
        return labelMap.get(iri);
18✔
209
    }
210

211
    /**
212
     * Returns the IRI of the template, transforming it if necessary.
213
     *
214
     * @param iri the IRI to transform.
215
     * @return the transformed IRI, or the original IRI if no transformation is needed.
216
     */
217
    public IRI getFirstOccurrence(IRI iri) {
218
        iri = transform(iri);
12✔
219
        for (IRI i : getStatementIris()) {
33!
220
            if (statementMap.containsKey(i)) {
15✔
221
                // grouped statement
222
                for (IRI g : getStatementIris(i)) {
36✔
223
                    if (iri.equals(statementSubjects.get(g))) return g;
21!
224
                    if (iri.equals(statementPredicates.get(g))) return g;
27✔
225
                    if (iri.equals(statementObjects.get(g))) return g;
27✔
226
                }
6✔
227
            } else {
228
                // non-grouped statement
229
                if (iri.equals(statementSubjects.get(i))) return i;
27✔
230
                if (iri.equals(statementPredicates.get(i))) return i;
27✔
231
                if (iri.equals(statementObjects.get(i))) return i;
27✔
232
            }
233
        }
3✔
234
        return null;
×
235
    }
236

237
    /**
238
     * Returns the datatype for the given literal placeholder IRI.
239
     *
240
     * @param iri the literal placeholder IRI.
241
     * @return the datatype for the literal.
242
     */
243
    public IRI getDatatype(IRI iri) {
244
        iri = transform(iri);
12✔
245
        return datatypeMap.get(iri);
18✔
246
    }
247

248
    /**
249
     * Returns the language tag for the given literal placeholder IRI.
250
     *
251
     * @param iri the literal placeholder IRI.
252
     * @return the language tag for the literal.
253
     */
254
    public String getLanguageTag(IRI iri) {
255
        iri = transform(iri);
12✔
256
        return languageTagMap.get(iri);
18✔
257
    }
258

259
    /**
260
     * Returns the language tags a language-tag-selectable placeholder is restricted to.
261
     *
262
     * @param iri the literal placeholder IRI.
263
     * @return the normalized allowed language tags, or null if unrestricted.
264
     */
265
    public List<String> getPossibleLanguageTags(IRI iri) {
266
        iri = transform(iri);
12✔
267
        return possibleLanguageTagMap.get(iri);
18✔
268
    }
269

270
    /**
271
     * Returns the prefix for the given IRI.
272
     *
273
     * @param iri the IRI.
274
     * @return the prefix for the IRI.
275
     */
276
    public String getPrefix(IRI iri) {
277
        iri = transform(iri);
12✔
278
        return prefixMap.get(iri);
18✔
279
    }
280

281
    /**
282
     * Returns the prefix label for a given IRI.
283
     *
284
     * @param iri the IRI to get the prefix label for.
285
     * @return the prefix label for the IRI, or null if not found.
286
     */
287
    public String getPrefixLabel(IRI iri) {
288
        iri = transform(iri);
12✔
289
        return prefixLabelMap.get(iri);
18✔
290
    }
291

292
    /**
293
     * Returns the regex pattern for a given IRI.
294
     *
295
     * @param iri the IRI to get the regex for.
296
     * @return the regex pattern for the IRI, or null if not found.
297
     */
298
    public String getRegex(IRI iri) {
299
        iri = transform(iri);
12✔
300
        return regexMap.get(iri);
18✔
301
    }
302

303
    /**
304
     * Transforms an IRI by removing the artifact code if it is present.
305
     *
306
     * @param iri the IRI to transform.
307
     * @return the transformed IRI, or the original IRI if no transformation is needed.
308
     */
309
    public Value getDefault(IRI iri) {
310
        if (iri.stringValue().matches(".*__[0-9]+")) {
15✔
311
            String baseIri = iri.stringValue().replaceFirst("__[0-9]+$", "");
18✔
312
            Value v = defaultValues.get(vf.createIRI(baseIri));
24✔
313
            if (v instanceof IRI vIri) {
9!
314
                // Append repeat suffix only for local URIs; fixed URIs stay unchanged
315
                if (vIri.stringValue().startsWith(nanopub.getUri().stringValue())) {
×
316
                    int repeatSuffix = Integer.parseInt(iri.stringValue().replaceFirst("^.*__([0-9]+)$", "$1"));
×
317
                    return vf.createIRI(vIri.stringValue() + (repeatSuffix + 1));
×
318
                }
319
                return vIri;
×
320
            }
321
        }
322
        iri = transform(iri);
12✔
323
        return defaultValues.get(iri);
18✔
324
    }
325

326
    /**
327
     * Returns the statement IRIs associated with the template.
328
     *
329
     * @return the list of statement IRIs for the template.
330
     */
331
    public List<IRI> getStatementIris() {
332
        return statementMap.get(templateIri);
21✔
333
    }
334

335
    /**
336
     * Returns the statement IRIs associated with a specific group IRI.
337
     *
338
     * @param groupIri the IRI of the group for which to retrieve statement IRIs.
339
     * @return the list of statement IRIs for the specified group IRI, or null if no statements are associated with that group.
340
     */
341
    public List<IRI> getStatementIris(IRI groupIri) {
342
        return statementMap.get(groupIri);
18✔
343
    }
344

345
    /**
346
     * Returns the subject, predicate, and object of a statement given its IRI.
347
     *
348
     * @param statementIri the IRI of the statement to retrieve.
349
     * @return the subject, predicate, and object of the statement as a triple.
350
     */
351
    public IRI getSubject(IRI statementIri) {
352
        return statementSubjects.get(statementIri);
18✔
353
    }
354

355
    /**
356
     * Returns the predicate of a statement given its IRI.
357
     *
358
     * @param statementIri the IRI of the statement to retrieve.
359
     * @return the predicate of the statement, or null if not found.
360
     */
361
    public IRI getPredicate(IRI statementIri) {
362
        return statementPredicates.get(statementIri);
18✔
363
    }
364

365
    /**
366
     * Returns the object of a statement given its IRI.
367
     *
368
     * @param statementIri the IRI of the statement to retrieve.
369
     * @return the object of the statement, or null if not found.
370
     */
371
    public Value getObject(IRI statementIri) {
372
        return statementObjects.get(statementIri);
18✔
373
    }
374

375
    /**
376
     * Returns the subject of a statement as the template declares it, which may be a literal
377
     * and therefore invalid in that position (in which case {@link #getSubject(IRI)} is null).
378
     * Use this for rendering, so that an invalid subject is shown to the user instead of
379
     * silently disappearing; see {@link #getStatementErrors()}.
380
     *
381
     * @param statementIri the IRI of the statement to retrieve.
382
     * @return the declared subject of the statement, or null if the statement declares none.
383
     */
384
    public Value getDeclaredSubject(IRI statementIri) {
385
        IRI subj = statementSubjects.get(statementIri);
18✔
386
        if (subj != null) return subj;
12✔
387
        return nonIriStatementSubjects.get(statementIri);
18✔
388
    }
389

390
    /**
391
     * Returns the predicate of a statement as the template declares it, which may be a literal
392
     * and therefore invalid in that position (in which case {@link #getPredicate(IRI)} is null).
393
     * See {@link #getDeclaredSubject(IRI)}.
394
     *
395
     * @param statementIri the IRI of the statement to retrieve.
396
     * @return the declared predicate of the statement, or null if the statement declares none.
397
     */
398
    public Value getDeclaredPredicate(IRI statementIri) {
399
        IRI pred = statementPredicates.get(statementIri);
18✔
400
        if (pred != null) return pred;
12✔
401
        return nonIriStatementPredicates.get(statementIri);
18✔
402
    }
403

404
    /**
405
     * Returns human-readable descriptions of the statements of this template that cannot
406
     * produce valid RDF, because their subject or predicate is a literal, is a placeholder
407
     * that can only be filled with a literal, or is missing altogether. Only URIs are allowed
408
     * in subject and predicate position.
409
     *
410
     * @return the error descriptions, empty if all statements are well-formed.
411
     */
412
    public List<String> getStatementErrors() {
413
        List<String> errors = new ArrayList<>();
12✔
414
        List<IRI> topIris = getStatementIris();
9✔
415
        if (topIris == null) return errors;
6!
416
        int i = 0;
6✔
417
        for (IRI top : topIris) {
30✔
418
            i++;
3✔
419
            List<IRI> memberIris = getStatementIris(top);
12✔
420
            if (memberIris == null) {
6✔
421
                collectStatementErrors(top, "Statement " + i, errors);
21✔
422
            } else {
423
                int j = 0;
6✔
424
                for (IRI member : memberIris) {
30✔
425
                    j++;
3✔
426
                    collectStatementErrors(member, "Statement " + i + "." + j, errors);
21✔
427
                }
3✔
428
            }
429
        }
3✔
430
        return errors;
6✔
431
    }
432

433
    private void collectStatementErrors(IRI statementIri, String statementNumber, List<String> errors) {
434
        String statementName = statementNumber + " (" + getLocalName(statementIri) + ")";
18✔
435
        collectPositionError(getDeclaredSubject(statementIri), "subject", statementName, errors);
24✔
436
        collectPositionError(getDeclaredPredicate(statementIri), "predicate", statementName, errors);
24✔
437
    }
3✔
438

439
    private void collectPositionError(Value value, String position, String statementName, List<String> errors) {
440
        if (value == null) {
6✔
441
            errors.add(statementName + ": no " + position + " is defined.");
21✔
442
        } else if (value instanceof Literal) {
9✔
443
            errors.add(statementName + ": the literal " + describeValue(value) + " is used in " + position +
30✔
444
                    " position, but only URIs are allowed there.");
445
        } else if (isLiteralPlaceholder((IRI) value)) {
15✔
446
            errors.add(statementName + ": the literal placeholder " + describeValue(value) + " is used in " + position +
27✔
447
                    " position, but only URIs are allowed there.");
448
        }
449
    }
3✔
450

451
    private String describeValue(Value value) {
452
        if (value instanceof Literal) return "\"" + value.stringValue() + "\"";
21✔
453
        String label = getLabel((IRI) value);
15✔
454
        String localName = getLocalName(value);
12✔
455
        if (label == null) return localName;
6!
456
        return "\"" + label + "\" (" + localName + ")";
12✔
457
    }
458

459
    private String getLocalName(Value value) {
460
        return value.stringValue().replaceFirst("^.*[/#]", "");
18✔
461
    }
462

463
    /**
464
     * Checks if the template is a local resource.
465
     *
466
     * @param iri the IRI to check.
467
     * @return true if the IRI is a local resource, false otherwise.
468
     */
469
    public boolean isLocalResource(IRI iri) {
470
        iri = transform(iri);
12✔
471
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.LOCAL_RESOURCE);
51✔
472
    }
473

474
    /**
475
     * Checks if the template is an introduced resource.
476
     *
477
     * @param iri the IRI to check.
478
     * @return true if the IRI is an introduced resource, false otherwise.
479
     */
480
    public boolean isIntroducedResource(IRI iri) {
481
        iri = transform(iri);
12✔
482
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.INTRODUCED_RESOURCE);
51✔
483
    }
484

485
    /**
486
     * Returns the role-instantiation direction pin declared on a (predicate) IRI, if
487
     * any: {@link KPXL_TERMS#INVERSE_ROLE_PROPERTY} or
488
     * {@link KPXL_TERMS#REGULAR_ROLE_PROPERTY}. Templates declare the pin as an
489
     * {@code rdf:type} on the predicate (constant or placeholder); nanodash lifts it into
490
     * pubinfo at publish time so nanopub-query can resolve a custom role predicate's
491
     * direction (see #525).
492
     *
493
     * @param iri the IRI to check (a predicate constant or placeholder).
494
     * @return the pin class, or null if the IRI carries no role-direction pin.
495
     */
496
    public IRI getRoleDirectionPin(IRI iri) {
497
        iri = transform(iri);
12✔
498
        if (!typeMap.containsKey(iri)) return null;
21✔
499
        List<IRI> types = typeMap.get(iri);
18✔
500
        if (types.contains(KPXL_TERMS.INVERSE_ROLE_PROPERTY)) return KPXL_TERMS.INVERSE_ROLE_PROPERTY;
12!
501
        if (types.contains(KPXL_TERMS.REGULAR_ROLE_PROPERTY)) return KPXL_TERMS.REGULAR_ROLE_PROPERTY;
12!
502
        return null;
6✔
503
    }
504

505
    /**
506
     * Checks if the template is an embedded resource.
507
     *
508
     * @param iri the IRI to check.
509
     * @return true if the IRI is an embedded resource, false otherwise.
510
     */
511
    public boolean isEmbeddedResource(IRI iri) {
512
        iri = transform(iri);
12✔
513
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.EMBEDDED_RESOURCE);
45!
514
    }
515

516
    /**
517
     * Checks if the IRI is a value placeholder.
518
     *
519
     * @param iri the IRI to check.
520
     * @return true if the IRI is a value placeholder, false otherwise.
521
     */
522
    public boolean isValuePlaceholder(IRI iri) {
523
        iri = transform(iri);
12✔
524
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.VALUE_PLACEHOLDER);
21!
525
    }
526

527
    /**
528
     * Checks if the IRI is a URI placeholder.
529
     *
530
     * @param iri the IRI to check.
531
     * @return true if the IRI is a URI placeholder, false otherwise.
532
     */
533
    public boolean isUriPlaceholder(IRI iri) {
534
        iri = transform(iri);
12✔
535
        if (!typeMap.containsKey(iri)) return false;
21✔
536
        for (IRI t : typeMap.get(iri)) {
42✔
537
            if (t.equals(NTEMPLATE.URI_PLACEHOLDER)) return true;
18✔
538
            if (t.equals(NTEMPLATE.EXTERNAL_URI_PLACEHOLDER)) return true;
18✔
539
            if (t.equals(NTEMPLATE.TRUSTY_URI_PLACEHOLDER)) return true;
18✔
540
            if (t.equals(NTEMPLATE.AUTO_ESCAPE_URI_PLACEHOLDER)) return true;
12!
541
            if (t.equals(NTEMPLATE.RESTRICTED_CHOICE_PLACEHOLDER)) return true;
12!
542
            if (t.equals(NTEMPLATE.GUIDED_CHOICE_PLACEHOLDER)) return true;
18✔
543
            if (t.equals(NTEMPLATE.AGENT_PLACEHOLDER)) return true;
18✔
544
        }
3✔
545
        return false;
6✔
546
    }
547

548
    /**
549
     * Checks if the IRI is an external URI placeholder.
550
     *
551
     * @param iri the IRI to check.
552
     * @return true if the IRI is an external URI placeholder, false otherwise.
553
     */
554
    public boolean isExternalUriPlaceholder(IRI iri) {
555
        iri = transform(iri);
12✔
556
        if (!typeMap.containsKey(iri)) return false;
15!
557
        for (IRI t : typeMap.get(iri)) {
42✔
558
            if (t.equals(NTEMPLATE.EXTERNAL_URI_PLACEHOLDER)) return true;
18✔
559
            if (t.equals(NTEMPLATE.TRUSTY_URI_PLACEHOLDER)) return true;
12!
560
        }
3✔
561
        return false;
6✔
562
    }
563

564
    /**
565
     * Checks if the IRI is a trusty URI placeholder.
566
     *
567
     * @param iri the IRI to check.
568
     * @return true if the IRI is a trusty URI placeholder, false otherwise.
569
     */
570
    public boolean isTrustyUriPlaceholder(IRI iri) {
571
        iri = transform(iri);
12✔
572
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.TRUSTY_URI_PLACEHOLDER);
45!
573
    }
574

575
    /**
576
     * Checks if the IRI is an auto-escape URI placeholder.
577
     *
578
     * @param iri the IRI to check.
579
     * @return true if the IRI is an auto-escape URI placeholder, false otherwise.
580
     */
581
    public boolean isAutoEscapePlaceholder(IRI iri) {
582
        iri = transform(iri);
12✔
583
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.AUTO_ESCAPE_URI_PLACEHOLDER);
45!
584
    }
585

586
    /**
587
     * Checks if the IRI is a literal placeholder.
588
     *
589
     * @param iri the IRI to check.
590
     * @return true if the IRI is a literal placeholder, false otherwise.
591
     */
592
    public boolean isLiteralPlaceholder(IRI iri) {
593
        iri = transform(iri);
12✔
594
        return typeMap.containsKey(iri) && (typeMap.get(iri).contains(NTEMPLATE.LITERAL_PLACEHOLDER) || typeMap.get(iri).contains(NTEMPLATE.LONG_LITERAL_PLACEHOLDER)
75✔
595
                || typeMap.get(iri).contains(LANGUAGE_TAGGED_LITERAL_PLACEHOLDER));
24✔
596
    }
597

598
    /**
599
     * Checks if the given literal placeholder lets the user select the language tag at fill time.
600
     *
601
     * @param iri the IRI to check.
602
     * @return true if the IRI is a language-tag-selectable literal placeholder, false otherwise.
603
     */
604
    public boolean isLanguageTagSelectable(IRI iri) {
605
        iri = transform(iri);
12✔
606
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(LANGUAGE_TAGGED_LITERAL_PLACEHOLDER);
51✔
607
    }
608

609
    /**
610
     * Checks if the IRI is a long literal placeholder.
611
     *
612
     * @param iri the IRI to check.
613
     * @return true if the IRI is a long literal placeholder, false otherwise.
614
     */
615
    public boolean isLongLiteralPlaceholder(IRI iri) {
616
        iri = transform(iri);
12✔
617
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.LONG_LITERAL_PLACEHOLDER);
51✔
618
    }
619

620
    /**
621
     * Checks if the IRI is a restricted choice placeholder.
622
     *
623
     * @param iri the IRI to check.
624
     * @return true if the IRI is a restricted choice placeholder, false otherwise.
625
     */
626
    public boolean isRestrictedChoicePlaceholder(IRI iri) {
627
        iri = transform(iri);
12✔
628
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.RESTRICTED_CHOICE_PLACEHOLDER);
51✔
629
    }
630

631
    /**
632
     * Checks if the IRI is a guided choice placeholder.
633
     *
634
     * @param iri the IRI to check.
635
     * @return true if the IRI is a guided choice placeholder, false otherwise.
636
     */
637
    public boolean isGuidedChoicePlaceholder(IRI iri) {
638
        iri = transform(iri);
12✔
639
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.GUIDED_CHOICE_PLACEHOLDER);
51✔
640
    }
641

642
    /**
643
     * Checks if the IRI is an agent placeholder.
644
     *
645
     * @param iri the IRI to check.
646
     * @return true if the IRI is an agent placeholder, false otherwise.
647
     */
648
    public boolean isAgentPlaceholder(IRI iri) {
649
        iri = transform(iri);
12✔
650
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.AGENT_PLACEHOLDER);
51✔
651
    }
652

653
    /**
654
     * Checks if the IRI is a sequence element placeholder.
655
     *
656
     * @param iri the IRI to check.
657
     * @return true if the IRI is a sequence element placeholder, false otherwise.
658
     */
659
    public boolean isSequenceElementPlaceholder(IRI iri) {
660
        iri = transform(iri);
12✔
661
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.SEQUENCE_ELEMENT_PLACEHOLDER);
45!
662
    }
663

664
    /**
665
     * Checks if the IRI is a root nanopub placeholder.
666
     *
667
     * @param iri the IRI to check.
668
     * @return true if the IRI is a root nanopub placeholder, false otherwise.
669
     */
670
    public boolean isRootNanopubPlaceholder(IRI iri) {
671
        iri = transform(iri);
12✔
672
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.ROOT_NANOPUB_PLACEHOLDER);
51✔
673
    }
674

675
    /**
676
     * Checks if the IRI is a placeholder of any type.
677
     *
678
     * @param iri the IRI to check.
679
     * @return true if the IRI is a placeholder, false otherwise.
680
     */
681
    public boolean isPlaceholder(IRI iri) {
682
        iri = transform(iri);
12✔
683
        if (!typeMap.containsKey(iri)) return false;
21✔
684
        for (IRI t : typeMap.get(iri)) {
42✔
685
            if (t.equals(NTEMPLATE.VALUE_PLACEHOLDER)) return true;
18✔
686
            if (t.equals(NTEMPLATE.URI_PLACEHOLDER)) return true;
18✔
687
            if (t.equals(NTEMPLATE.EXTERNAL_URI_PLACEHOLDER)) return true;
18✔
688
            if (t.equals(NTEMPLATE.TRUSTY_URI_PLACEHOLDER)) return true;
18✔
689
            if (t.equals(NTEMPLATE.AUTO_ESCAPE_URI_PLACEHOLDER)) return true;
18✔
690
            if (t.equals(NTEMPLATE.RESTRICTED_CHOICE_PLACEHOLDER)) return true;
18✔
691
            if (t.equals(NTEMPLATE.GUIDED_CHOICE_PLACEHOLDER)) return true;
18✔
692
            if (t.equals(NTEMPLATE.AGENT_PLACEHOLDER)) return true;
18✔
693
            if (t.equals(NTEMPLATE.LITERAL_PLACEHOLDER)) return true;
18✔
694
            if (t.equals(NTEMPLATE.LONG_LITERAL_PLACEHOLDER)) return true;
18✔
695
            if (t.equals(LANGUAGE_TAGGED_LITERAL_PLACEHOLDER)) return true;
18✔
696
            if (t.equals(NTEMPLATE.SEQUENCE_ELEMENT_PLACEHOLDER)) return true;
16✔
697
            if (t.equals(NTEMPLATE.ROOT_NANOPUB_PLACEHOLDER)) return true;
18✔
698
        }
3✔
699
        return false;
6✔
700
    }
701

702
    /**
703
     * Checks if the IRI is an optional statement.
704
     *
705
     * @param iri the IRI to check.
706
     * @return true if the IRI is an optional statement, false otherwise.
707
     */
708
    public boolean isOptionalStatement(IRI iri) {
709
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.OPTIONAL_STATEMENT);
51✔
710
    }
711

712
    /**
713
     * Whether the placeholder with the given parameter name is a required field of
714
     * this template: it appears (as subject or object) in at least one non-optional
715
     * statement — i.e. a value must be provided to publish. A repetition-index
716
     * suffix on the name (e.g. {@code public-key__.1}) is ignored, matching the base
717
     * placeholder. Used to decide whether an empty mapped value should hide a view
718
     * action's button (see docs/magic-query-params.md).
719
     *
720
     * @param paramName the parameter name (placeholder local name, optionally with a
721
     *                  {@code __.N} repetition suffix)
722
     * @return true if the placeholder is required (appears in a non-optional statement)
723
     */
724
    public boolean isRequiredField(String paramName) {
725
        if (paramName == null) return false;
×
726
        String base = paramName.replaceFirst("__\\..*$", "");
×
727
        for (IRI top : getStatementIris()) {
×
728
            List<IRI> members = getStatementIris(top);
×
729
            if (members != null) {
×
730
                if (isOptionalStatement(top)) continue; // whole group optional
×
731
                for (IRI st : members) {
×
732
                    if (!isOptionalStatement(st) && statementReferencesPlaceholder(st, base)) return true;
×
733
                }
×
734
            } else if (!isOptionalStatement(top) && statementReferencesPlaceholder(top, base)) {
×
735
                return true;
×
736
            }
737
        }
×
738
        return false;
×
739
    }
740

741
    private boolean statementReferencesPlaceholder(IRI statementIri, String localName) {
742
        return matchesPlaceholderLocalName(getSubject(statementIri), localName)
×
743
                || matchesPlaceholderLocalName(getObject(statementIri), localName);
×
744
    }
745

746
    private boolean matchesPlaceholderLocalName(Value value, String localName) {
747
        if (!(value instanceof IRI iri) || !isPlaceholder(iri)) return false;
×
748
        String s = iri.stringValue();
×
749
        int i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('#'));
×
750
        return localName.equals(i < 0 ? s : s.substring(i + 1));
×
751
    }
752

753
    /**
754
     * Checks if the IRI is an advanced statement, which are only show upon expanding to full view in form mode.
755
     *
756
     * @param iri the IRI to check.
757
     * @return true if the IRI is an advanced statement, false otherwise.
758
     */
759
    public boolean isAdvancedStatement(IRI iri) {
760
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(ADVANCED_STATEMENT);
45!
761
    }
762

763
    /**
764
     * Checks if the IRI is a grouped statement.
765
     *
766
     * @param iri the IRI to check.
767
     * @return true if the IRI is a grouped statement, false otherwise.
768
     */
769
    public boolean isGroupedStatement(IRI iri) {
770
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.GROUPED_STATEMENT);
51✔
771
    }
772

773
    /**
774
     * Checks if the IRI is a repeatable statement.
775
     *
776
     * @param iri the IRI to check.
777
     * @return true if the IRI is a repeatable statement, false otherwise.
778
     */
779
    public boolean isRepeatableStatement(IRI iri) {
780
        return typeMap.containsKey(iri) && typeMap.get(iri).contains(NTEMPLATE.REPEATABLE_STATEMENT);
51✔
781
    }
782

783
    /**
784
     * Returns the possible values for a given IRI.
785
     *
786
     * @param iri the IRI for which to get possible values.
787
     * @return a list of possible values for the IRI. If no values are found, an empty list is returned.
788
     */
789
    public List<Value> getPossibleValues(IRI iri) {
790
        iri = transform(iri);
12✔
791
        List<Value> l = possibleValueMap.get(iri);
18✔
792
        if (l == null) {
6✔
793
            l = new ArrayList<>();
12✔
794
            possibleValueMap.put(iri, l);
18✔
795
        }
796
        List<IRI> nanopubList = possibleValuesToLoadMap.get(iri);
18✔
797
        if (nanopubList != null) {
6!
798
            for (IRI npIri : new ArrayList<>(nanopubList)) {
×
799
                try {
800
                    Nanopub valuesNanopub = Utils.getNanopub(npIri.stringValue());
×
801
                    for (Statement st : valuesNanopub.getAssertion()) {
×
802
                        if (st.getPredicate().equals(RDFS.LABEL)) {
×
803
                            l.add((IRI) st.getSubject());
×
804
                        }
805
                    }
×
806
                    nanopubList.remove(npIri);
×
807
                } catch (Exception ex) {
×
808
                    logger.error("Could not load possible values from nanopub {}", npIri.stringValue(), ex);
×
809
                }
×
810
            }
×
811
        }
812
        return l;
6✔
813
    }
814

815
    /**
816
     * Returns the IRI of the default provenance for the template.
817
     *
818
     * @return the IRI of the default provenance, or null if not set.
819
     */
820
    public IRI getDefaultProvenance() {
821
        return defaultProvenance;
9✔
822
    }
823

824
    /**
825
     * Returns the target namespace for the template.
826
     *
827
     * @return the target namespace as a string, or null if not set.
828
     */
829
    public String getTargetNamespace() {
830
        return targetNamespace;
9✔
831
    }
832

833
    /**
834
     * Returns the nanopub label pattern.
835
     *
836
     * @return the nanopub label pattern as a string, or null if not set.
837
     */
838
    public String getNanopubLabelPattern() {
839
        return nanopubLabelPattern;
9✔
840
    }
841

842
    /**
843
     * Returns the list of target nanopub types.
844
     *
845
     * @return a list of IRI objects representing the target nanopub types.
846
     */
847
    public List<IRI> getTargetNanopubTypes() {
848
        return targetNanopubTypes;
×
849
    }
850

851
    /**
852
     * Returns the list of the required pubInfo elements for the template.
853
     *
854
     * @return a list of IRI objects representing the required pubInfo elements.
855
     */
856
    public List<IRI> getRequiredPubInfoElements() {
857
        return requiredPubInfoElements;
9✔
858
    }
859

860
    /**
861
     * Returns the possible values from an API for a given IRI and search term.
862
     *
863
     * @param iri        the IRI for which to get possible values from the API.
864
     * @param searchTerm the search term to filter the possible values.
865
     * @param labelMap   a map to store labels for the possible values.
866
     * @return a list of possible values from the API, filtered by the search term.
867
     */
868
    public List<String> getPossibleValuesFromApi(IRI iri, String searchTerm, Map<String, String> labelMap) {
869
        iri = transform(iri);
12✔
870
        List<String> values = new ArrayList<>();
12✔
871
        List<String> apiList = apiMap.get(iri);
18✔
872
        if (apiList != null) {
6!
873
            for (String apiString : apiList) {
30✔
874
                LookupApis.getPossibleValues(apiString, searchTerm, labelMap, values);
15✔
875
            }
3✔
876
        }
877
        return values;
6✔
878
    }
879

880
    /**
881
     * Returns the tag associated with the template.
882
     *
883
     * @return the tag as a string, or null if not set.
884
     */
885
    public String getTag() {
886
        return tag;
9✔
887
    }
888

889
    private void processTemplate(Nanopub templateNp) throws MalformedTemplateException {
890
        IRI assertionUri = templateNp.getAssertionUri();
9✔
891
        Set<IRI> templateNodes = new LinkedHashSet<>();
12✔
892
        for (Statement st : templateNp.getAssertion()) {
33✔
893
            if (!st.getPredicate().equals(RDF.TYPE)) continue;
18✔
894
            if (!(st.getSubject() instanceof IRI subjIri)) continue;
27!
895
            if (st.getObject().equals(NTEMPLATE.ASSERTION_TEMPLATE) || st.getObject().equals(NTEMPLATE.PROVENANCE_TEMPLATE) || st.getObject().equals(NTEMPLATE.PUBINFO_TEMPLATE)) {
45✔
896
                templateNodes.add(subjIri);
12✔
897
            }
898
        }
3✔
899

900
        if (templateNodes.contains(assertionUri)) {
12✔
901
            // Legacy shape (template node = assertion graph URI) takes precedence
902
            // over any other typed node, so every template that parsed before
903
            // embedded identity existed keeps parsing identically.
904
            templateIri = assertionUri;
9✔
905
            processNpTemplate(templateNp);
12✔
906
        } else if (templateNodes.size() == 1) {
12✔
907
            IRI node = templateNodes.iterator().next();
15✔
908
            if (!node.stringValue().startsWith(templateNp.getUri().stringValue())) {
21✔
909
                throw new MalformedTemplateException("Embedded template node must be in the nanopub's namespace: " + node);
21✔
910
            }
911
            templateIri = node;
9✔
912
            embeddedIdentity = true;
9✔
913
            processNpTemplate(templateNp);
9✔
914
        } else if (templateNodes.size() > 1) {
15✔
915
            throw new MalformedTemplateException("Multiple embedded template nodes found");
15✔
916
        } else {
917
            // Experimental SHACL-based template:
918
            processShaclTemplate(templateNp);
9✔
919
        }
920
        tagUntypedLocalIrisAsLocalResources(templateNp);
9✔
921
        checkLanguageTagPlaceholders();
6✔
922
    }
3✔
923

924
    // A literal is either language-tagged or datatyped, never both; the language-tag
925
    // picker wins so the placeholder keeps rendering as a text field.
926
    private void checkLanguageTagPlaceholders() {
927
        for (Map.Entry<IRI, List<IRI>> e : typeMap.entrySet()) {
36✔
928
            if (e.getValue().contains(LANGUAGE_TAGGED_LITERAL_PLACEHOLDER) && datatypeMap.containsKey(e.getKey())) {
36✔
929
                logger.warn("Ignoring datatype {} on language-tag-selectable placeholder {}", datatypeMap.get(e.getKey()), e.getKey());
30✔
930
                datatypeMap.remove(e.getKey());
18✔
931
            }
932
        }
3✔
933
    }
3✔
934

935
    // Local IRIs in used statement positions that carry no identity type (no placeholder
936
    // type, not a Local/Introduced/Embedded Resource) are automatically treated as Local
937
    // Resources, so every produced nanopub mints them under its own namespace instead of
938
    // copying the template-local IRI verbatim (see issue #551).
939
    private void tagUntypedLocalIrisAsLocalResources(Nanopub templateNp) {
940
        List<IRI> topIris = statementMap.get(templateIri);
21✔
941
        if (topIris == null) return;
9✔
942
        String npUri = templateNp.getUri().stringValue();
12✔
943
        List<IRI> statementIris = new ArrayList<>();
12✔
944
        for (IRI iri : topIris) {
30✔
945
            if (statementMap.containsKey(iri)) {
15✔
946
                // grouped statement
947
                statementIris.addAll(statementMap.get(iri));
27✔
948
            } else {
949
                statementIris.add(iri);
12✔
950
            }
951
        }
3✔
952
        for (IRI st : statementIris) {
30✔
953
            tagIfUntypedLocal(statementSubjects.get(st), npUri);
24✔
954
            tagIfUntypedLocal(statementPredicates.get(st), npUri);
24✔
955
            tagIfUntypedLocal(statementObjects.get(st), npUri);
24✔
956
        }
3✔
957
    }
3✔
958

959
    private void tagIfUntypedLocal(Value value, String npUri) {
960
        if (!(value instanceof IRI iri)) return;
24✔
961
        String s = iri.stringValue();
9✔
962
        // Only sub-IRIs of the template nanopub are local; the nanopub URI itself is a
963
        // global reference to the template and stays verbatim:
964
        if (!s.startsWith(npUri) || s.length() == npUri.length()) return;
30✔
965
        if (iri.equals(templateIri)) return;
15!
966
        // Statement/group identifiers referenced as values stay untouched:
967
        if (statementMap.containsKey(iri) || statementSubjects.containsKey(iri)) return;
33!
968
        if (isPlaceholder(iri) || isLocalResource(iri) || isIntroducedResource(iri) || isEmbeddedResource(iri)) return;
51!
969
        addType(iri, NTEMPLATE.LOCAL_RESOURCE);
12✔
970
    }
3✔
971

972
    private void processNpTemplate(Nanopub templateNp) throws MalformedTemplateException {
973
        for (Statement st : templateNp.getAssertion()) {
33✔
974
            final IRI subj = (IRI) st.getSubject();
12✔
975
            final IRI pred = st.getPredicate();
9✔
976
            final Value obj = st.getObject();
9✔
977
            final String objS = obj.stringValue();
9✔
978

979
            if (subj.equals(templateIri)) {
15✔
980
                if (pred.equals(RDFS.LABEL)) {
12✔
981
                    label = objS;
12✔
982
                } else if (pred.equals(DCTERMS.DESCRIPTION)) {
12✔
983
                    description = Utils.sanitizeHtml(objS);
15✔
984
                } else if (obj instanceof IRI objIri) {
18✔
985
                    if (pred.equals(NTEMPLATE.HAS_DEFAULT_PROVENANCE)) {
12✔
986
                        defaultProvenance = objIri;
12✔
987
                    } else if (pred.equals(NTEMPLATE.HAS_REQUIRED_PUBINFO_ELEMENT)) {
12✔
988
                        requiredPubInfoElements.add(objIri);
18✔
989
                    } else if (pred.equals(NTEMPLATE.HAS_TARGET_NAMESPACE)) {
12✔
990
                        targetNamespace = objS;
12✔
991
                    } else if (pred.equals(NTEMPLATE.HAS_TARGET_NANOPUB_TYPE)) {
12✔
992
                        targetNanopubTypes.add(objIri);
18✔
993
                    } else if (pred.equals(DCTERMS.IS_VERSION_OF)) {
12✔
994
                        templateKindIri = objIri;
12✔
995
                    } else if (pred.equals(KPXL_TERMS.GOVERNED_BY)) {
12✔
996
                        governingSpace = objIri;
12✔
997
                    }
998
                } else if (obj instanceof Literal) {
9!
999
                    if (pred.equals(NTEMPLATE.HAS_TAG)) {
12✔
1000
                        // TODO This should be replaced at some point with a more sophisticated mechanism based on classes.
1001
                        // We are assuming that there is at most one tag.
1002
                        this.tag = objS;
12✔
1003
                    } else if (pred.equals(NTEMPLATE.HAS_NANOPUB_LABEL_PATTERN)) {
12!
1004
                        nanopubLabelPattern = objS;
9✔
1005
                    }
1006
                }
1007
            }
1008
            if (pred.equals(RDF.TYPE) && obj instanceof IRI objIri) {
30!
1009
                addType(subj, objIri);
15✔
1010
            } else if (pred.equals(NTEMPLATE.HAS_STATEMENT) && obj instanceof IRI objIri) {
30!
1011
                List<IRI> l = statementMap.get(subj);
18✔
1012
                if (l == null) {
6✔
1013
                    l = new ArrayList<>();
12✔
1014
                    statementMap.put(subj, l);
18✔
1015
                }
1016
                l.add((IRI) objIri);
12✔
1017
            } else if (pred.equals(NTEMPLATE.POSSIBLE_VALUE)) {
15✔
1018
                List<Value> l = possibleValueMap.get(subj);
18✔
1019
                if (l == null) {
6✔
1020
                    l = new ArrayList<>();
12✔
1021
                    possibleValueMap.put(subj, l);
18✔
1022
                }
1023
                l.add(obj);
12✔
1024
            } else if (pred.equals(NTEMPLATE.POSSIBLE_VALUES_FROM)) {
15✔
1025
                List<IRI> l = possibleValuesToLoadMap.get(subj);
18✔
1026
                if (l == null) {
6!
1027
                    l = new ArrayList<>();
12✔
1028
                    possibleValuesToLoadMap.put(subj, l);
18✔
1029
                }
1030
                if (obj instanceof IRI objIri) {
18!
1031
                    l.add(objIri);
12✔
1032
                    Nanopub valuesNanopub = Utils.getNanopub(objS);
9✔
1033
                    for (Statement s : valuesNanopub.getAssertion()) {
33✔
1034
                        if (s.getPredicate().equals(RDFS.LABEL)) {
15!
1035
                            labelMap.put((IRI) s.getSubject(), s.getObject().stringValue());
30✔
1036
                        }
1037
                    }
3✔
1038
                }
1039
            } else if (pred.equals(NTEMPLATE.POSSIBLE_VALUES_FROM_API)) {
15✔
1040
                List<String> l = apiMap.get(subj);
18✔
1041
                if (l == null) {
6✔
1042
                    l = new ArrayList<>();
12✔
1043
                    apiMap.put(subj, l);
18✔
1044
                }
1045
                if (obj instanceof Literal) {
9!
1046
                    l.add(objS);
12✔
1047
                }
1048
            } else if (pred.equals(RDFS.LABEL) && obj instanceof Literal) {
24!
1049
                labelMap.put(subj, objS);
21✔
1050
            } else if (pred.equals(NTEMPLATE.HAS_DATATYPE) && obj instanceof IRI objIri) {
30!
1051
                datatypeMap.put(subj, objIri);
21✔
1052
            } else if (pred.equals(NTEMPLATE.HAS_LANGUAGE_TAG) && obj instanceof Literal) {
21!
1053
                languageTagMap.put(subj, Literals.normalizeLanguageTag(objS));
24✔
1054
            } else if (pred.equals(POSSIBLE_LANGUAGE_TAG) && obj instanceof Literal) {
21!
1055
                possibleLanguageTagMap.computeIfAbsent(subj, k -> new ArrayList<>()).add(Literals.normalizeLanguageTag(objS));
45✔
1056
            } else if (pred.equals(NTEMPLATE.HAS_PREFIX) && obj instanceof Literal) {
21!
1057
                prefixMap.put(subj, objS);
21✔
1058
            } else if (pred.equals(NTEMPLATE.HAS_PREFIX_LABEL) && obj instanceof Literal) {
21!
1059
                prefixLabelMap.put(subj, objS);
21✔
1060
            } else if (pred.equals(NTEMPLATE.HAS_REGEX) && obj instanceof Literal) {
21!
1061
                regexMap.put(subj, objS);
21✔
1062
            } else if (pred.equals(RDF.SUBJECT)) {
12✔
1063
                if (obj instanceof IRI objIri) {
18✔
1064
                    statementSubjects.put(subj, objIri);
21✔
1065
                } else {
1066
                    nonIriStatementSubjects.put(subj, obj);
21✔
1067
                }
1068
            } else if (pred.equals(RDF.PREDICATE)) {
12✔
1069
                if (obj instanceof IRI objIri) {
18!
1070
                    statementPredicates.put(subj, objIri);
21✔
1071
                } else {
1072
                    nonIriStatementPredicates.put(subj, obj);
×
1073
                }
1074
            } else if (pred.equals(RDF.OBJECT)) {
12✔
1075
                statementObjects.put(subj, obj);
21✔
1076
            } else if (pred.equals(NTEMPLATE.HAS_DEFAULT_VALUE)) {
12✔
1077
                defaultValues.put(subj, obj);
21✔
1078
            } else if (pred.equals(NTEMPLATE.STATEMENT_ORDER)) {
12✔
1079
                if (obj instanceof Literal && objS.matches("[0-9]+")) {
21!
1080
                    statementOrder.put(subj, Integer.valueOf(objS));
21✔
1081
                }
1082
            }
1083
        }
3✔
1084
//                List<IRI> assertionTypes = typeMap.get(templateIri);
1085
//                if (assertionTypes == null || (!assertionTypes.contains(NTEMPLATE.ASSERTION_TEMPLATE) &&
1086
//                                !assertionTypes.contains(NTEMPLATE.PROVENANCE_TEMPLATE) && !assertionTypes.contains(PUBINFO_TEMPLATE))) {
1087
//                        throw new MalformedTemplateException("Unknown template type");
1088
//                }
1089
        List<IRI> groupIris = new ArrayList<>();
12✔
1090
        for (IRI iri : typeMap.keySet()) {
36✔
1091
            if (typeMap.get(iri).contains(NTEMPLATE.GROUPED_STATEMENT)) groupIris.add(iri);
36✔
1092
        }
3✔
1093
        // Nested groups and repeatable members are out of scope (the group is the unit of
1094
        // repetition); ignore such member flags with a warning instead of half-rendering:
1095
        for (IRI iri : groupIris) {
30✔
1096
            if (!isGroupedStatement(iri)) continue;
15✔
1097
            List<IRI> memberIris = getStatementIris(iri);
12✔
1098
            if (memberIris == null) {
6✔
1099
                throw new MalformedTemplateException("Grouped statement has no member statements: " + iri);
21✔
1100
            }
1101
            for (IRI member : memberIris) {
30✔
1102
                List<IRI> memberTypes = typeMap.get(member);
18✔
1103
                if (memberTypes == null) continue;
9✔
1104
                if (memberTypes.remove(NTEMPLATE.GROUPED_STATEMENT)) {
12✔
1105
                    logger.warn("Ignoring nested nt:GroupedStatement flag on member {} of group {}", member, iri);
15✔
1106
                }
1107
                if (memberTypes.remove(NTEMPLATE.REPEATABLE_STATEMENT)) {
12✔
1108
                    logger.warn("Ignoring nt:RepeatableStatement flag on member {} of group {}", member, iri);
15✔
1109
                }
1110
            }
3✔
1111
        }
3✔
1112
        for (IRI iri : groupIris) {
30✔
1113
            if (!isGroupedStatement(iri)) continue;
15✔
1114
            List<IRI> memberIris = getStatementIris(iri);
12✔
1115
            // A group needs at least one required member: an all-optional group would match zero
1116
            // statements, making its presence meaningless and repetition detection non-terminating.
1117
            // Degrade to group-level optionality (today's semantics) instead of rejecting:
1118
            boolean hasRequiredMember = false;
6✔
1119
            for (IRI member : memberIris) {
30✔
1120
                if (!isOptionalStatement(member)) hasRequiredMember = true;
18✔
1121
            }
3✔
1122
            if (!hasRequiredMember) {
6✔
1123
                logger.warn("All members of group {} are optional; treating the group itself as optional instead", iri);
12✔
1124
                if (!isOptionalStatement(iri)) {
12✔
1125
                    addType(iri, NTEMPLATE.OPTIONAL_STATEMENT);
12✔
1126
                }
1127
                for (IRI member : memberIris) {
30✔
1128
                    typeMap.get(member).remove(NTEMPLATE.OPTIONAL_STATEMENT);
24✔
1129
                }
3✔
1130
            }
1131
            // Grouped IRI are added via group, so direct link from template is redundant:
1132
            for (IRI groupedIri : memberIris) {
30✔
1133
                statementMap.get(templateIri).remove(groupedIri);
27✔
1134
            }
3✔
1135
        }
3✔
1136
        for (List<IRI> l : statementMap.values()) {
36✔
1137
            l.sort(statementComparator);
12✔
1138
        }
3✔
1139
    }
3✔
1140

1141
    private void processShaclTemplate(Nanopub templateNp) throws MalformedTemplateException {
1142
        templateIri = null;
9✔
1143
        for (Statement st : templateNp.getAssertion()) {
33!
1144
            if (st.getPredicate().equals(SHACL.TARGET_CLASS)) {
15✔
1145
                templateIri = (IRI) st.getSubject();
15✔
1146
                break;
3✔
1147
            }
1148
        }
3✔
1149
        if (templateIri == null) {
9!
1150
            throw new MalformedTemplateException("Base node shape not found");
×
1151
        }
1152

1153
        IRI baseSubj = vf.createIRI(templateIri.stringValue() + "+subj");
21✔
1154
        addType(baseSubj, NTEMPLATE.INTRODUCED_RESOURCE);
12✔
1155

1156
        List<IRI> statementList = new ArrayList<>();
12✔
1157
        Map<IRI, Integer> minCounts = new HashMap<>();
12✔
1158
        Map<IRI, Integer> maxCounts = new HashMap<>();
12✔
1159

1160
        for (Statement st : templateNp.getAssertion()) {
33✔
1161
            final IRI subj = (IRI) st.getSubject();
12✔
1162
            final IRI pred = st.getPredicate();
9✔
1163
            final Value obj = st.getObject();
9✔
1164
            final String objS = obj.stringValue();
9✔
1165

1166
            if (subj.equals(templateIri)) {
15✔
1167
                if (pred.equals(RDFS.LABEL)) {
12!
1168
                    label = objS;
×
1169
                } else if (pred.equals(DCTERMS.DESCRIPTION)) {
12!
1170
                    description = Utils.sanitizeHtml(objS);
×
1171
                } else if (obj instanceof IRI objIri) {
18✔
1172
                    if (pred.equals(NTEMPLATE.HAS_DEFAULT_PROVENANCE)) {
12!
1173
                        defaultProvenance = objIri;
×
1174
                    } else if (pred.equals(NTEMPLATE.HAS_REQUIRED_PUBINFO_ELEMENT)) {
12!
1175
                        requiredPubInfoElements.add(objIri);
×
1176
                    } else if (pred.equals(NTEMPLATE.HAS_TARGET_NAMESPACE)) {
12!
1177
                        targetNamespace = objS;
×
1178
                    } else if (pred.equals(NTEMPLATE.HAS_TARGET_NANOPUB_TYPE)) {
12!
1179
                        targetNanopubTypes.add(objIri);
×
1180
                    }
1181
                } else if (obj instanceof Literal) {
9!
1182
                    if (pred.equals(NTEMPLATE.HAS_TAG)) {
12!
1183
                        // TODO This should be replaced at some point with a more sophisticated mechanism based on classes.
1184
                        // We are assuming that there is at most one tag.
1185
                        this.tag = objS;
×
1186
                    } else if (pred.equals(NTEMPLATE.HAS_NANOPUB_LABEL_PATTERN)) {
12!
1187
                        nanopubLabelPattern = objS;
×
1188
                    }
1189
                }
1190
            }
1191
            if (pred.equals(RDF.TYPE) && obj instanceof IRI objIri) {
30!
1192
                addType(subj, objIri);
15✔
1193
            } else if (pred.equals(SHACL.PROPERTY) && obj instanceof IRI objIri) {
30!
1194
                statementList.add(objIri);
12✔
1195
                List<IRI> l = statementMap.get(subj);
18✔
1196
                if (l == null) {
6✔
1197
                    l = new ArrayList<>();
12✔
1198
                    statementMap.put(subj, l);
18✔
1199
                }
1200
                l.add((IRI) objIri);
12✔
1201
                IRI stSubjIri = vf.createIRI(subj.stringValue() + "+subj");
18✔
1202
                statementSubjects.put(objIri, stSubjIri);
18✔
1203
                addType(stSubjIri, NTEMPLATE.LOCAL_RESOURCE);
12✔
1204
                addType(stSubjIri, NTEMPLATE.URI_PLACEHOLDER);
12✔
1205
            } else if (pred.equals(SHACL.PATH) && obj instanceof IRI objIri) {
33!
1206
                statementPredicates.put(subj, objIri);
21✔
1207
            } else if (pred.equals(SHACL.HAS_VALUE) && obj instanceof IRI objIri) {
30!
1208
                statementObjects.put(subj, objIri);
21✔
1209
            } else if (pred.equals(SHACL.TARGET_CLASS) && obj instanceof IRI objIri) {
30!
1210
                IRI stIri = vf.createIRI(templateNp.getUri() + "/$type");
21✔
1211
                statementList.add(stIri);
12✔
1212
                List<IRI> l = statementMap.get(subj);
18✔
1213
                if (l == null) {
6✔
1214
                    l = new ArrayList<>();
12✔
1215
                    statementMap.put(subj, l);
18✔
1216
                }
1217
                l.add((IRI) stIri);
12✔
1218
                statementSubjects.put(stIri, baseSubj);
18✔
1219
                statementPredicates.put(stIri, RDF.TYPE);
18✔
1220
                statementObjects.put(stIri, objIri);
18✔
1221
            } else if (pred.equals(NTEMPLATE.POSSIBLE_VALUE)) {
15!
1222
                List<Value> l = possibleValueMap.get(subj);
×
1223
                if (l == null) {
×
1224
                    l = new ArrayList<>();
×
1225
                    possibleValueMap.put(subj, l);
×
1226
                }
1227
                l.add(obj);
×
1228
            } else if (pred.equals(NTEMPLATE.POSSIBLE_VALUES_FROM)) {
12!
1229
                List<IRI> l = possibleValuesToLoadMap.get(subj);
×
1230
                if (l == null) {
×
1231
                    l = new ArrayList<>();
×
1232
                    possibleValuesToLoadMap.put(subj, l);
×
1233
                }
1234
                if (obj instanceof IRI objIri) {
×
1235
                    l.add(objIri);
×
1236
                    Nanopub valuesNanopub = Utils.getNanopub(objS);
×
1237
                    for (Statement s : valuesNanopub.getAssertion()) {
×
1238
                        if (s.getPredicate().equals(RDFS.LABEL)) {
×
1239
                            labelMap.put((IRI) s.getSubject(), s.getObject().stringValue());
×
1240
                        }
1241
                    }
×
1242
                }
1243
            } else if (pred.equals(NTEMPLATE.POSSIBLE_VALUES_FROM_API)) {
12!
1244
                List<String> l = apiMap.get(subj);
×
1245
                if (l == null) {
×
1246
                    l = new ArrayList<>();
×
1247
                    apiMap.put(subj, l);
×
1248
                }
1249
                if (obj instanceof Literal) {
×
1250
                    l.add(objS);
×
1251
                }
1252
            } else if (pred.equals(RDFS.LABEL) && obj instanceof Literal) {
21!
1253
                labelMap.put(subj, objS);
21✔
1254
            } else if (pred.equals(NTEMPLATE.HAS_DATATYPE) && obj instanceof IRI objIri) {
12!
1255
                datatypeMap.put(subj, objIri);
×
1256
            } else if (pred.equals(NTEMPLATE.HAS_LANGUAGE_TAG) && obj instanceof Literal) {
12!
1257
                languageTagMap.put(subj, Literals.normalizeLanguageTag(objS));
×
1258
            } else if (pred.equals(POSSIBLE_LANGUAGE_TAG) && obj instanceof Literal) {
12!
1259
                possibleLanguageTagMap.computeIfAbsent(subj, k -> new ArrayList<>()).add(Literals.normalizeLanguageTag(objS));
×
1260
            } else if (pred.equals(NTEMPLATE.HAS_PREFIX) && obj instanceof Literal) {
12!
1261
                prefixMap.put(subj, objS);
×
1262
            } else if (pred.equals(NTEMPLATE.HAS_PREFIX_LABEL) && obj instanceof Literal) {
12!
1263
                prefixLabelMap.put(subj, objS);
×
1264
            } else if (pred.equals(NTEMPLATE.HAS_REGEX) && obj instanceof Literal) {
12!
1265
                regexMap.put(subj, objS);
×
1266
//                        } else if (pred.equals(RDF.SUBJECT) && obj instanceof IRI objIri) {
1267
//                                statementSubjects.put(subj, objIri);
1268
//                        } else if (pred.equals(RDF.PREDICATE) && obj instanceof IRI objIri) {
1269
//                                statementPredicates.put(subj, objIri);
1270
//                        } else if (pred.equals(RDF.OBJECT)) {
1271
//                                statementObjects.put(subj, obj);
1272
            } else if (pred.equals(NTEMPLATE.HAS_DEFAULT_VALUE)) {
12!
1273
                defaultValues.put(subj, obj);
×
1274
            } else if (pred.equals(NTEMPLATE.STATEMENT_ORDER)) {
12!
1275
                if (obj instanceof Literal && objS.matches("[0-9]+")) {
×
1276
                    statementOrder.put(subj, Integer.valueOf(objS));
×
1277
                }
1278
            } else if (pred.equals(SHACL.MIN_COUNT)) {
12✔
1279
                try {
1280
                    minCounts.put(subj, Integer.parseInt(obj.stringValue()));
24✔
1281
                } catch (NumberFormatException ex) {
×
1282
                    logger.error("Could not parse <{}> value: {}", SHACL.MIN_COUNT, obj.stringValue());
×
1283
                }
3✔
1284
            } else if (pred.equals(SHACL.MAX_COUNT)) {
12✔
1285
                try {
1286
                    maxCounts.put(subj, Integer.parseInt(obj.stringValue()));
24✔
1287
                } catch (NumberFormatException ex) {
×
1288
                    logger.error("Could not parse <{}> value: {}", SHACL.MAX_COUNT, obj.stringValue());
×
1289
                }
3✔
1290
            }
1291
        }
3✔
1292
        for (List<IRI> l : statementMap.values()) {
36✔
1293
            l.sort(statementComparator);
12✔
1294
        }
3✔
1295
        for (IRI iri : statementList) {
30✔
1296
            if (!statementObjects.containsKey(iri)) {
15✔
1297
                IRI stObjIri = vf.createIRI(iri.stringValue() + "+obj");
18✔
1298
                statementObjects.put(iri, stObjIri);
18✔
1299
                addType(stObjIri, NTEMPLATE.VALUE_PLACEHOLDER);
12✔
1300
                if (!minCounts.containsKey(iri) || minCounts.get(iri) <= 0) {
30✔
1301
                    addType(iri, NTEMPLATE.OPTIONAL_STATEMENT);
12✔
1302
                }
1303
                if (!maxCounts.containsKey(iri) || maxCounts.get(iri) > 1) {
33!
1304
                    addType(iri, NTEMPLATE.REPEATABLE_STATEMENT);
12✔
1305
                }
1306
            }
1307
        }
3✔
1308
        if (!labelMap.containsKey(baseSubj) && typeMap.get(baseSubj).contains(NTEMPLATE.URI_PLACEHOLDER) && typeMap.get(baseSubj).contains(NTEMPLATE.LOCAL_RESOURCE)) {
63!
1309
            labelMap.put(baseSubj, "short ID as URI suffix");
18✔
1310
        }
1311

1312
        if (label == null) {
9!
1313
            label = NanopubUtils.getLabel(templateNp);
12✔
1314
        }
1315
    }
3✔
1316

1317
    public void addToLabelMap(Object key, String label) {
1318
        if (key instanceof IRI iri) {
×
1319
            labelMap.put(iri, label);
×
1320
        } else {
1321
            labelMap.put(vf.createIRI(key.toString()), label);
×
1322
        }
1323
    }
×
1324

1325
    private void addType(IRI thing, IRI type) {
1326
        List<IRI> l = typeMap.get(thing);
18✔
1327
        if (l == null) {
6✔
1328
            l = new ArrayList<>();
12✔
1329
            typeMap.put(thing, l);
18✔
1330
        }
1331
        l.add(type);
12✔
1332
    }
3✔
1333

1334
    private IRI transform(IRI iri) {
1335
        // A template can leave a statement part undefined (see getStatementErrors()), in which
1336
        // case the null propagates here through getSubject()/getPredicate(). Let the type and
1337
        // label lookups below answer "no" for it instead of failing to render the whole form.
1338
        if (iri == null) return null;
12✔
1339
        String s = iri.stringValue();
9✔
1340
        // Map a rendered IRI back to its template form for map lookups (label, type,
1341
        // datatype, …). RepetitionGroup.transform expands the template's "~~ARTIFACTCODE~~"
1342
        // placeholder to the target-namespace "~~~ARTIFACTCODE~~~" form and appends a "__N"
1343
        // repetition suffix; both must be undone here or the lookup keyed on the template
1344
        // form (e.g. an attached rdfs:label) is missed.
1345
        if (s.contains("~~~ARTIFACTCODE~~~")) {
12✔
1346
            s = s.replace("~~~ARTIFACTCODE~~~", "~~ARTIFACTCODE~~");
15✔
1347
        }
1348
        if (s.matches(".*__[0-9]+")) {
12✔
1349
            // TODO: Check that this double-underscore pattern isn't used otherwise:
1350
            s = s.replaceFirst("__[0-9]+$", "");
15✔
1351
        }
1352
        if (s.equals(iri.stringValue())) return iri;
21✔
1353
        return vf.createIRI(s);
12✔
1354
    }
1355

1356
    private StatementComparator statementComparator = new StatementComparator();
18✔
1357

1358
    private class StatementComparator implements Comparator<IRI>, Serializable {
18✔
1359

1360
        /**
1361
         * Compares two IRIs based on their order in the template.
1362
         *
1363
         * @param arg0 the first object to be compared.
1364
         * @param arg1 the second object to be compared.
1365
         * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
1366
         */
1367
        @Override
1368
        public int compare(IRI arg0, IRI arg1) {
1369
            Integer i0 = statementOrder.get(arg0);
21✔
1370
            Integer i1 = statementOrder.get(arg1);
21✔
1371
            if (i0 == null && i1 == null) return arg0.stringValue().compareTo(arg1.stringValue());
30✔
1372
            if (i0 == null) return 1;
10✔
1373
            if (i1 == null) return -1;
10✔
1374
            return i0 - i1;
18✔
1375
        }
1376

1377
    }
1378

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