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

knowledgepixels / nanodash / 24880477649

24 Apr 2026 08:39AM UTC coverage: 18.287% (+0.8%) from 17.496%
24880477649

push

github

web-flow
Merge pull request #453 from knowledgepixels/add-root-nanopub-placeholder

feat: support RootNanopubPlaceholder in templates

956 of 6218 branches covered (15.37%)

Branch coverage included in aggregate %.

2251 of 11319 relevant lines covered (19.89%)

2.76 hits per line

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

22.72
src/main/java/com/knowledgepixels/nanodash/template/TemplateContext.java
1
package com.knowledgepixels.nanodash.template;
2

3
import com.knowledgepixels.nanodash.LocalUri;
4
import com.knowledgepixels.nanodash.NanodashSession;
5
import com.knowledgepixels.nanodash.Utils;
6
import com.knowledgepixels.nanodash.component.LiteralDateItem;
7
import com.knowledgepixels.nanodash.component.PublishForm.FillMode;
8
import com.knowledgepixels.nanodash.component.StatementItem;
9
import org.apache.wicket.Component;
10
import org.apache.wicket.model.IModel;
11
import org.apache.wicket.model.Model;
12
import org.eclipse.rdf4j.model.IRI;
13
import org.eclipse.rdf4j.model.Statement;
14
import org.eclipse.rdf4j.model.Value;
15
import org.eclipse.rdf4j.model.ValueFactory;
16
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
17
import org.eclipse.rdf4j.model.vocabulary.RDFS;
18
import org.eclipse.rdf4j.model.vocabulary.XSD;
19
import org.nanopub.*;
20
import org.nanopub.vocabulary.NTEMPLATE;
21

22
import java.io.Serializable;
23
import java.time.ZonedDateTime;
24
import java.util.*;
25

26
/**
27
 * Context for a template, containing all necessary information to fill.
28
 */
