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

knowledgepixels / nanodash / 30350501786

28 Jul 2026 10:22AM UTC coverage: 33.452% (+3.2%) from 30.298%
30350501786

Pull #567

github

web-flow
Merge 7dc757397 into 7086521ef
Pull Request #567: Report and block templates with literals in subject/predicate position

2337 of 7695 branches covered (30.37%)

Branch coverage included in aggregate %.

4534 of 12845 relevant lines covered (35.3%)

5.45 hits per line

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

81.7
src/main/java/com/knowledgepixels/nanodash/component/StatementItem.java
1
package com.knowledgepixels.nanodash.component;
2

3
import com.knowledgepixels.nanodash.template.ContextType;
4
import com.knowledgepixels.nanodash.template.Template;
5
import com.knowledgepixels.nanodash.template.TemplateContext;
6
import com.knowledgepixels.nanodash.template.UnificationException;
7
import org.apache.wicket.AttributeModifier;
8
import org.apache.wicket.Component;
9
import org.apache.wicket.ajax.AjaxEventBehavior;
10
import org.apache.wicket.ajax.AjaxRequestTarget;
11
import org.apache.wicket.behavior.AttributeAppender;
12
import org.apache.wicket.markup.html.WebMarkupContainer;
13
import org.apache.wicket.markup.html.basic.Label;
14
import org.apache.wicket.markup.html.form.FormComponent;
15
import org.apache.wicket.markup.html.list.ListItem;
16
import org.apache.wicket.markup.html.list.ListView;
17
import org.apache.wicket.markup.html.panel.Panel;
18
import org.apache.wicket.model.IModel;
19
import org.eclipse.rdf4j.model.IRI;
20
import org.eclipse.rdf4j.model.Statement;
21
import org.eclipse.rdf4j.model.Value;
22
import org.eclipse.rdf4j.model.ValueFactory;
23
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
24
import org.nanopub.MalformedNanopubException;
25
import org.nanopub.NanopubAlreadyFinalizedException;
26
import org.nanopub.NanopubCreator;
27
import org.nanopub.vocabulary.NTEMPLATE;
28
import org.slf4j.Logger;
29
import org.slf4j.LoggerFactory;
30

31
import java.io.Serializable;
32
import java.util.*;
33

34
/**
35
 * Represents a single item in a statement, which can be a subject, predicate, or object.
36
 */
