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

knowledgepixels / nanodash / 28111182502

24 Jun 2026 03:49PM UTC coverage: 26.584% (+0.03%) from 26.558%
28111182502

Pull #504

github

web-flow
Merge 5925d3a10 into 2a3e19dbb
Pull Request #504: fix: don't abort the whole template fill on one unification failure

1563 of 6931 branches covered (22.55%)

Branch coverage included in aggregate %.

3441 of 11892 relevant lines covered (28.94%)

4.25 hits per line

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

58.74
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 org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(TemplateContext.class);
9✔
32
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
9✔
33

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

468
    /**
469
     * Fills the context with statements, processing each StatementItem.
470
     *
471
     * @param statements the list of statements to fill
472
     * @throws UnificationException if there is an error during unification of statements
473
     */
474
    public void fill(List<Statement> statements) throws UnificationException {
475
        for (StatementItem si : statementItems) {
33✔
476
            // Isolate each statement: a unification failure on one must not abort the rest,
477
            // otherwise every later template statement is left unmatched. Roll back any partial
478
            // statement consumption so the next statement sees an intact pool.
479
            List<Statement> statementsBefore = new ArrayList<>(statements);
15✔
480
            try {
481
                si.fill(statements);
9✔
482
            } catch (UnificationException ex) {
×
483
                logger.warn("Could not fill statement; continuing with the remaining statements", ex);
×
484
                statements.clear();
×
485
                statements.addAll(statementsBefore);
×
486
            }
3✔
487
        }
3✔
488
        for (StatementItem si : statementItems) {
33✔
489
            si.fillFinished();
6✔
490
        }
3✔
491
    }
3✔
492

493
    /**
494
     * Returns the existing Nanopub associated with this context, if any.
495
     *
496
     * @return the existing Nanopub, or null if this context is for a new Nanopub
497
     */
498
    public Nanopub getExistingNanopub() {
499
        return existingNanopub;
9✔
500
    }
501

502
    /**
503
     * Returns the nanopub being used to fill this context (e.g. in supersede/derive
504
     * mode), if any. Unlike {@link #getExistingNanopub()} this does not imply the
505
     * context is read-only.
506
     *
507
     * @return the fill source nanopub, or null if none
508
     */
509
    public Nanopub getFillSource() {
510
        return fillSource;
9✔
511
    }
512

513
    /**
514
     * Sets the nanopub used to fill this context (e.g. in supersede/derive mode).
515
     *
516
     * @param fillSource the nanopub providing the values, or null to clear
517
     */
518
    public void setFillSource(Nanopub fillSource) {
519
        this.fillSource = fillSource;
9✔
520
    }
3✔
521

522
    /**
523
     * Returns the existing or fill-source nanopub for this context, preferring the
524
     * existing nanopub if set.
525
     *
526
     * @return the reference nanopub, or null if none
527
     */
528
    public Nanopub getReferenceNanopub() {
529
        return existingNanopub != null ? existingNanopub : fillSource;
18!
530
    }
531

532
    /**
533
     * Checks if this context is read-only.
534
     *
535
     * @return true if the context is read-only, false otherwise
536
     */
537
    public boolean isReadOnly() {
538
        return existingNanopub != null;
15!
539
    }
540

541
    /**
542
     * Returns the label for a given IRI, if available.
543
     *
544
     * @param iri the IRI for which to get the label
545
     * @return the label as a String, or null if no label is found
546
     */
547
    public String getLabel(IRI iri) {
548
        if (existingNanopub == null) return null;
×
549
        if (labels == null) {
×
550
            labels = new HashMap<>();
×
551
            for (Statement st : existingNanopub.getPubinfo()) {
×
552
                if (st.getPredicate().equals(NTEMPLATE.HAS_LABEL_FROM_API) || st.getPredicate().equals(RDFS.LABEL)) {
×
553
                    String label = st.getObject().stringValue();
×
554
                    labels.put((IRI) st.getSubject(), label);
×
555
                }
556
            }
×
557
        }
558
        return labels.get(iri);
×
559
    }
560

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