From 6d4485943d34645bf87aa23a30bc57ebbc658e53 Mon Sep 17 00:00:00 2001 From: Tobias Kuhn Date: Tue, 25 Aug 2026 16:08:49 +0200 Subject: [PATCH] feat: transient pubinfo templates not carried over on supersede/override/derive/use Introduces the nt:TransientTemplate flag (issue #606): a pubinfo template so flagged has its filled content bound to the one nanopub it was published with. When that nanopub is used as a fill source, the template's statements are consumed by throwaway contexts and discarded, so they neither carry over into the new nanopub nor fall through to the unused-statement warning or the hand-coded-statements fallback. The flag is checked on the latest template version, so flagging a template also covers nanopubs created with older versions of it. Along with it: - A change-note field is offered by default (removable) when superseding or overriding a nanopub. - Throwaway contexts clear the preset creator slot so other people's dct:creator triples are consumed too, and the source creators' foaf:name triples are dropped like the session user's. - The hand-coded catch-all context is filled last, after that cleanup, and is dropped when nothing is left for it (its statement is required, so an empty section blocked publishing). The Derived, Change note and Creator pubinfo templates are published as transient; the change-note statement is optional in its latest version. Closes #606 Co-Authored-By: Claude Fable 5 --- .../nanodash/component/PublishForm.java | 95 +++++++++++++++++-- .../nanodash/template/Template.java | 19 ++++ .../template/TransientTemplateTest.java | 58 +++++++++++ 3 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/knowledgepixels/nanodash/template/TransientTemplateTest.java diff --git a/src/main/java/com/knowledgepixels/nanodash/component/PublishForm.java b/src/main/java/com/knowledgepixels/nanodash/component/PublishForm.java index 726de360..404ea15c 100644 --- a/src/main/java/com/knowledgepixels/nanodash/component/PublishForm.java +++ b/src/main/java/com/knowledgepixels/nanodash/component/PublishForm.java @@ -54,6 +54,7 @@ import org.nanopub.extra.services.ApiResponseEntry; import org.nanopub.extra.services.QueryRef; import org.nanopub.vocabulary.NPX; +import org.nanopub.SimpleCreatorPattern; import org.nanopub.vocabulary.NTEMPLATE; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,6 +80,7 @@ public class PublishForm extends Panel { public static final String DEFAULT_PROV_TEMPLATE = "https://w3id.org/np/RA7lSq6MuK_TIC6JMSHvLtee3lpLoZDOqLJCLXevnrPoU"; private static final String supersedesPubInfoTemplateId = "https://w3id.org/np/RAoTD7udB2KtUuOuAe74tJi1t3VzK0DyWS7rYVAq1GRvw"; private static final String derivesFromPubInfoTemplateId = "https://w3id.org/np/RARW4MsFkHuwjycNElvEVtuMjpf4yWDL10-0C5l2MqqRQ"; + private static final String changeNotePubInfoTemplateId = "https://w3id.org/np/RAVXmu2rj-pkWoDjHH4n1oAHbseAf3RcUYpYiwI1WXDmY"; private static final String[] fixedPubInfoTemplates = new String[]{CREATOR_PUB_INFO_TEMPLATE, LICENSE_PUB_INFO_TEMPLATE}; @@ -267,6 +269,17 @@ public PublishForm(String id, final PageParameters pageParams, Class transientPiTemplateIds = new ArrayList<>(); if (fillNp != null && !fillOnlyAssertion) { for (IRI piTemplateId : td.getPubinfoTemplateIds(fillNp)) { + // The flag is checked on the latest template version, so flagging a + // template also stops carry-over from nanopubs created with older, + // unflagged versions of it: String piTemplateIdLatest = td.getLatestTemplateId(piTemplateId.stringValue()); if (piTemplateIdLatest.equals(supersedesPubInfoTemplateId)) { continue; } + if (isTransientTemplate(piTemplateIdLatest)) { + transientPiTemplateIds.add(piTemplateId.stringValue()); + continue; + } if (!pubInfoContextMap.containsKey(piTemplateIdLatest)) { // TODO Allow for automatically using latest template version createPubInfoContext(piTemplateId.stringValue()); @@ -409,18 +433,68 @@ public PublishForm(String id, final PageParameters pageParams, Class creatorModel = c.getComponentModels().get(NTEMPLATE.CREATOR_PLACEHOLDER); + if (creatorModel instanceof Model) { + ((Model) creatorModel).setObject(""); + } + c.initStatements(); + piFiller.fill(c); + } while (piFiller.getUnusedStatements().size() < unusedBefore); + } + // The hand-coded catch-all (present already when the source was made with + // it) is filled last, after the cleanup below, so it only gets what no + // other template and no cleanup rule has claimed: + final String handcodedStatementsTemplateId = "https://w3id.org/np/RAMEgudZsQ1bh1fZhfYnkthqH6YSXpghSE_DEN1I-6eAI"; + final String handcodedIdLatest = td.getLatestTemplateId(handcodedStatementsTemplateId); + TemplateContext handcodedContext = null; for (TemplateContext c : pubInfoContexts) { + String latestId = td.getLatestTemplateId(c.getTemplateId()); + if (isTransientTemplate(latestId)) { + // A transient template kept in the form (e.g. the fresh derivation + // link in derive mode) starts from its parameters only; values from + // the fill source must not leak into it. + continue; + } + if (latestId.equals(handcodedIdLatest)) { + handcodedContext = c; + continue; + } piFiller.fill(c); } piFiller.removeUnusedStatements(NanodashSession.get().getUserIri(), FOAF.NAME, null); + // The name triples of the source's creators are auto-generated alongside + // the creator statements; with those discarded as transient, keeping the + // orphaned names would leak them into hand-coded statements: + for (IRI creator : SimpleCreatorPattern.getCreators(fillNp)) { + piFiller.removeUnusedStatements(creator, FOAF.NAME, null); + } if (piFiller.hasUnusedStatements()) { - final String handcodedStatementsTemplateId = "https://w3id.org/np/RAMEgudZsQ1bh1fZhfYnkthqH6YSXpghSE_DEN1I-6eAI"; - if (!pubInfoContextMap.containsKey(handcodedStatementsTemplateId)) { - TemplateContext c = createPubInfoContext(handcodedStatementsTemplateId); - c.setFillSource(fillNp); - c.initStatements(); - piFiller.fill(c); + if (handcodedContext == null) { + handcodedContext = createPubInfoContext(handcodedStatementsTemplateId); + handcodedContext.setFillSource(fillNp); + handcodedContext.initStatements(); } + piFiller.fill(handcodedContext); + } else if (handcodedContext != null && !requiredPubInfoContexts.contains(handcodedContext)) { + // Everything was claimed or discarded: an empty catch-all would only + // block publishing on its required statement, so drop it. + pubInfoContexts.remove(handcodedContext); + pubInfoContextMap.values().remove(handcodedContext); } unusedPiStatementList.addAll(piFiller.getUnusedStatements()); // TODO: Also use pubinfo templates stated in nanopub to be filled in? @@ -1045,6 +1119,15 @@ private TemplateContext newContext(ContextType contextType, String templateId, S return context; } + /** + * Checks whether the given template is transient (see {@link Template#isTransient()}), + * i.e. its filled content is not carried over when the nanopub is used as fill source. + */ + private static boolean isTransientTemplate(String templateId) { + Template t = TemplateData.get().getTemplate(templateId); + return t != null && t.isTransient(); + } + private TemplateContext createPubInfoContext(String piTemplateId) { TemplateContext c; if (pubInfoContextMap.containsKey(piTemplateId)) { diff --git a/src/main/java/com/knowledgepixels/nanodash/template/Template.java b/src/main/java/com/knowledgepixels/nanodash/template/Template.java index 6c6b9c88..7d6ea421 100644 --- a/src/main/java/com/knowledgepixels/nanodash/template/Template.java +++ b/src/main/java/com/knowledgepixels/nanodash/template/Template.java @@ -48,6 +48,13 @@ public class Template implements Serializable { */ public static final IRI POSSIBLE_LANGUAGE_TAG = vf.createIRI("https://w3id.org/np/o/ntemplate/possibleLanguageTag"); + /** + * Type of a template whose filled content applies only to the specific nanopublication + * it was published with, and is not carried over when that nanopublication is + * superseded, overridden, derived from, or otherwise used as a fill source. + */ + public static final IRI TRANSIENT_TEMPLATE = vf.createIRI("https://w3id.org/np/o/ntemplate/TransientTemplate"); + private final Nanopub nanopub; private String label; private String description; @@ -118,6 +125,18 @@ public boolean isUnlisted() { return types != null && types.contains(NTEMPLATE.UNLISTED_TEMPLATE); } + /** + * Checks if the template is transient, meaning its filled content applies only to the + * specific nanopublication it was published with and is not carried over when that + * nanopublication is superseded, overridden, derived from, or used as a fill source. + * + * @return true if the template is transient, false otherwise. + */ + public boolean isTransient() { + List types = typeMap.get(templateIri); + return types != null && types.contains(TRANSIENT_TEMPLATE); + } + /** * Returns the Nanopub object representing the template. * diff --git a/src/test/java/com/knowledgepixels/nanodash/template/TransientTemplateTest.java b/src/test/java/com/knowledgepixels/nanodash/template/TransientTemplateTest.java new file mode 100644 index 00000000..a9f9c3cd --- /dev/null +++ b/src/test/java/com/knowledgepixels/nanodash/template/TransientTemplateTest.java @@ -0,0 +1,58 @@ +package com.knowledgepixels.nanodash.template; + +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.model.vocabulary.RDFS; +import org.junit.jupiter.api.Test; +import org.nanopub.NanopubCreator; +import org.nanopub.vocabulary.NTEMPLATE; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for the nt:TransientTemplate flag (issue #606): a pubinfo template so flagged + * has its filled content bound to the one nanopub it was published with, and the + * publish form discards that content instead of carrying it over when the nanopub is + * superseded, overridden, derived from, or used as a fill source. + */ +public class TransientTemplateTest { + + private static final ValueFactory vf = SimpleValueFactory.getInstance(); + + private static final String NP_URI = "https://w3id.org/np/RAAbCdEfGhIjKlMnOpQrStUvWxYz0123456789-_AbCdE"; + private static final IRI ST1 = vf.createIRI(NP_URI + "/st1"); + private static final IRI NOTE = vf.createIRI(NP_URI + "/note"); + + private static NanopubCreator templateCreator() throws Exception { + NanopubCreator creator = new NanopubCreator(NP_URI); + creator.addProvenanceStatement(vf.createStatement(creator.getAssertionUri(), RDFS.SEEALSO, creator.getAssertionUri())); + creator.addPubinfoStatement(vf.createStatement(creator.getNanopubUri(), RDFS.SEEALSO, creator.getNanopubUri())); + IRI templateNode = creator.getAssertionUri(); + creator.addAssertionStatement(templateNode, RDF.TYPE, NTEMPLATE.PUBINFO_TEMPLATE); + creator.addAssertionStatement(templateNode, RDFS.LABEL, vf.createLiteral("Transient template test template")); + creator.addAssertionStatement(templateNode, NTEMPLATE.HAS_STATEMENT, ST1); + creator.addAssertionStatement(ST1, RDF.SUBJECT, vf.createIRI(NP_URI + "/nanopub")); + creator.addAssertionStatement(ST1, RDF.PREDICATE, RDFS.COMMENT); + creator.addAssertionStatement(ST1, RDF.OBJECT, NOTE); + creator.addAssertionStatement(NOTE, RDF.TYPE, NTEMPLATE.LITERAL_PLACEHOLDER); + return creator; + } + + @Test + void flaggedTemplateIsTransient() throws Exception { + NanopubCreator creator = templateCreator(); + creator.addAssertionStatement(creator.getAssertionUri(), RDF.TYPE, Template.TRANSIENT_TEMPLATE); + Template t = new Template(creator.finalizeNanopub()); + assertTrue(t.isTransient()); + } + + @Test + void unflaggedTemplateIsNotTransient() throws Exception { + Template t = new Template(templateCreator().finalizeNanopub()); + assertFalse(t.isTransient()); + } + +}