37
public class StatementItem extends Panel {
38

39
    private TemplateContext context;
40
    private IRI statementId;
41
    private List<IRI> statementPartIds = new ArrayList<>();
15✔
42
    private List<WebMarkupContainer> viewElements = new ArrayList<>();
15✔
43
    private List<RepetitionGroup> repetitionGroups = new ArrayList<>();
15✔
44
    private boolean repetitionGroupsChanged = true;
9✔
45
    private Set<IRI> iriSet = new HashSet<>();
15✔
46
    private boolean isMatched = false;
9✔
47
    private static final Logger logger = LoggerFactory.getLogger(StatementItem.class);
9✔
48

49
    /**
50
     * Constructor for creating a StatementItem with a specific ID and statement ID.
51
     *
52
     * @param id          the Wicket component ID
53
     * @param statementId the IRI of the statement this item represents
54
     * @param context     the template context containing information about the template and its items
55
     */
56
    public StatementItem(String id, IRI statementId, TemplateContext context) {
57
        super(id);
9✔
58

59
        this.statementId = statementId;
9✔
60
        this.context = context;
9✔
61
        setOutputMarkupId(true);
12✔
62

63
        if (isGrouped()) {
9✔
64
            statementPartIds.addAll(getTemplate().getStatementIris(statementId));
27✔
65
        } else {
66
            statementPartIds.add(statementId);
15✔
67
        }
68

69
        addRepetitionGroup();
6✔
70

71
        ListView<WebMarkupContainer> v = new ListView<WebMarkupContainer>("statement-group", viewElements) {
48✔
72

73
            @Override
74
            protected void populateItem(ListItem<WebMarkupContainer> item) {
75
                item.add(item.getModelObject());
33✔
76
            }
3✔
77

78
        };
79
        v.setOutputMarkupId(true);
12✔
80
        add(v);
27✔
81
    }
3✔
82

83
    /**
84
     * Adds a new repetition group to this StatementItem with a default RepetitionGroup.
85
     */
86
    public void addRepetitionGroup() {
87
        addRepetitionGroup(new RepetitionGroup());
18✔
88
    }
3✔
89

90
    /**
91
     * Adds a new repetition group to this StatementItem.
92
     *
93
     * @param rg the RepetitionGroup to add
94
     */
95
    public void addRepetitionGroup(RepetitionGroup rg) {
96
        repetitionGroups.add(rg);
15✔
97
        repetitionGroupsChanged = true;
9✔
98
    }
3✔
99

100
    /**
101
     * {@inheritDoc}
102
     */
103
    @Override
104
    protected void onBeforeRender() {
105
        if (repetitionGroupsChanged) {
9!
106
            updateViewElements();
6✔
107
            finalizeValues();
6✔
108
        }
109
        repetitionGroupsChanged = false;
9✔
110
        super.onBeforeRender();
6✔
111
    }
3✔
112

113
    private void updateViewElements() {
114
        viewElements.clear();
9✔
115
        boolean first = true;
6✔
116
        for (RepetitionGroup r : repetitionGroups) {
33✔
117
            if (isGrouped() && !first) {
9!
118
                viewElements.add(new HorizontalLine("statement"));
×
119
            }
120
            viewElements.addAll(r.getShownStatementParts());
18✔
121
            boolean isOnly = repetitionGroups.size() == 1;
24!
122
            boolean isLast = repetitionGroups.get(repetitionGroups.size() - 1) == r;
39!
123
            r.addRepetitionButton.setVisible(!context.isReadOnly() && isRepeatable() && isLast);
48!
124
            r.removeRepetitionButton.setVisible(!context.isReadOnly() && isRepeatable() && !isOnly);
42!
125
            r.optionalMark.setVisible(isOnly);
15✔
126
            first = false;
6✔
127
        }
3✔
128
        String htmlClassString = "";
6✔
129
        if (!context.isReadOnly()) {
12!
130
            if (isOptional()) {
9!
131
                htmlClassString += "nanopub-optional ";
×
132
            }
133
            if (isAdvanced()) {
9!
134
                htmlClassString += "advanced ";
×
135
            }
136
        }
137
        boolean singleItem = context.getStatementItems().size() == 1;
27!
138
        boolean repeatableOrRepeated = (!context.isReadOnly() && isRepeatable()) || (context.isReadOnly() && getRepetitionCount() > 1);
45!
139
        if ((isGrouped() || repeatableOrRepeated) && !singleItem) {
21!
140
            htmlClassString += "nanopub-group ";
×
141
        }
142
        if (!htmlClassString.isEmpty()) {
9!
143
            add(new AttributeModifier("class", htmlClassString));
×
144
        }
145
    }
3✔
146

147
    /**
148
     * Adds the triples of this statement item to the given NanopubCreator.
149
     *
150
     * @param npCreator the NanopubCreator to which the triples will be added
151
     * @throws org.nanopub.MalformedNanopubException        if the statement item is not properly set up
152
     * @throws org.nanopub.NanopubAlreadyFinalizedException if the NanopubCreator has already been finalized
153
     */
154
    public void addTriplesTo(NanopubCreator npCreator) throws MalformedNanopubException, NanopubAlreadyFinalizedException {
155
        if (hasEmptyElements()) {
9✔
156
            if (isOptional()) {
9✔
157
                return;
3✔
158
            } else {
159
                throw new MalformedNanopubException("Field of statement not set.");
15✔
160
            }
161
        }
162
        for (RepetitionGroup rg : repetitionGroups) {
33✔
163
            rg.addTriplesTo(npCreator);
9✔
164
        }
3✔
165
    }
3✔
166

167
    private Template getTemplate() {
168
        return context.getTemplate();
12✔
169
    }
170

171
    /**
172
     * Returns the number of the repetition groups for this statement item.
173
     *
174
     * @return the number of repetition groups
175
     */
176
    public int getRepetitionCount() {
177
        return repetitionGroups.size();
12✔
178
    }
179

180
    /**
181
     * Returns whether the statement is optional.
182
     */
183
    public boolean isOptional() {
184
        return repetitionGroups.size() == 1 && getTemplate().isOptionalStatement(statementId);
45!
185
    }
186

187
    /**
188
     * Returns whether the statement is advanced.
189
     */
190
    public boolean isAdvanced() {
191
        return getTemplate().isAdvancedStatement(statementId);
18✔
192
    }
193

194
    /**
195
     * Checks if this statement item is grouped.
196
     *
197
     * @return true if the statement item is grouped, false otherwise
198
     */
199
    public boolean isGrouped() {
200
        return getTemplate().isGroupedStatement(statementId);
18✔
201
    }
202

203
    /**
204
     * Checks if this statement item is repeatable.
205
     *
206
     * @return true if the statement item is repeatable, false otherwise
207
     */
208
    public boolean isRepeatable() {
209
        return getTemplate().isRepeatableStatement(statementId);
18✔
210
    }
211

212
    /**
213
     * Checks if this statement item has empty elements.
214
     *
215
     * @return true if any of the repetition groups has empty elements, false otherwise
216
     */
217
    public boolean hasEmptyElements() {
218
        return repetitionGroups.get(0).hasEmptyElements();
21✔
219
    }
220

221
    /**
222
     * Returns the set of IRIs associated with this statement item.
223
     *
224
     * @return a set of IRIs
225
     */
226
    public Set<IRI> getIriSet() {
227
        return iriSet;
9✔
228
    }
229

230
    /**
231
     * Checks if this statement item will match any triple.
232
     *
233
     * @return true if it will match any triple, false otherwise
234
     */
235
    public boolean willMatchAnyTriple() {
236
        return repetitionGroups.get(0).matches(dummyStatementList);
×
237
    }
238

239
    /**
240
     * Fills this statement item with the provided list of statements, matching them against the repetition groups.
241
     *
242
     * @param statements the list of statements to match against
243
     * @throws com.knowledgepixels.nanodash.template.UnificationException if the statements cannot be unified with this statement item
244
     */
245
    public void fill(List<Statement> statements) throws UnificationException {
246
        if (isMatched) return;
9!
247
        if (repetitionGroups.size() == 1) {
15!
248
            RepetitionGroup rg = repetitionGroups.get(0);
18✔
249
            if (rg.matches(statements)) {
12✔
250
                rg.fill(statements);
12✔
251
            } else {
252
                return;
3✔
253
            }
254
        } else {
3✔
255
            return;
×
256
        }
257
        isMatched = true;
9✔
258
        if (!isRepeatable()) return;
12✔
259
        while (true) {
260
            Set<IRI> modelsBefore = new HashSet<>(context.getComponentModels().keySet());
24✔
261
            List<Statement> statementsBefore = new ArrayList<>(statements);
15✔
262
            RepetitionGroup newGroup = new RepetitionGroup();
15✔
263
            boolean filled;
264
            if (newGroup.matches(statements)) {
12✔
265
                try {
266
                    newGroup.fill(statements);
9✔
267
                    filled = true;
6✔
268
                } catch (UnificationException ex) {
×
269
                    // matches() validates unifiability statelessly, but fill() mutates shared
270
                    // placeholder models via unifyWith as it goes, so binding an earlier part can
271
                    // make a later one non-unifiable ("seemed to work but then didn't"). Treat this
272
                    // like end-of-repetitions rather than letting it abort the whole template fill:
273
                    // roll back the partial statement consumption and discard the trial group.
274
                    logger.warn("Repetition fill failed after matches() succeeded for {}; stopping repetitions of this statement", statementId, ex);
×
275
                    statements.clear();
×
276
                    statements.addAll(statementsBefore);
×
277
                    filled = false;
×
278
                }
3✔
279
            } else {
280
                filled = false;
6✔
281
            }
282
            if (!filled) {
6✔
283
                newGroup.disconnect();
6✔
284
                // The trial group's constructor registered fresh (empty) component
285
                // models for its narrow-scope placeholders (e.g. public-key__N). If
286
                // left behind, a later real repetition group at the same index reuses
287
                // the stale empty model and skips param seeding — the "derive new
288
                // introduction" empty-fields bug. Drop exactly the models this trial
289
                // group added; shared (wide-scope) models existed before and stay.
290
                context.getComponentModels().keySet().retainAll(modelsBefore);
21✔
291
                return;
3✔
292
            }
293
            addRepetitionGroup(newGroup);
9✔
294
        }
3✔
295
    }
296

297
    /**
298
     * Marks the filling of this statement item as finished, indicating that all values have been filled.
299
     */
300
    public void fillFinished() {
301
        for (RepetitionGroup rg : repetitionGroups) {
33✔
302
            rg.fillFinished();
6✔
303
        }
3✔
304
    }
3✔
305

306
    /**
307
     * Finalizes the values of all ValueItems in this statement item.
308
     */
309
    public void finalizeValues() {
310
        for (RepetitionGroup rg : repetitionGroups) {
33✔
311
            rg.finalizeValues();
6✔
312
        }
3✔
313
    }
3✔
314

315
    /**
316
     * Returns true if the statement item has been matched with a set of statements.
317
     *
318
     * @return true if matched, false otherwise
319
     */
320
    public boolean isMatched() {
321
        return isMatched;
9✔
322
    }
323

324
    /**
325
     * Checks if this statement item is empty, meaning it has no filled repetition groups.
326
     *
327
     * @return true if the statement item is empty, false otherwise
328
     */
329
    public boolean isEmpty() {
330
        return repetitionGroups.size() == 1 && repetitionGroups.get(0).isEmpty();
48✔
331
    }
332

333
    /**
334
     * Represents a group of repetitions for a statement item, containing multiple statement parts.
335
     */
336
    public class RepetitionGroup implements Serializable {
337

338
        private List<StatementPartItem> statementParts;
339
        private List<ValueItem> localItems = new ArrayList<>();
15✔
340
        private boolean filled = false;
9✔
341
        // Optional group members that were left unassigned by a successful fill();
342
        // read-only rendering hides these rows:
343
        private Set<IRI> unmatchedParts = new HashSet<>();
15✔
344

345
        private List<ValueItem> items = new ArrayList<>();
15✔
346

347
        Label addRepetitionButton, removeRepetitionButton, optionalMark;
348

349
        /**
350
         * Constructor for creating a RepetitionGroup.
351
         */
352
        public RepetitionGroup() {
15✔
353
            statementParts = new ArrayList<>();
15✔
354
            for (IRI s : statementPartIds) {
33✔
355
                // Declared subject/predicate: a template can (invalidly) put a literal there,
356
                // and rendering it shows the user what is wrong; Template.getStatementErrors()
357
                // spells it out.
358
                StatementPartItem statement = new StatementPartItem("statement",
18✔
359
                        makeValueItem("subj", getTemplate().getDeclaredSubject(s), s),
24✔
360
                        makeValueItem("pred", getTemplate().getDeclaredPredicate(s), s),
24✔
361
                        makeValueItem("obj", getTemplate().getObject(s), s)
21✔
362
                );
363
                statementParts.add(statement);
15✔
364

365
                // Some of the methods of StatementItem and RepetitionGroup don't work properly before this
366
                // object is fully instantiated:
367
                boolean isFirstGroup = repetitionGroups.isEmpty();
12✔
368
                boolean isFirstLine = statementParts.size() == 1;
27✔
369
                boolean isLastLine = statementParts.size() == statementPartIds.size();
33✔
370
                boolean isOptional = getTemplate().isOptionalStatement(statementId);
18✔
371

372
                if (statementParts.size() == 1 && !isFirstGroup) {
21✔
373
                    statement.add(new AttributeAppender("class", " separate-statement"));
39✔
374
                }
375

376
                // This code adds "advanced" marks similar to "optional":
377
//                if (!context.isReadOnly()) {
378
//                    if (isOptional && isLastLine) {
379
//                        if (isAdvanced()) {
380
//                            optionalMark = new Label("label", "(optional, advanced)");
381
//                        } else {
382
//                            optionalMark = new Label("label", "(optional)");
383
//                        }
384
//                    } else if (isAdvanced()) {
385
//                        optionalMark = new Label("label", "(advanced)");
386
//                    } else {
387
//                        optionalMark = new Label("label", "");
388
//                        optionalMark.setVisible(false);
389
//                    }
390
//                } else {
391
//                    optionalMark = new Label("label", "");
392
//                    optionalMark.setVisible(false);
393
//                }
394

395
                boolean isMemberOptional = isGrouped() && getTemplate().isOptionalStatement(s);
36✔
396

397
                if (!context.isReadOnly() && isOptional && isLastLine) {
24✔
398
                    optionalMark = new Label("label", "(optional)");
24✔
399
                } else {
400
                    optionalMark = new Label("label", "");
21✔
401
                    optionalMark.setVisible(false);
15✔
402
                }
403
                statement.add(optionalMark);
30✔
404
                // Member-level mark, rendered inline on the member's own line; unlike the
405
                // group-level mark it holds per repetition, so it is never toggled off:
406
                Label partOptionalMark = new Label("part-label", "(optional)");
18✔
407
                partOptionalMark.setVisible(!context.isReadOnly() && isMemberOptional);
36✔
408
                statement.add(partOptionalMark);
27✔
409
                if (!context.isReadOnly() && isMemberOptional) {
18✔
410
                    statement.add(new AttributeAppender("class", " nanopub-optional-part"));
39✔
411
                }
412
                if (isLastLine) {
6✔
413
                    addRepetitionButton = new Label("add-repetition", "+");
21✔
414
                    statement.add(addRepetitionButton);
30✔
415
                    addRepetitionButton.add(new AjaxEventBehavior("click") {
74✔
416

417
                        @Override
418
                        protected void onEvent(AjaxRequestTarget target) {
419
                            addRepetitionGroup(new RepetitionGroup());
×
420
                            target.add(StatementItem.this);
×
421
                            target.appendJavaScript("updateElements();");
×
422
                        }
×
423

424
                    });
425
                } else {
426
                    statement.add(new Label("add-repetition", "").setVisible(false));
45✔
427
                }
428
                if (isFirstLine) {
6✔
429
                    removeRepetitionButton = new Label("remove-repetition", "-");
21✔
430
                    statement.add(removeRepetitionButton);
30✔
431
                    removeRepetitionButton.add(new AjaxEventBehavior("click") {
74✔
432

433
                        @Override
434
                        protected void onEvent(AjaxRequestTarget target) {
435
                            RepetitionGroup.this.remove();
×
436
                            target.appendJavaScript("updateElements();");
×
437
                            target.add(StatementItem.this);
×
438
                        }
×
439

440
                    });
441
                } else {
442
                    statement.add(new Label("remove-repetition", "").setVisible(false));
45✔
443
                }
444
            }
3✔
445
        }
3✔
446

447
        private ValueItem makeValueItem(String id, Value value, IRI statementPartId) {
448
            if (isFirst() && value instanceof IRI) {
18✔
449
                iriSet.add((IRI) value);
21✔
450
            }
451
            ValueItem vi = new ValueItem(id, transform(value), statementPartId, this);
30✔
452
            localItems.add(vi);
15✔
453
            items.add(vi);
15✔
454
            return vi;
6✔
455
        }
456

457
        private void disconnect() {
458
            for (ValueItem vi : new ArrayList<>(localItems)) {
42✔
459
                // TODO These remove operations on list are slow. Improve:
460
                localItems.remove(vi);
15✔
461
                items.remove(vi);
15✔
462
                vi.removeFromContext();
6✔
463
            }
3✔
464
        }
3✔
465

466
        /**
467
         * Returns the statement parts.
468
         *
469
         * @return a list of StatementPartItem objects representing the statement parts
470
         */
471
        public List<StatementPartItem> getStatementParts() {
472
            return statementParts;
×
473
        }
474

475
        /**
476
         * Returns the statement parts to render: in read-only contexts, optional group
477
         * members that were left unassigned by fill() are hidden; in editable contexts
478
         * (fresh publish, derive, update) all parts are shown, with skipped members
479
         * rendering as empty fields.
480
         *
481
         * @return a list of StatementPartItem objects to render
482
         */
483
        private List<StatementPartItem> getShownStatementParts() {
484
            if (!context.isReadOnly() || unmatchedParts.isEmpty()) return statementParts;
24!
485
            List<StatementPartItem> shown = new ArrayList<>();
×
486
            for (int i = 0; i < statementParts.size(); i++) {
×
487
                if (!unmatchedParts.contains(statementPartIds.get(i))) {
×
488
                    shown.add(statementParts.get(i));
×
489
                }
490
            }
491
            return shown;
×
492
        }
493

494
        /**
495
         * Returns the index of this repetition group in the list of repetition groups.
496
         *
497
         * @return the index of this repetition group
498
         */
499
        public int getRepeatIndex() {
500
            if (!repetitionGroups.contains(this)) return repetitionGroups.size();
33✔
501
            return repetitionGroups.indexOf(this);
18✔
502
        }
503

504
        /**
505
         * Returns the total number of repetition groups for this statement.
506
         *
507
         * @return the number of repetition groups
508
         */
509
        public int getRepetitionCount() {
510
            return StatementItem.this.getRepetitionCount();
×
511
        }
512

513
        /**
514
         * Returns true if the repeat index if the first one.
515
         *
516
         * @return true if the repeat index is 0, false otherwise
517
         */
518
        public boolean isFirst() {
519
            return getRepeatIndex() == 0;
21✔
520
        }
521

522
        /**
523
         * Returns true if the repeat index is the last one.
524
         *
525
         * @return true if the repeat index is the last one, false otherwise
526
         */
527
        public boolean isLast() {
528
            return getRepeatIndex() == repetitionGroups.size() - 1;
×
529
        }
530

531
        private void remove() {
532
            String thisSuffix = getRepeatSuffix();
9✔
533
            for (IRI iriBase : iriSet) {
36✔
534
                // Shift the value model and any derived model (e.g. the language-tag
535
                // model of a language-tag-selectable literal placeholder) alike.
536
                for (String derived : new String[]{"", TemplateContext.LANGUAGE_SUFFIX}) {
75✔
537
                    IRI thisIri = vf.createIRI(iriBase + thisSuffix + derived);
24✔
538
                    if (context.getComponentModels().containsKey(thisIri)) {
21✔
539
                        IModel swapModel1 = (IModel) context.getComponentModels().get(thisIri);
24✔
540
                        for (int i = getRepeatIndex() + 1; i < repetitionGroups.size(); i++) {
39✔
541
                            IModel swapModel2 = (IModel) context.getComponentModels().get(vf.createIRI(iriBase + getRepeatSuffix(i) + derived));
48✔
542
                            if (swapModel1 != null && swapModel2 != null) {
12!
543
                                swapModel1.setObject(swapModel2.getObject());
12✔
544
                            }
545
                            // Drop any retained rawInput so the shifted model value is rendered
546
                            // instead of the user's previous (post-validation-error) entry.
547
                            clearInputForModel(swapModel1);
9✔
548
                            swapModel1 = swapModel2;
6✔
549
                        }
550
                        if (swapModel1 != null) {
6!
551
                            swapModel1.setObject(null);
9✔
552
                            clearInputForModel(swapModel1);
9✔
553
                        }
554
                    }
555
                }
556
            }
3✔
557
            RepetitionGroup lastGroup = repetitionGroups.get(repetitionGroups.size() - 1);
36✔
558
            repetitionGroups.remove(lastGroup);
18✔
559
            for (ValueItem vi : lastGroup.items) {
33✔
560
                vi.removeFromContext();
6✔
561
            }
3✔
562
            repetitionGroupsChanged = true;
12✔
563
        }
3✔
564

565
        private void clearInputForModel(IModel<?> model) {
566
            if (model == null) return;
6!
567
            for (Component c : context.getComponents()) {
39✔
568
                if (c instanceof FormComponent && c.getDefaultModel() == model) {
21!
569
                    ((FormComponent<?>) c).clearInput();
9✔
570
                }
571
            }
3✔
572
        }
3✔
573

574
        private String getRepeatSuffix() {
575
            return getRepeatSuffix(getRepeatIndex());
15✔
576
        }
577

578
        private String getRepeatSuffix(int i) {
579
            if (i == 0) return "";
12✔
580
            // TODO: Check that this double-underscore pattern isn't used otherwise:
581
            return "__" + i;
9✔
582
        }
583

584
        /**
585
         * Returns the template context associated.
586
         *
587
         * @return the TemplateContext
588
         */
589
        public TemplateContext getContext() {
590
            return context;
12✔
591
        }
592

593
        /**
594
         * Checks if this repetition group is optional.
595
         *
596
         * @return true if the repetition group is optional, false otherwise
597
         */
598
        public boolean isOptional() {
599
            if (!getTemplate().isOptionalStatement(statementId)) return false;
30✔
600
            if (repetitionGroups.size() == 0) return true;
21✔
601
            if (repetitionGroups.size() == 1 && repetitionGroups.get(0) == this) return true;
39!
602
            return false;
6✔
603
        }
604

605
        /**
606
         * Checks if the given member statement is effectively optional in this repetition group:
607
         * either the whole group is optional or the member itself carries the optional flag.
608
         * Unlike group-level optionality, the member-level flag holds per repetition, so it is
609
         * not affected by the number of repetition groups.
610
         *
611
         * @param partId the IRI of the member statement to check
612
         * @return true if the member statement is effectively optional, false otherwise
613
         */
614
        public boolean isOptionalPart(IRI partId) {
615
            return isOptional() || getTemplate().isOptionalStatement(partId);
39✔
616
        }
617

618
        private Value transform(Value value) {
619
            if (!(value instanceof IRI)) {
9✔
620
                return value;
6✔
621
            }
622
            IRI iri = (IRI) value;
9✔
623
            String iriString = iri.stringValue();
9✔
624
            iriString = iriString.replaceAll("~~ARTIFACTCODE~~", "~~~ARTIFACTCODE~~~");
15✔
625
            // Only add "__N" to URI from second repetition group on; for the first group, information about
626
            // narrow scopes is not yet complete.
627
            if (getRepeatIndex() > 0 && context.hasNarrowScope(iri)) {
27✔
628
                if (context.getTemplate().isPlaceholder(iri) || context.getTemplate().isLocalResource(iri)) {
42!
629
                    iriString += getRepeatSuffix();
15✔
630
                }
631
            }
632
            return vf.createIRI(iriString);
12✔
633
        }
634

635
        /**
636
         * Adds the triples of this repetition group to the given NanopubCreator.
637
         *
638
         * @param npCreator the NanopubCreator to which the triples will be added
639
         * @throws org.nanopub.NanopubAlreadyFinalizedException if the NanopubCreator has already been finalized
640
         */
641
        public void addTriplesTo(NanopubCreator npCreator) throws NanopubAlreadyFinalizedException {
642
            Template t = getTemplate();
12✔
643
            for (IRI s : statementPartIds) {
36✔
644
                IRI subj = context.processIri((IRI) transform(t.getSubject(s)));
33✔
645
                IRI pred = context.processIri((IRI) transform(t.getPredicate(s)));
33✔
646
                Value obj = context.processValue(transform(t.getObject(s)));
30✔
647
                if (isGrouped() && t.isOptionalStatement(s) && (subj == null || pred == null || obj == null)) {
42!
648
                    // Optional group member without all elements resolved: drop just this
649
                    // triple; the rest of the group is still emitted.
650
                    continue;
3✔
651
                }
652
                if (context.getType() == ContextType.ASSERTION) {
18!
653
                    npCreator.addAssertionStatement(subj, pred, obj);
18✔
654
                } else if (context.getType() == ContextType.PROVENANCE) {
×
655
                    npCreator.addProvenanceStatement(subj, pred, obj);
×
656
                } else if (context.getType() == ContextType.PUBINFO) {
×
657
                    npCreator.addPubinfoStatement(subj, pred, obj);
×
658
                }
659
            }
3✔
660
            for (ValueItem vi : items) {
33✔
661
                if (vi.getComponent() instanceof GuidedChoiceItem) {
12!
662
                    String value = ((GuidedChoiceItem) vi.getComponent()).getModel().getObject();
×
663
                    if (value != null && GuidedChoiceItem.getLabel(value) != null) {
×
664
                        String label = GuidedChoiceItem.getLabel(value);
×
665
                        if (label.length() > 1000) label = label.substring(0, 997) + "...";
×
666
                        try {
667
                            npCreator.addPubinfoStatement(vf.createIRI(value), NTEMPLATE.HAS_LABEL_FROM_API, vf.createLiteral(label));
×
668
                        } catch (IllegalArgumentException ex) {
×
669
                            logger.error("Could not create IRI from value: {}", value, ex);
×
670
                        }
×
671
                    }
672
                }
673
            }
3✔
674
        }
3✔
675

676
        private boolean hasEmptyElements() {
677
            for (IRI s : statementPartIds) {
36✔
678
                // Optional group members may stay empty; they are dropped at triple-creation
679
                // time and must not block or drop the rest of the group:
680
                if (isGrouped() && getTemplate().isOptionalStatement(s)) continue;
33✔
681
                if (context.processIri((IRI) transform(getTemplate().getSubject(s))) == null) return true;
45✔
682
                if (context.processIri((IRI) transform(getTemplate().getPredicate(s))) == null) return true;
39!
683
                if (context.processValue(transform(getTemplate().getObject(s))) == null) return true;
42✔
684
            }
3✔
685
            return false;
6✔
686
        }
687

688
        /**
689
         * Checks if this repetition group is empty, meaning it has no filled items.
690
         *
691
         * @return true if the repetition group is empty, false otherwise
692
         */
693
        public boolean isEmpty() {
694
            for (IRI s : statementPartIds) {
36✔
695
                Template t = getTemplate();
12✔
696
                IRI subj = t.getSubject(s);
12✔
697
                if (t.isPlaceholder(subj) && context.hasNarrowScope(subj) && context.processIri((IRI) transform(subj)) != null)
57✔
698
                    return false;
6✔
699
                IRI pred = t.getPredicate(s);
12✔
700
                if (t.isPlaceholder(pred) && context.hasNarrowScope(pred) && context.processIri((IRI) transform(pred)) != null)
12!
701
                    return false;
×
702
                Value obj = t.getObject(s);
12✔
703
                if (obj instanceof IRI && t.isPlaceholder((IRI) obj) && context.hasNarrowScope((IRI) obj) && context.processValue(transform(obj)) != null)
69!
704
                    return false;
6✔
705
            }
3✔
706
            return true;
6✔
707
        }
708

709
        /**
710
         * Checks if this repetition group matches the provided list of statements.
711
         *
712
         * @param statements the list of statements to match against
713
         * @return true if the repetition group matches, false otherwise
714
         */
715
        public boolean matches(List<Statement> statements) {
716
            if (filled) return false;
9!
717
            // matches() must agree with fill(): because fill() binds shared placeholder models as it
718
            // goes, a valid assignment can only be confirmed by actually simulating those bindings.
719
            // We do exactly that (with backtracking) but on a copy and then restore the models, so
720
            // matches() stays side-effect free.
721
            Map<IRI, Object> snapshot = snapshotModels();
9✔
722
            Set<IRI> unmatchedBefore = new HashSet<>(unmatchedParts);
18✔
723
            try {
724
                List<Statement> copy = new ArrayList<>(statements);
15✔
725
                // A match must consume at least one statement: with optional members, an
726
                // assignment that skips every part would otherwise "match" without evidence,
727
                // which would also keep the repetition loop in fill() spinning forever.
728
                return assignParts(0, copy) && copy.size() < statements.size();
48!
729
            } finally {
730
                restoreModels(snapshot);
9✔
731
                unmatchedParts.clear();
9✔
732
                unmatchedParts.addAll(unmatchedBefore);
15✔
733
            }
734
        }
735

736
        /**
737
         * Fills this repetition group with the provided list of statements, unifying them with the statement parts.
738
         *
739
         * @param statements the list of statements to match against
740
         * @throws UnificationException if the statements cannot be unified with this repetition group
741
         */
742
        public void fill(List<Statement> statements) throws UnificationException {
743
            if (filled) throw new UnificationException("Already filled");
9!
744
            // Backtracking assignment: a greedy first-match can bind a shared placeholder in a way
745
            // that blocks a later part even though a consistent assignment exists. assignParts tries
746
            // alternatives and rolls back the model bindings between attempts. On success the matched
747
            // statements are removed from the list and the winning bindings are kept.
748
            unmatchedParts.clear();
9✔
749
            int sizeBefore = statements.size();
9✔
750
            if (!assignParts(0, statements) || statements.size() == sizeBefore) {
27!
751
                throw new UnificationException("Unification seemed to work but then didn't");
×
752
            }
753
            filled = true;
9✔
754
        }
3✔
755

756
        /**
757
         * Tries to assign each remaining statement part (from index {@code partIndex} on) to a distinct
758
         * statement in {@code available} such that all bindings of shared placeholder models are mutually
759
         * consistent. Uses backtracking: on a failed branch the model bindings are restored and the next
760
         * candidate is tried. On success the chosen statements are removed from {@code available} and the
761
         * winning bindings remain applied to the component models.
762
         *
763
         * @param partIndex the index of the next statement part to assign
764
         * @param available the statements still available for assignment (mutated in place)
765
         * @return true if all remaining parts could be assigned consistently
766
         */
767
        private boolean assignParts(int partIndex, List<Statement> available) {
768
            if (partIndex == statementParts.size()) return true;
21✔
769
            StatementPartItem p = statementParts.get(partIndex);
18✔
770
            for (int i = 0; i < available.size(); i++) {
24✔
771
                Statement s = available.get(i);
15✔
772
                Map<IRI, Object> snapshot = snapshotModels();
9✔
773
                if (unifyPart(p.getPredicate(), s.getPredicate())  // checking predicate first optimizes performance
27✔
774
                        && unifyPart(p.getSubject(), s.getSubject())
21✔
775
                        && unifyPart(p.getObject(), s.getObject())) {
15✔
776
                    available.remove(i);
12✔
777
                    if (assignParts(partIndex + 1, available)) return true;
27✔
778
                    available.add(i, s);
12✔
779
                }
780
                restoreModels(snapshot);
9✔
781
            }
782
            IRI partId = statementPartIds.get(partIndex);
21✔
783
            if (isGrouped() && getTemplate().isOptionalStatement(partId)) {
30✔
784
                // Optional group member with no consistent candidate: leave it unassigned and
785
                // move on. Trying all candidates first (above) keeps fills maximal — a member
786
                // is only skipped when no consistent assignment exists.
787
                unmatchedParts.add(partId);
15✔
788
                if (assignParts(partIndex + 1, available)) return true;
27!
789
                unmatchedParts.remove(partId);
×
790
            }
791
            return false;
6✔
792
        }
793

794
        /**
795
         * Checks unifiability and, if unifiable, applies the binding. Returns false instead of throwing,
796
         * so the caller can backtrack. (A partial binding left behind here is rolled back by the caller's
797
         * model snapshot.)
798
         */
799
        private boolean unifyPart(ValueItem item, Value v) {
800
            if (!item.isUnifiableWith(v)) return false;
18✔
801
            try {
802
                item.unifyWith(v);
9✔
803
                return true;
6✔
804
            } catch (UnificationException ex) {
×
805
                return false;
×
806
            }
807
        }
808

809
        /**
810
         * Snapshots the current values of all shared component models, so a trial binding can be rolled back.
811
         */
812
        private Map<IRI, Object> snapshotModels() {
813
            Map<IRI, Object> snapshot = new HashMap<>();
12✔
814
            for (Map.Entry<IRI, IModel<?>> e : context.getComponentModels().entrySet()) {
42✔
815
                snapshot.put(e.getKey(), e.getValue().getObject());
30✔
816
            }
3✔
817
            return snapshot;
6✔
818
        }
819

820
        /**
821
         * Restores component model values captured by {@link #snapshotModels()}.
822
         */
823
        @SuppressWarnings("unchecked")
824
        private void restoreModels(Map<IRI, Object> snapshot) {
825
            Map<IRI, IModel<?>> models = context.getComponentModels();
15✔
826
            for (Map.Entry<IRI, Object> e : snapshot.entrySet()) {
33✔
827
                IModel<?> m = models.get(e.getKey());
18✔
828
                if (m != null) ((IModel<Object>) m).setObject(e.getValue());
18!
829
            }
3✔
830
        }
3✔
831

832
        /**
833
         * Marks the filling of this repetition group as finished, indicating that all values have been filled.
834
         */
835
        public void fillFinished() {
836
            for (ValueItem vi : items) {
33✔
837
                vi.fillFinished();
6✔
838
            }
3✔
839
        }
3✔
840

841
        /**
842
         * Finalizes the values of all ValueItems in this repetition group.
843
         */
844
        public void finalizeValues() {
845
            for (ValueItem vi : items) {
33✔
846
                vi.finalizeValues();
6✔
847
            }
3✔
848
        }
3✔
849

850
    }
851

852
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
6✔
853
    private static final List<Statement> dummyStatementList = new ArrayList<Statement>(Collections.singletonList(vf.createStatement(vf.createIRI("http://dummy.com/"), vf.createIRI("http://dummy.com/"), vf.createIRI("http://dummy.com/"))));
51✔
854

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