diff --git a/src/main/java/com/knowledgepixels/nanodash/QueryApiAccess.java b/src/main/java/com/knowledgepixels/nanodash/QueryApiAccess.java
index 76507f84..2b41dc00 100644
--- a/src/main/java/com/knowledgepixels/nanodash/QueryApiAccess.java
+++ b/src/main/java/com/knowledgepixels/nanodash/QueryApiAccess.java
@@ -421,6 +421,32 @@ public static boolean isNanopubLoaded(String nanopubId) throws FailedApiCallExce
return r != null && !r.getData().isEmpty();
}
+ /**
+ * Checks whether the given IRI has already been used as the identifier of a resource,
+ * i.e. whether some nanopublication already introduces it.
+ *
+ * This is the question an identifier that carries no artifact code raises: nothing makes
+ * it unique, so the same form filled with the same name twice yields the same IRI, and
+ * the second nanopublication silently attaches itself to the first one's resource (#646).
+ * The lookup runs against the meta repository, where {@code npx:introduces} is indexed.
+ *
+ * A query service that cannot be reached answers false: a check that cannot be made is
+ * not evidence of a collision, and publishing should not depend on the query services
+ * being up.
+ *
+ * @param uri The IRI to check.
+ * @return True if a nanopublication introducing the IRI was found.
+ */
+ public static boolean isUriIntroduced(String uri) {
+ try {
+ ApiResponse r = get(new QueryRef(GET_INTRODUCING_NANOPUB, "thing", uri));
+ return r != null && !r.getData().isEmpty();
+ } catch (Exception ex) {
+ logger.error("Could not check whether IRI '{}' is already introduced", uri, ex);
+ return false;
+ }
+ }
+
/**
* Extracts the query ID from a given query IRI.
*
diff --git a/src/main/java/com/knowledgepixels/nanodash/component/PublishForm.java b/src/main/java/com/knowledgepixels/nanodash/component/PublishForm.java
index e632dbc9..2fdcd787 100644
--- a/src/main/java/com/knowledgepixels/nanodash/component/PublishForm.java
+++ b/src/main/java/com/knowledgepixels/nanodash/component/PublishForm.java
@@ -638,6 +638,9 @@ protected void onSubmit() {
try {
Nanopub np = createNanopub();
logger.info("Nanopublication created: {}", np.getUri());
+ if (!areMintedIdsUnused()) {
+ return;
+ }
TransformContext tc = new TransformContext(SignatureAlgorithm.RSA, NanodashSession.get().getKeyPair(), NanodashSession.get().getUserIri(), false, false, false);
signedNp = SignNanopub.signAndTransform(np, tc);
logger.info("Nanopublication signed: {}", signedNp.getUri());
@@ -1045,6 +1048,11 @@ public void onSubmit() {
}
Nanopub np = createNanopub();
+ // Checked here too: the preview page publishes the nanopublication it
+ // was given, without coming back through this form.
+ if (!areMintedIdsUnused()) {
+ return;
+ }
TransformContext tc = new TransformContext(SignatureAlgorithm.RSA, NanodashSession.get().getKeyPair(), NanodashSession.get().getUserIri(), false, false, false);
Nanopub signedNp = SignNanopub.signAndTransform(np, tc);
String previewId = signedNp.getUri().stringValue();
@@ -1180,6 +1188,7 @@ private static void collectTemplateErrors(String part, TemplateContext context,
private synchronized Nanopub createNanopub() throws MalformedNanopubException, NanopubAlreadyFinalizedException {
assertionContext.getIntroducedIris().clear();
+ assertionContext.getPrefixMintedIris().clear();
assertionContext.getRolePropertyPins().clear();
NanopubCreator npCreator = new NanopubCreator(targetNamespace);
npCreator.setAssertionUri(vf.createIRI(targetNamespace + "assertion"));
@@ -1338,6 +1347,50 @@ private boolean canPublishFromSource(PageParameters pageParams) {
return false;
}
+ private boolean areMintedIdsUnused() {
+ IRI takenId = findTakenMintedId(assertionContext);
+ if (takenId == null) {
+ return true;
+ }
+ feedbackPanel.error("The identifier " + takenId.stringValue()
+ + " is already in use. Pick a different one, or use a template for describing"
+ + " an existing resource if that is what you mean to do.");
+ return false;
+ }
+
+ /**
+ * Returns the first identifier the given assertion context mints under a fixed prefix --
+ * the IRI of a new space, say -- that is already in use, or null if all of them are free.
+ *
+ * An identifier minted under the new nanopublication's own namespace picks up its
+ * artifact code and is unique by construction; one minted under a prefix the template or
+ * the space supplies is not, so filling the same form with the same name twice yields the
+ * same IRI, and the second nanopublication silently extends the first one's resource
+ * instead of defining a new one. A nanopublication cannot be edited afterwards, so the
+ * collision is worth catching before publishing rather than after (#646).
+ *
+ * Not every identifier is checked. An IRI the user typed out in full names an existing
+ * thing rather than minting one -- templates such as "Defining an open-ended Space with
+ * existing URI" exist precisely to do that -- and neither superseding nor overriding
+ * mints anything: keeping the source's identifier is the point of both.
+ *
+ * @param assertionContext the assertion context, after its values have been processed
+ * @return the first minted identifier that is already in use, or null if none is
+ */
+ public static IRI findTakenMintedId(TemplateContext assertionContext) {
+ FillMode fillMode = assertionContext.getFillMode();
+ if (fillMode == FillMode.SUPERSEDE || fillMode == FillMode.OVERRIDE) {
+ return null;
+ }
+ for (IRI mintedIri : assertionContext.getPrefixMintedIris()) {
+ // Only identifiers the nanopublication declares as newly introduced resources: a
+ // prefixed field can just as well point at something that already exists.
+ if (!assertionContext.getIntroducedIris().contains(mintedIri)) continue;
+ if (QueryApiAccess.isUriIntroduced(mintedIri.stringValue())) return mintedIri;
+ }
+ return null;
+ }
+
/**
* Returns the ID of the nanopublication that the given page parameters ask
* to supersede or override, or null if they ask for neither.
diff --git a/src/main/java/com/knowledgepixels/nanodash/template/TemplateContext.java b/src/main/java/com/knowledgepixels/nanodash/template/TemplateContext.java
index d1c8090b..30d6837b 100644
--- a/src/main/java/com/knowledgepixels/nanodash/template/TemplateContext.java
+++ b/src/main/java/com/knowledgepixels/nanodash/template/TemplateContext.java
@@ -42,6 +42,7 @@ public class TemplateContext implements Serializable {
private final Map> componentModels = new HashMap<>();
private Set introducedIris = new HashSet<>();
private Set embeddedIris = new HashSet<>();
+ private Set prefixMintedIris = new LinkedHashSet<>();
private Map rolePropertyPins = new LinkedHashMap<>();
private List statementItems;
private Set iriSet = new HashSet<>();
@@ -420,6 +421,23 @@ public Set getEmbeddedIris() {
return embeddedIris;
}
+ /**
+ * Returns the IRIs this context minted by putting a name the user typed under a prefix
+ * the template or the navigation context supplied, rather than under the namespace of
+ * the nanopublication being created.
+ *
+ * Such an IRI carries no artifact code, so nothing makes it unique: two people filling
+ * the same form with the same name arrive at the same identifier. That is what makes it
+ * worth checking against what has already been published, and the reason this set is
+ * kept apart from {@link #getIntroducedIris()} (see #646). An IRI the user typed out in
+ * full is not in here: naming an existing thing is not minting one.
+ *
+ * @return a set of IRIs minted under a prefix, in the order they were processed
+ */
+ public Set getPrefixMintedIris() {
+ return prefixMintedIris;
+ }
+
/**
* Returns the role-instantiation direction pins collected in this context, mapping
* each filled/constant role predicate to its pin class
@@ -516,6 +534,7 @@ public Value processValue(Value value) {
if (v.matches("[^:# ]+")) v = targetNamespace + v;
if (Utils.isUriValue(v)) {
processedValue = vf.createIRI(v);
+ recordIfMintedUnderPrefix(iri, (IRI) processedValue, prefix);
} else {
processedValue = vf.createLiteral(tfObject);
}
@@ -549,6 +568,7 @@ public Value processValue(Value value) {
if (!unresolvedPrefix) {
if (v.matches("[^:# ]+")) v = targetNamespace + v;
processedValue = vf.createIRI(v);
+ recordIfMintedUnderPrefix(iri, (IRI) processedValue, prefix);
}
}
} else if (template.isIntroducedResource(iri)
@@ -638,6 +658,32 @@ public Value processValue(Value value) {
return processedValue;
}
+ /**
+ * Records an IRI that was formed by appending what the user typed to a prefix, so that
+ * the publish form can check it against the identifiers already in use (#646).
+ *
+ * Three kinds of IRI are deliberately left out:
+ *
+ * - one the user typed out in full, which names a thing rather than minting one;
+ * - one minted under the new nanopublication's own namespace, whose artifact code is
+ * substituted at signing time and makes it unique by construction;
+ * - one built by an auto-escaping placeholder, where the IRI is derived from the text
+ * itself. AIDA sentences are the case in point: two people writing the same sentence are
+ * meant to arrive at the same IRI, so an existing one is agreement, not a
+ * collision.
+ *
+ *
+ * @param placeholder the placeholder the value was entered into
+ * @param mintedIri the IRI that was just formed
+ * @param prefix the prefix it was formed under, empty if there was none
+ */
+ private void recordIfMintedUnderPrefix(IRI placeholder, IRI mintedIri, String prefix) {
+ if (prefix == null || prefix.isEmpty()) return;
+ if (mintedIri.stringValue().startsWith(targetNamespace)) return;
+ if (template.isAutoEscapePlaceholder(placeholder)) return;
+ prefixMintedIris.add(mintedIri);
+ }
+
/**
* Returns the statement items associated with this context.
*
diff --git a/src/test/java/com/knowledgepixels/nanodash/QueryApiAccessTest.java b/src/test/java/com/knowledgepixels/nanodash/QueryApiAccessTest.java
index 20fa2a47..ddc93abc 100644
--- a/src/test/java/com/knowledgepixels/nanodash/QueryApiAccessTest.java
+++ b/src/test/java/com/knowledgepixels/nanodash/QueryApiAccessTest.java
@@ -69,4 +69,41 @@ void getQueryNameExtractsQueryNameFromValidIRI() {
assertEquals(queryName, result);
}
+ // An identifier minted under a prefix carries no artifact code, so whether it is free has
+ // to be asked of what has already been published (#646).
+ @Test
+ void isUriIntroducedReportsAnIdentifierThatIsAlreadyTaken() {
+ ApiResponse response = new ApiResponse();
+ ApiResponseEntry entry = new ApiResponseEntry();
+ entry.add("np", "https://w3id.org/np/RAMVfH7NHhyXFkvbdYYBtimoivyFpQl6CrXgoKgmjGE6I");
+ response.getData().add(entry);
+
+ try (MockedStatic mockQueryAccess = mockStatic(QueryAccess.class)) {
+ mockQueryAccess.when(() -> QueryAccess.get(any(QueryRef.class))).thenReturn(response);
+
+ assertTrue(QueryApiAccess.isUriIntroduced("https://w3id.org/spaces/example/bar"));
+ }
+ }
+
+ @Test
+ void isUriIntroducedReportsAnIdentifierThatIsFree() {
+ try (MockedStatic mockQueryAccess = mockStatic(QueryAccess.class)) {
+ mockQueryAccess.when(() -> QueryAccess.get(any(QueryRef.class))).thenReturn(new ApiResponse());
+
+ assertFalse(QueryApiAccess.isUriIntroduced("https://w3id.org/spaces/example/unused"));
+ }
+ }
+
+ // A check that cannot be made is not evidence of a collision: an unreachable query
+ // service must not be what stops someone from publishing.
+ @Test
+ void isUriIntroducedAnswersFalseWhenTheQueryFails() {
+ try (MockedStatic mockQueryAccess = mockStatic(QueryAccess.class)) {
+ mockQueryAccess.when(() -> QueryAccess.get(any(QueryRef.class)))
+ .thenThrow(new APINotReachableException("no instance reachable"));
+
+ assertFalse(QueryApiAccess.isUriIntroduced("https://w3id.org/spaces/example/bar"));
+ }
+ }
+
}
\ No newline at end of file
diff --git a/src/test/java/com/knowledgepixels/nanodash/template/PrefixMintedIdTest.java b/src/test/java/com/knowledgepixels/nanodash/template/PrefixMintedIdTest.java
new file mode 100644
index 00000000..a8d36d11
--- /dev/null
+++ b/src/test/java/com/knowledgepixels/nanodash/template/PrefixMintedIdTest.java
@@ -0,0 +1,233 @@
+package com.knowledgepixels.nanodash.template;
+
+import com.knowledgepixels.nanodash.QueryApiAccess;
+import com.knowledgepixels.nanodash.WicketApplication;
+import com.knowledgepixels.nanodash.component.PublishForm;
+import com.knowledgepixels.nanodash.component.PublishForm.FillMode;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.util.tester.WicketTester;
+import org.eclipse.rdf4j.model.IRI;
+import org.eclipse.rdf4j.model.Value;
+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.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.nanopub.NanopubCreator;
+import org.nanopub.vocabulary.NTEMPLATE;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * An identifier minted under a prefix the template supplies carries no artifact code, so
+ * nothing makes it unique and the publish form has to check it against the identifiers
+ * already in use (#646). {@link TemplateContext#getPrefixMintedIris()} is what tells those
+ * apart from the identifiers that need no checking: one the user typed out in full (naming an
+ * existing thing rather than minting a new one), one minted under the new nanopublication's
+ * own namespace (unique by construction), and one derived from the text itself (meant to be
+ * arrived at more than once).
+ */
+class PrefixMintedIdTest {
+
+ private static final ValueFactory vf = SimpleValueFactory.getInstance();
+
+ private static final String NP_URI = "https://w3id.org/np/RAAbCdEfGhIjKlMnOpQrStUvWxYz0123456789-_AbCdE";
+ private static final String TARGET_NAMESPACE = "https://w3id.org/np/~~~ARTIFACTCODE~~~/";
+ private static final String SPACE_PREFIX = "https://w3id.org/spaces/";
+ private static final String AIDA_PREFIX = "http://purl.org/aida/";
+
+ // As in "Defining an open-ended Space (with root definition)": an introduced resource
+ // named below a fixed prefix.
+ private static final IRI SPACE_FIELD = vf.createIRI(NP_URI + "/space");
+ // As in "Introducing a user": an introduced resource the user types out in full.
+ private static final IRI AGENT_FIELD = vf.createIRI(NP_URI + "/agent");
+ // An introduced resource minted under the nanopublication itself.
+ private static final IRI LOCAL_FIELD = vf.createIRI(NP_URI + "/local");
+ // As in the AIDA sentence templates: an introduced resource whose IRI is derived from
+ // the text itself.
+ private static final IRI AIDA_FIELD = vf.createIRI(NP_URI + "/aida");
+
+ private MockedStatic templateDataMockedStatic;
+
+ @BeforeEach
+ void setUp() {
+ new WicketTester(new WicketApplication());
+ templateDataMockedStatic = mockStatic(TemplateData.class);
+ }
+
+ @AfterEach
+ void tearDown() {
+ templateDataMockedStatic.close();
+ }
+
+ /**
+ * Builds a template that introduces four resources, one per way of arriving at an IRI.
+ */
+ private TemplateContext spaceTemplateContext() 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();
+ IRI stSpace = vf.createIRI(NP_URI + "/st1");
+ IRI stAgent = vf.createIRI(NP_URI + "/st2");
+ IRI stLocal = vf.createIRI(NP_URI + "/st3");
+ IRI stAida = vf.createIRI(NP_URI + "/st4");
+ creator.addAssertionStatement(templateNode, RDF.TYPE, NTEMPLATE.ASSERTION_TEMPLATE);
+ creator.addAssertionStatement(templateNode, RDFS.LABEL, vf.createLiteral("Defining a space"));
+ creator.addAssertionStatement(templateNode, NTEMPLATE.HAS_STATEMENT, stSpace);
+ creator.addAssertionStatement(templateNode, NTEMPLATE.HAS_STATEMENT, stAgent);
+ creator.addAssertionStatement(templateNode, NTEMPLATE.HAS_STATEMENT, stLocal);
+ creator.addAssertionStatement(templateNode, NTEMPLATE.HAS_STATEMENT, stAida);
+ creator.addAssertionStatement(stSpace, RDF.SUBJECT, SPACE_FIELD);
+ creator.addAssertionStatement(stSpace, RDF.PREDICATE, RDF.TYPE);
+ creator.addAssertionStatement(stSpace, RDF.OBJECT, vf.createIRI("https://w3id.org/kpxl/gen/terms/Space"));
+ creator.addAssertionStatement(stAgent, RDF.SUBJECT, SPACE_FIELD);
+ creator.addAssertionStatement(stAgent, RDF.PREDICATE, vf.createIRI("https://w3id.org/kpxl/gen/terms/hasAdmin"));
+ creator.addAssertionStatement(stAgent, RDF.OBJECT, AGENT_FIELD);
+ creator.addAssertionStatement(stLocal, RDF.SUBJECT, SPACE_FIELD);
+ creator.addAssertionStatement(stLocal, RDF.PREDICATE, RDFS.SEEALSO);
+ creator.addAssertionStatement(stLocal, RDF.OBJECT, LOCAL_FIELD);
+ creator.addAssertionStatement(stAida, RDF.SUBJECT, SPACE_FIELD);
+ creator.addAssertionStatement(stAida, RDF.PREDICATE, RDFS.COMMENT);
+ creator.addAssertionStatement(stAida, RDF.OBJECT, AIDA_FIELD);
+ creator.addAssertionStatement(SPACE_FIELD, RDF.TYPE, NTEMPLATE.EXTERNAL_URI_PLACEHOLDER);
+ creator.addAssertionStatement(SPACE_FIELD, RDF.TYPE, NTEMPLATE.INTRODUCED_RESOURCE);
+ creator.addAssertionStatement(SPACE_FIELD, NTEMPLATE.HAS_PREFIX, vf.createLiteral(SPACE_PREFIX));
+ creator.addAssertionStatement(SPACE_FIELD, RDFS.LABEL, vf.createLiteral("Space identifier"));
+ creator.addAssertionStatement(AGENT_FIELD, RDF.TYPE, NTEMPLATE.AGENT_PLACEHOLDER);
+ creator.addAssertionStatement(AGENT_FIELD, RDF.TYPE, NTEMPLATE.INTRODUCED_RESOURCE);
+ creator.addAssertionStatement(AGENT_FIELD, RDFS.LABEL, vf.createLiteral("an admin of the space"));
+ creator.addAssertionStatement(LOCAL_FIELD, RDF.TYPE, NTEMPLATE.URI_PLACEHOLDER);
+ creator.addAssertionStatement(LOCAL_FIELD, RDF.TYPE, NTEMPLATE.LOCAL_RESOURCE);
+ creator.addAssertionStatement(LOCAL_FIELD, RDF.TYPE, NTEMPLATE.INTRODUCED_RESOURCE);
+ creator.addAssertionStatement(LOCAL_FIELD, RDFS.LABEL, vf.createLiteral("short id of the record"));
+ creator.addAssertionStatement(AIDA_FIELD, RDF.TYPE, NTEMPLATE.AUTO_ESCAPE_URI_PLACEHOLDER);
+ creator.addAssertionStatement(AIDA_FIELD, RDF.TYPE, NTEMPLATE.INTRODUCED_RESOURCE);
+ creator.addAssertionStatement(AIDA_FIELD, NTEMPLATE.HAS_PREFIX, vf.createLiteral(AIDA_PREFIX));
+ creator.addAssertionStatement(AIDA_FIELD, RDFS.LABEL, vf.createLiteral("the AIDA sentence"));
+ Template template = TemplateTestUtil.parseTemplate(creator.finalizeNanopub());
+
+ TemplateData templateDataMock = mock(TemplateData.class);
+ templateDataMockedStatic.when(TemplateData::get).thenReturn(templateDataMock);
+ when(templateDataMock.getTemplate(NP_URI)).thenReturn(template);
+
+ TemplateContext context = new TemplateContext(ContextType.ASSERTION, NP_URI, "statement", TARGET_NAMESPACE);
+ context.initStatements();
+ return context;
+ }
+
+ // The headline case: the user types "my-space" and the template's prefix turns it into a
+ // full IRI that nothing else guarantees to be free.
+ @Test
+ void aNameTypedBelowAPrefixIsMinted() throws Exception {
+ TemplateContext context = spaceTemplateContext();
+ context.getComponentModels().put(SPACE_FIELD, Model.of("my-space"));
+ Value processed = context.processValue(SPACE_FIELD);
+ assertEquals(SPACE_PREFIX + "my-space", processed.stringValue());
+ assertTrue(context.getPrefixMintedIris().contains(processed),
+ "an IRI formed from the template's prefix has to be checked for collisions");
+ assertTrue(context.getIntroducedIris().contains(processed));
+ }
+
+ // "Defining an open-ended Space with existing URI" exists precisely so that an existing
+ // IRI can be used, so a fully typed-out IRI is a reference, not a mint.
+ @Test
+ void anIriTypedOutInFullIsNotMinted() throws Exception {
+ TemplateContext context = spaceTemplateContext();
+ context.getComponentModels().put(SPACE_FIELD, Model.of("https://example.org/spaces/existing"));
+ Value processed = context.processValue(SPACE_FIELD);
+ assertEquals("https://example.org/spaces/existing", processed.stringValue());
+ assertFalse(context.getPrefixMintedIris().contains(processed),
+ "naming an existing resource is not minting a new identifier");
+ assertTrue(context.getIntroducedIris().contains(processed));
+ }
+
+ // The user-introduction case: the ORCID is an introduced resource that is meant to be
+ // introduced again whenever a new key is declared, so it must not be checked.
+ @Test
+ void anAgentIriWithoutAPrefixIsNotMinted() throws Exception {
+ TemplateContext context = spaceTemplateContext();
+ context.getComponentModels().put(AGENT_FIELD, Model.of("https://orcid.org/0000-0002-1267-0234"));
+ Value processed = context.processValue(AGENT_FIELD);
+ assertEquals("https://orcid.org/0000-0002-1267-0234", processed.stringValue());
+ assertFalse(context.getPrefixMintedIris().contains(processed),
+ "a placeholder without a prefix mints nothing");
+ assertTrue(context.getIntroducedIris().contains(processed));
+ }
+
+ // An AIDA sentence IRI is the sentence itself: two people writing the same sentence are
+ // meant to arrive at the same IRI, so finding it already published is agreement rather
+ // than a collision.
+ @Test
+ void anAutoEscapedIriIsNotMinted() throws Exception {
+ TemplateContext context = spaceTemplateContext();
+ context.getComponentModels().put(AIDA_FIELD, Model.of("The cat sat on the mat."));
+ Value processed = context.processValue(AIDA_FIELD);
+ assertEquals(AIDA_PREFIX + "The+cat+sat+on+the+mat.", processed.stringValue());
+ assertFalse(context.getPrefixMintedIris().contains(processed),
+ "an IRI derived from the text itself is meant to be arrived at more than once");
+ assertTrue(context.getIntroducedIris().contains(processed));
+ }
+
+ // A local resource picks up this nanopublication's artifact code at signing time, which
+ // is what makes it unique; there is nothing to check.
+ @Test
+ void aResourceMintedUnderTheNanopubIsNotChecked() throws Exception {
+ TemplateContext context = spaceTemplateContext();
+ context.getComponentModels().put(LOCAL_FIELD, Model.of("record"));
+ Value processed = context.processValue(LOCAL_FIELD);
+ assertEquals(TARGET_NAMESPACE + "record", processed.stringValue());
+ assertFalse(context.getPrefixMintedIris().contains(processed),
+ "an identifier carrying the nanopublication's artifact code is unique by construction");
+ assertTrue(context.getIntroducedIris().contains(processed));
+ }
+
+ // What the publish form does with all of the above: refuse the publication and name the
+ // identifier that is taken.
+ @Test
+ void aTakenIdentifierIsReported() throws Exception {
+ TemplateContext context = spaceTemplateContext();
+ context.getComponentModels().put(SPACE_FIELD, Model.of("example/bar"));
+ IRI minted = (IRI) context.processValue(SPACE_FIELD);
+ try (MockedStatic q = mockStatic(QueryApiAccess.class)) {
+ q.when(() -> QueryApiAccess.isUriIntroduced(minted.stringValue())).thenReturn(true);
+ assertEquals(minted, PublishForm.findTakenMintedId(context));
+ }
+ }
+
+ @Test
+ void aFreeIdentifierLetsThePublicationThrough() throws Exception {
+ TemplateContext context = spaceTemplateContext();
+ context.getComponentModels().put(SPACE_FIELD, Model.of("nobody-took-this"));
+ context.processValue(SPACE_FIELD);
+ try (MockedStatic q = mockStatic(QueryApiAccess.class)) {
+ q.when(() -> QueryApiAccess.isUriIntroduced(anyString())).thenReturn(false);
+ assertNull(PublishForm.findTakenMintedId(context));
+ }
+ }
+
+ // Superseding and overriding keep the source's identifier on purpose (docs/fill-modes.md),
+ // so the identifier being in use is exactly what is expected there.
+ @Test
+ void supersedingKeepsTheIdentifierWithoutAsking() throws Exception {
+ TemplateContext context = spaceTemplateContext();
+ context.setFillMode(FillMode.SUPERSEDE);
+ context.getComponentModels().put(SPACE_FIELD, Model.of("example/bar"));
+ context.processValue(SPACE_FIELD);
+ try (MockedStatic q = mockStatic(QueryApiAccess.class)) {
+ assertNull(PublishForm.findTakenMintedId(context));
+ q.verifyNoInteractions();
+ }
+ }
+
+}