29
public class TemplateContext implements Serializable {
30

31
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
9✔
32

33
    private final ContextType contextType;
34
    private final Template template;
35
    private final String componentId;
36
    private final Map<String, String> params = new HashMap<>();
15✔
37
    private List<Component> components = new ArrayList<>();
15✔
38
    private final Map<IRI, IModel<?>> componentModels = new HashMap<>();
15✔
39
    private Set<IRI> introducedIris = new HashSet<>();
15✔
40
    private Set<IRI> embeddedIris = new HashSet<>();
15✔
41
    private List<StatementItem> statementItems;
42
    private Set<IRI> iriSet = new HashSet<>();
15✔
43
    private Map<IRI, StatementItem> narrowScopeMap = new HashMap<>();
15✔
44
    private String targetNamespace = Template.DEFAULT_TARGET_NAMESPACE;
9✔
45
    private Nanopub existingNanopub;
46
    private Nanopub fillSource;
47
    private Map<IRI, String> labels;
48
    private FillMode fillMode = null;
9✔
49

50
    /**
51
     * Constructor for creating a new template context for filling a template.
52
     *
53
     * @param contextType     the type of context
54
     * @param templateId      the ID of the template to fill
55
     * @param componentId     the ID of the component that will use this context
56
     * @param targetNamespace the target namespace for the template, can be null to use the default namespace
57
     */
58
    public TemplateContext(ContextType contextType, String templateId, String componentId, String targetNamespace) {
59
        this(contextType, templateId, componentId, targetNamespace, null);
21✔
60
    }
3✔
61

62
    /**
63
     * Constructor for creating a new template context for filling a template.
64
     *
65
     * @param contextType     the type of context
66
     * @param templateId      the ID of the template to fill
67
     * @param componentId     the ID of the component that will use this context
68
     * @param existingNanopub an existing nanopub to fill, can be null if creating a new nanopub
69
     */
70
    public TemplateContext(ContextType contextType, String templateId, String componentId, Nanopub existingNanopub) {
71
        this(contextType, templateId, componentId, null, existingNanopub);
×
72
    }
×
73

74
    private TemplateContext(ContextType contextType, String templateId, String componentId, String targetNamespace, Nanopub existingNanopub) {
6✔
75
        this.contextType = contextType;
9✔
76
        // TODO: check whether template is of correct type:
77
        this.template = TemplateData.get().getTemplate(templateId);
15✔
78
        this.componentId = componentId;
9✔
79
        if (targetNamespace != null) {
6✔
80
            this.targetNamespace = targetNamespace;
9✔
81
        }
82
        this.existingNanopub = existingNanopub;
9✔
83
        if (existingNanopub == null && NanodashSession.get().getUserIri() != null) {
15!
84
            componentModels.put(NTEMPLATE.CREATOR_PLACEHOLDER, Model.of(NanodashSession.get().getUserIri().stringValue()));
×
85
        }
86
    }
3✔
87

88
    /**
89
     * Initializes the statements for this context.
90
     */
91
    public void initStatements() {
92
        if (statementItems != null) return;
×
93
        statementItems = new ArrayList<>();
×
94
        for (IRI st : template.getStatementIris()) {
×
95
            StatementItem si = new StatementItem(componentId, st, this);
×
96
            statementItems.add(si);
×
97
            for (IRI i : si.getIriSet()) {
×
98
                if (iriSet.contains(i)) {
×
99
                    narrowScopeMap.remove(i);
×
100
                } else {
101
                    iriSet.add(i);
×
102
                    narrowScopeMap.put(i, si);
×
103
                }
104
            }
×
105
        }
×
106
    }
×
107

108
    /**
109
     * Finalizes the statements by processing all parameters and setting the repetition counts.
110
     */
111
    public void finalizeStatements() {
112
        Map<StatementItem, Integer> finalRepetitionCount = new HashMap<>();
×
113
        for (IRI ni : narrowScopeMap.keySet()) {
×
114
            // TODO: Move all occurrences of this to utility function:
115
            String postfix = Utils.getUriPostfix(ni);
×
116
            StatementItem si = narrowScopeMap.get(ni);
×
117
            int i = si.getRepetitionCount();
×
118
            while (true) {
119
                String p = postfix + "__" + i;
×
120
                if (hasParam(p)) {
×
121
                    si.addRepetitionGroup();
×
122
                } else {
123
                    break;
124
                }
125
                i++;
×
126
            }
×
127
            i = 1;
×
128
            int corr = 0;
×
129
            if (si.isEmpty()) corr = 1;
×
130
            while (true) {
131
                String p = postfix + "__." + i;
×
132
                if (hasParam(p)) {
×
133
                    int absPos = si.getRepetitionCount() + i - 1 - corr;
×
134
                    String param = postfix + "__" + absPos;
×
135
                    if (i - corr == 0) param = postfix;
×
136
                    setParam(param, getParam(p));
×
137
                    finalRepetitionCount.put(si, i - corr);
×
138
                } else {
139
                    break;
140
                }
141
                i++;
×
142
            }
×
143
        }
×
144
        for (StatementItem si : finalRepetitionCount.keySet()) {
×
145
            for (int i = 0; i < finalRepetitionCount.get(si); i++) {
×
146
                si.addRepetitionGroup();
×
147
            }
148
        }
×
149
        for (StatementItem si : statementItems) {
×
150
            si.finalizeValues();
×
151
        }
×
152
    }
×
153

154
    /**
155
     * Sets the fill mode for this context.
156
     *
157
     * @param fillMode the fill mode to set
158
     */
159
    public void setFillMode(FillMode fillMode) {
160
        this.fillMode = fillMode;
9✔
161
    }
3✔
162

163
    /**
164
     * Gets the fill mode for this context.
165
     *
166
     * @return the fill mode, or null if not set
167
     */
168
    public FillMode getFillMode() {
169
        return fillMode;
×
170
    }
171

172
    /**
173
     * Returns the type of context.
174
     *
175
     * @return the context type
176
     */
177
    public ContextType getType() {
178
        return contextType;
×
179
    }
180

181
    /**
182
     * Returns the template associated with this context.
183
     *
184
     * @return the template
185
     */
186
    public Template getTemplate() {
187
        return template;
×
188
    }
189

190
    /**
191
     * Returns the ID of the template associated with this context.
192
     *
193
     * @return the template ID
194
     */
195
    public String getTemplateId() {
196
        return template.getId();
×
197
    }
198

199
    /**
200
     * Sets a parameter for this context.
201
     *
202
     * @param name  the name of the parameter
203
     * @param value the value of the parameter
204
     */
205
    public void setParam(String name, String value) {
206
        params.put(name, value);
×
207
    }
×
208

209
    /**
210
     * Gets a parameter value by its name.
211
     *
212
     * @param name the name of the parameter
213
     * @return the value of the parameter, or null if not set
214
     */
215
    public String getParam(String name) {
216
        return params.get(name);
×
217
    }
218

219
    /**
220
     * Checks if a parameter with the given name exists.
221
     *
222
     * @param name the name of the parameter
223
     * @return true if the parameter exists, false otherwise
224
     */
225
    public boolean hasParam(String name) {
226
        return params.containsKey(name);
×
227
    }
228

229
    /**
230
     * Returns the components associated with this context.
231
     *
232
     * @return a list of components
233
     */
234
    public List<Component> getComponents() {
235
        return components;
×
236
    }
237

238
    /**
239
     * Returns the component models associated with this context.
240
     *
241
     * @return a map of IRI to model of strings
242
     */
243
    public Map<IRI, IModel<?>> getComponentModels() {
244
        return componentModels;
9✔
245
    }
246

247
    /**
248
     * Returns the introduced IRIs in this context.
249
     *
250
     * @return a set of introduced IRIs
251
     */
252
    public Set<IRI> getIntroducedIris() {
253
        return introducedIris;
×
254
    }
255

256
    /**
257
     * Returns the embedded IRIs in this context.
258
     *
259
     * @return a set of embedded IRIs
260
     */
261
    public Set<IRI> getEmbeddedIris() {
262
        return embeddedIris;
×
263
    }
264

265
    /**
266
     * Processes an IRI by applying the template's processing rules.
267
     *
268
     * @param iri the IRI to process
269
     * @return the processed IRI, or null if the processing results in no value
270
     */
271
    public IRI processIri(IRI iri) {
272
        Value v = processValue(iri);
×
273
        if (v == null) return null;
×
274
        if (v instanceof IRI) return (IRI) v;
×
275
        return iri;
×
276
    }
277

278
    /**
279
     * Processes a Value according to the template's rules.
280
     *
281
     * @param value the Value to process
282
     * @return the processed Value, or the original Value if no processing is applicable
283
     */
284
    public Value processValue(Value value) {
285
        if (!(value instanceof IRI)) {
9!
286
            return value;
×
287
        }
288
        IRI iri = (IRI) value;
9✔
289
        if (iri.equals(NTEMPLATE.CREATOR_PLACEHOLDER)) {
12!
290
            iri = NanodashSession.get().getUserIri();
×
291
        }
292
        if (iri.equals(NTEMPLATE.ASSERTION_PLACEHOLDER)) {
12!
293
            iri = vf.createIRI(targetNamespace + "assertion");
×
294
        } else if (iri.equals(NTEMPLATE.NANOPUB_PLACEHOLDER)) {
12!
295
            iri = vf.createIRI(targetNamespace);
×
296
        } else if (template.isRootNanopubPlaceholder(iri)) {
15!
297
            IModel<?> rootModel = componentModels.get(iri);
18✔
298
            String rootValue = (rootModel == null || rootModel.getObject() == null) ? "" : rootModel.getObject().toString();
33!
299
            if (rootValue.equals(LocalUri.of("nanopub").stringValue())) {
18✔
300
                // Sentinel: in supersede/derive mode this means the existing nanopub was the root
301
                Nanopub ref = getReferenceNanopub();
9✔
302
                if (ref != null && (fillMode == FillMode.SUPERSEDE || fillMode == FillMode.DERIVE)) {
18!
303
                    iri = vf.createIRI(ref.getUri().stringValue());
21✔
304
                } else {
305
                    iri = vf.createIRI(targetNamespace);
15✔
306
                }
307
            } else if (rootValue.isEmpty()) {
12✔
308
                iri = vf.createIRI(targetNamespace);
18✔
309
            } else {
310
                iri = vf.createIRI(rootValue);
12✔
311
            }
312
        }
313
        // TODO: Move this code below to the respective placeholder classes:
314
        IModel<?> tf = componentModels.get(iri);
18✔
315
        Value processedValue = null;
6✔
316
        Object tfObjectGeneric = null;
6✔
317
        if (tf != null) {
6!
318
            tfObjectGeneric = tf.getObject();
×
319
        }
320
        if (template.isRestrictedChoicePlaceholder(iri)) {
15!
321
            String tfObject = (String) tfObjectGeneric;
×
322
            if (tf != null && tfObject != null && !tfObject.isEmpty()) {
×
323
                String prefix = template.getPrefix(iri);
×
324
                if (prefix == null) prefix = "";
×
325
                if (template.isLocalResource(iri)) prefix = targetNamespace;
×
326
                if (tfObject.matches("https?://.+")) prefix = "";
×
327
                String v = prefix + tf.getObject();
×
328
                if (v.matches("[^:# ]+")) v = targetNamespace + v;
×
329
                if (v.matches("https?://.*")) {
×
330
                    processedValue = vf.createIRI(v);
×
331
                } else {
332
                    processedValue = vf.createLiteral(tfObject);
×
333
                }
334
            }
335
        } else if (template.isUriPlaceholder(iri)) {
15!
336
            String tfObject = (String) tfObjectGeneric;
×
337
            if (tf != null && tfObject != null && !tfObject.isEmpty()) {
×
338
                String prefix = template.getPrefix(iri);
×
339
                if (prefix == null) prefix = "";
×
340
                if (template.isLocalResource(iri)) prefix = targetNamespace;
×
341
                String v;
342
                if (template.isAutoEscapePlaceholder(iri)) {
×
343
                    v = prefix + Utils.urlEncode(tf.getObject());
×
344
                } else {
345
                    if (tfObject.matches("https?://.+")) prefix = "";
×
346
                    v = prefix + tf.getObject();
×
347
                }
348
                if (v.matches("[^:# ]+")) v = targetNamespace + v;
×
349
                processedValue = vf.createIRI(v);
×
350
            }
351
        } else if (template.isLocalResource(iri)) {
15!
352
            String tfObject = (String) tfObjectGeneric;
×
353
            if (template.isIntroducedResource(iri) && fillMode == FillMode.SUPERSEDE) {
×
354
                if (tf != null && tfObject != null && !tfObject.isEmpty()) {
×
355
                    processedValue = vf.createIRI(tfObject);
×
356
                }
357
            } else {
358
                String prefix = Utils.getUriPrefix(iri);
×
359
                processedValue = vf.createIRI(iri.stringValue().replace(prefix, targetNamespace));
×
360
            }
361
        } else if (template.isLiteralPlaceholder(iri)) {
15!
362
            IRI datatype = template.getDatatype(iri);
×
363
            String languageTag = template.getLanguageTag(iri);
×
364
            if (datatype != null) {
×
365
                if (datatype.equals(XSD.DATETIME)) {
×
366
                    ZonedDateTime tfObject = (ZonedDateTime) tfObjectGeneric;
×
367
                    if (tfObject != null) {
×
368
                        processedValue = vf.createLiteral(tfObject);
×
369
                    }
370
                } else if (datatype.equals(XSD.DATE)) {
×
371
                    Date tfObject = (Date) tfObjectGeneric;
×
372
                    if (tfObject != null) {
×
373
                        processedValue = vf.createLiteral(LiteralDateItem.format.format(tfObject), datatype);
×
374
                    }
375
                }
×
376
            } else {
377
                String tfObject = (String) tfObjectGeneric;
×
378
                if (tf != null && tfObject != null && !tfObject.isEmpty()) {
×
379
                    if (datatype != null) {
×
380
                        processedValue = vf.createLiteral(tfObject, datatype);
×
381
                    } else if (languageTag != null) {
×
382
                        processedValue = vf.createLiteral(tfObject, languageTag);
×
383
                    } else {
384
                        processedValue = vf.createLiteral(tfObject);
×
385
                    }
386
                }
387
            }
388
        } else if (template.isValuePlaceholder(iri)) {
15!
389
            String tfObject = (String) tfObjectGeneric;
×
390
            if (tf != null && tfObject != null && !tfObject.isEmpty()) {
×
391
                if (Utils.isValidLiteralSerialization(tfObject)) {
×
392
                    processedValue = Utils.getParsedLiteral(tfObject);
×
393
                } else {
394
                    String v = tfObject;
×
395
                    if (v.matches("[^:# ]+")) v = targetNamespace + v;
×
396
                    processedValue = vf.createIRI(v);
×
397
                }
398
            }
399
        } else if (template.isSequenceElementPlaceholder(iri)) {
15!
400
            String tfObject = (String) tfObjectGeneric;
×
401
            if (tf != null && tfObject != null && !tfObject.isEmpty()) {
×
402
                processedValue = vf.createIRI(tfObject);
×
403
            }
404
        } else {
×
405
            processedValue = iri;
6✔
406
        }
407
        if (processedValue instanceof IRI pvIri && template.isIntroducedResource(iri)) {
33!
408
            introducedIris.add(pvIri);
×
409
        }
410
        if (processedValue instanceof IRI pvIri && template.isEmbeddedResource(iri)) {
33!
411
            embeddedIris.add(pvIri);
×
412
        }
413
        return processedValue;
6✔
414
    }
415

416
    /**
417
     * Returns the statement items associated with this context.
418
     *
419
     * @return a list of StatementItem objects
420
     */
421
    public List<StatementItem> getStatementItems() {
422
        return statementItems;
×
423
    }
424

425
    /**
426
     * Propagates the statements from this context to a NanopubCreator.
427
     *
428
     * @param npCreator the NanopubCreator to which the statements will be added
429
     * @throws org.nanopub.MalformedNanopubException        if there is an error in the nanopub structure
430
     * @throws org.nanopub.NanopubAlreadyFinalizedException if the nanopub has already been finalized
431
     */
432
    public void propagateStatements(NanopubCreator npCreator) throws MalformedNanopubException, NanopubAlreadyFinalizedException {
433
        if (template.getNanopub() instanceof NanopubWithNs) {
×
434
            NanopubWithNs np = (NanopubWithNs) template.getNanopub();
×
435
            for (String p : np.getNsPrefixes()) {
×
436
                npCreator.addNamespace(p, np.getNamespace(p));
×
437
            }
×
438
        }
439
        for (StatementItem si : statementItems) {
×
440
            si.addTriplesTo(npCreator);
×
441
        }
×
442
    }
×
443

444
    /**
445
     * Checks if the context has a narrow scope for the given IRI.
446
     *
447
     * @param iri the IRI to check
448
     * @return true if there is a narrow scope for the IRI, false otherwise
449
     */
450
    public boolean hasNarrowScope(IRI iri) {
451
        return narrowScopeMap.containsKey(iri);
×
452
    }
453

454
    /**
455
     * Checks if any of the statement items in this context will match any triple.
456
     *
457
     * @return true if any statement item will match any triple, false otherwise
458
     */
459
    public boolean willMatchAnyTriple() {
460
        initStatements();
×
461
        for (StatementItem si : statementItems) {
×
462
            if (si.willMatchAnyTriple()) return true;
×
463
        }
×
464
        return false;
×
465
    }
466

467
    /**
468
     * Fills the context with statements, processing each StatementItem.
469
     *
470
     * @param statements the list of statements to fill
471
     * @throws UnificationException if there is an error during unification of statements
472
     */
473
    public void fill(List<Statement> statements) throws UnificationException {
474
        for (StatementItem si : statementItems) {
×
475
            si.fill(statements);
×
476
        }
×
477
        for (StatementItem si : statementItems) {
×
478
            si.fillFinished();
×
479
        }
×
480
    }
×
481

482
    /**
483
     * Returns the existing Nanopub associated with this context, if any.
484
     *
485
     * @return the existing Nanopub, or null if this context is for a new Nanopub
486
     */
487
    public Nanopub getExistingNanopub() {
488
        return existingNanopub;
9✔
489
    }
490

491
    /**
492
     * Returns the nanopub being used to fill this context (e.g. in supersede/derive
493
     * mode), if any. Unlike {@link #getExistingNanopub()} this does not imply the
494
     * context is read-only.
495
     *
496
     * @return the fill source nanopub, or null if none
497
     */
498
    public Nanopub getFillSource() {
499
        return fillSource;
9✔
500
    }
501

502
    /**
503
     * Sets the nanopub used to fill this context (e.g. in supersede/derive mode).
504
     *
505
     * @param fillSource the nanopub providing the values, or null to clear
506
     */
507
    public void setFillSource(Nanopub fillSource) {
508
        this.fillSource = fillSource;
9✔
509
    }
3✔
510

511
    /**
512
     * Returns the existing or fill-source nanopub for this context, preferring the
513
     * existing nanopub if set.
514
     *
515
     * @return the reference nanopub, or null if none
516
     */
517
    public Nanopub getReferenceNanopub() {
518
        return existingNanopub != null ? existingNanopub : fillSource;
18!
519
    }
520

521
    /**
522
     * Checks if this context is read-only.
523
     *
524
     * @return true if the context is read-only, false otherwise
525
     */
526
    public boolean isReadOnly() {
527
        return existingNanopub != null;
15!
528
    }
529

530
    /**
531
     * Returns the label for a given IRI, if available.
532
     *
533
     * @param iri the IRI for which to get the label
534
     * @return the label as a String, or null if no label is found
535
     */
536
    public String getLabel(IRI iri) {
537
        if (existingNanopub == null) return null;
×
538
        if (labels == null) {
×
539
            labels = new HashMap<>();
×
540
            for (Statement st : existingNanopub.getPubinfo()) {
×
541
                if (st.getPredicate().equals(NTEMPLATE.HAS_LABEL_FROM_API) || st.getPredicate().equals(RDFS.LABEL)) {
×
542
                    String label = st.getObject().stringValue();
×
543
                    labels.put((IRI) st.getSubject(), label);
×
544
                }
545
            }
×
546
        }
547
        return labels.get(iri);
×
548
    }
549

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