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()); + } + +}