Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Status legend: ✅ Implemented · 🚧 In progress · 📋 Proposed
| [section-anchors](section-anchors.md) | ✅ Implemented | Display views generate fragment identifiers (`…/spaces/nanosuggestions#messages`) from their titles, with a copy-link handle and Ajax-aware scrolling |
| [structural-position](structural-position.md) | ✅ Implemented | `gen:hasStructuralPosition` `<section>.<sub>.<label>` strings order & group views on a page; strict format, primary digits 3–7 (intro…outro) in use ([#279](https://github.com/knowledgepixels/nanodash/issues/279)) |
| [space-namespace-prefixes](space-namespace-prefixes.md) | ✅ Implemented | `nt:hasPrefix "~~SPACE~~/"` / `"~~NAMESPACE~~"` mint resources under the space or maintained resource the form was opened from, with a picker when the `context` param determines none ([#571](https://github.com/knowledgepixels/nanodash/issues/571)) |
| [new-uri-placeholder](new-uri-placeholder.md) | ✅ Implemented | `nt:NewUriPlaceholder` marks a placeholder whose value names a resource that does not exist yet; publishing is refused when that identifier is already in use ([#646](https://github.com/knowledgepixels/nanodash/issues/646)) |
| [fill-modes](fill-modes.md) | ✅ Implemented | Publish-form fill modes (use / supersede / derive / override): which IDs & root definition are kept vs. re-minted; derive now resets root, new `override` mode added ([#527](https://github.com/knowledgepixels/nanodash/issues/527)) |
| [space-ref-identity](space-ref-identity.md) | 🚧 In progress | A space's identity is IRI + root-definition NPID (rival roots = distinct spaces); ref-keyed `SpaceRepository` + ref-scoped per-space authority queries shipped; one-`Space`-per-ref identity + disambiguation UI still to come |
| [shacl-alignment](shacl-alignment.md) | 📋 Proposed | Build on the SHACL standard: adopt `sh:`/`dash:` constraint terms inside templates (A) and export SHACL shapes for external validation (B) |
Expand Down
87 changes: 87 additions & 0 deletions docs/new-uri-placeholder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# New-URI placeholders

**Status:** ✅ Implemented — [#646](https://github.com/knowledgepixels/nanodash/issues/646)

## Goal

Stop a form from silently attaching a new nanopublication to somebody else's resource.

An identifier minted under the nanopublication's own namespace picks up its artifact code at
signing time, so it is unique by construction. One built from a prefix is not: fill "Defining
an open-ended Space" with the same name twice and both nanopublications claim
`https://w3id.org/spaces/my-space`. The second does not fail — it quietly becomes further
statements about the first one's space. A nanopublication cannot be edited afterwards, so
this is worth catching before publishing rather than after.

## The tag

A template marks the placeholders whose value names a resource that **does not exist yet**,
alongside the placeholder's own type:

```turtle
sub:space a nt:ExternalUriPlaceholder, nt:NewUriPlaceholder ;
nt:hasPrefix "https://w3id.org/spaces/" ;
rdfs:label "Space identifier" .
```

Before publishing, Nanodash asks whether any nanopublication already introduces the resulting
IRI. If one does, the publication is refused and the offending identifier is named.

## What is and is not checked

The tag is the only thing that turns the check on, and it is the template author's
declaration of intent rather than something Nanodash infers:

- **Tagged** — checked, however the value was formed. A name placed under a prefix and an IRI
the user typed out in full are treated the same, because whether a value names something new
is a property of the field, not of how the text was assembled.
- **Untagged** — never checked. It publishes exactly as before even if the IRI already exists.
This is deliberate: a field can perfectly well point at a resource that exists, and most do.

Two things are exempt even when tagged:

- **Identifiers under the nanopublication's own namespace.** The value at check time still
carries the `~~~ARTIFACTCODE~~~` marker rather than the IRI that ends up published, and the
artifact code makes it unique anyway.
- **The identifiers a superseded or overridden source already carries.** A new version keeps
the resource it is a version of (see [fill-modes](fill-modes.md)), so finding that one in use
is the expected answer, not a collision.

Note the shape of that second rule: it exempts the *identifier*, not the fill mode. Nothing
re-mints a prefix-minted identifier for a new version — there is no artifact code in it to
change — so editing the name while superseding defines a resource the source never had, and it
can collide like any other. An identifier under the nanopublication's own namespace does change
with the new artifact code, but those are excluded by the rule above and never reach the check.

With no source to compare against, nothing is checked at all, so a fill Nanodash does not
recognise leaves publishing exactly as it was.

A query service that cannot be reached answers "not taken". A check that cannot be made is not
evidence of a collision, and publishing should not depend on the query services being up.

## Why not infer it

An earlier version of this worked it out from the shape of the form: an IRI built from a
prefix, declared as an introduced resource, not auto-escaped, not superseding. That reads
intent out of mechanics. A prefix is a formatting device, and `nt:introduces` is attached by
templates that take the IRI of a thing that already exists too — templates such as "Defining
an open-ended Space with existing URI" exist precisely for that. Inference also missed the
opposite case, a new identifier typed out in full with no prefix involved.

Because a positive result blocks publishing outright, a false positive walls somebody out of a
nanopublication they are entitled to make, and the error message would be telling them to
change an identifier they had every right to use. Making it opt-in trades coverage for not
being wrong: a template that has not asked for the check behaves as it always did.

The cost is that existing templates get nothing until they are republished with the tag. Since
templates are themselves nanopublications, that means a new version and the governance pointer
moved with it.

## Vocabulary

`nt:NewUriPlaceholder` is `https://w3id.org/np/o/ntemplate/NewUriPlaceholder`, declared in
`Template` next to the other terms awaiting a home in nanopub-java's `NTEMPLATE`.

It says more than `nt:introduces`, which a template also attaches when the user supplies the
IRI of something that already exists. "New" here is about the identifier being minted by this
form, not about the resource being unfamiliar to the person filling it in.
26 changes: 26 additions & 0 deletions src/main/java/com/knowledgepixels/nanodash/QueryApiAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,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.
* <p>
* 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.
* <p>
* 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.nanopub.MalformedNanopubException;
import org.nanopub.Nanopub;
import org.nanopub.NanopubUtils;
import org.nanopub.NanopubAlreadyFinalizedException;
import org.nanopub.NanopubCreator;
import org.nanopub.extra.security.SignNanopub;
Expand Down Expand Up @@ -691,6 +692,9 @@ protected void onSubmit() {
try {
Nanopub np = createNanopub();
logger.info("Nanopublication created: {}", np.getUri());
if (!areNewUrisUnused()) {
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());
Expand Down Expand Up @@ -1156,6 +1160,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 (!areNewUrisUnused()) {
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();
Expand Down Expand Up @@ -1540,6 +1549,7 @@ private static void collectTemplateErrors(String part, TemplateContext context,

private synchronized Nanopub createNanopub() throws MalformedNanopubException, NanopubAlreadyFinalizedException {
assertionContext.getIntroducedIris().clear();
assertionContext.getNewUriIris().clear();
assertionContext.getRolePropertyPins().clear();
NanopubCreator npCreator = new NanopubCreator(targetNamespace);
npCreator.setAssertionUri(vf.createIRI(targetNamespace + "assertion"));
Expand Down Expand Up @@ -1720,6 +1730,75 @@ private boolean canPublishFromSource(PageParameters pageParams) {
return false;
}

private boolean areNewUrisUnused() {
IRI takenId = findTakenNewUri(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 forms for a placeholder the
* template marks as naming a resource that does not exist yet -- the IRI of a new space,
* say -- that is already in use, or null if all of them are free.
* <p>
* Such an identifier carries no artifact code, so nothing makes it unique: 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).
* <p>
* Only a placeholder the template tags with {@link com.knowledgepixels.nanodash.template.Template#NEW_URI_PLACEHOLDER} is
* checked; everything else publishes as before, whether or not its IRI already exists.
* <p>
* Superseding and overriding exempt the identifiers the source already carries, since a new
* version keeps the resource it is a version of, and finding that one in use is the expected
* answer rather than a collision. They are not exempt wholesale: a prefix-minted identifier
* carries no artifact code, so nothing re-mints it for the new version, and editing the name
* while superseding defines a genuinely new resource that can collide like any other. (An
* identifier minted under the nanopublication's own namespace does change with the new
* artifact code, but those never reach here -- see TemplateContext#recordIfNewUri.) With no
* source to compare against, nothing is checked, so an unrecognised fill leaves publishing
* exactly as it was.
*
* @param assertionContext the assertion context, after its values have been processed
* @return the first identifier for a new resource that is already in use, or null if none is
*/
public static IRI findTakenNewUri(TemplateContext assertionContext) {
FillMode fillMode = assertionContext.getFillMode();
Nanopub source = null;
if (fillMode == FillMode.SUPERSEDE || fillMode == FillMode.OVERRIDE) {
source = assertionContext.getReferenceNanopub();
if (source == null) return null;
}
for (IRI newUri : assertionContext.getNewUriIris()) {
if (source != null && isUsedIn(source, newUri)) continue;
if (QueryApiAccess.isUriIntroduced(newUri.stringValue())) return newUri;
}
return null;
}

/**
* Tells whether the given nanopublication already mentions the given IRI, which is how a
* superseding version says it is carrying the source's resource over rather than naming a
* new one. Every graph counts: the resource appears as a subject in the assertion and again
* under {@code npx:introduces} in the publication info.
*
* @param nanopub the nanopublication to look in
* @param iri the identifier to look for
* @return true if the nanopublication uses the identifier
*/
private static boolean isUsedIn(Nanopub nanopub, IRI iri) {
for (Statement st : NanopubUtils.getStatements(nanopub)) {
if (iri.equals(st.getSubject()) || iri.equals(st.getObject())) return true;
}
return false;
}

/**
* Returns the ID of the nanopublication that the given page parameters ask
* to supersede or override, or null if they ask for neither.
Expand Down
32 changes: 28 additions & 4 deletions src/main/java/com/knowledgepixels/nanodash/template/Template.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,27 @@ public class Template implements Serializable {
*/
public static final String DEFAULT_TARGET_NAMESPACE = "https://w3id.org/np/";

// TODO Move this to the other ntemplate vocabulary terms in nanopub-java (added there in
// Nanopublication/nanopub-java#156; drop it once that release is picked up):
// TODO Move these to the other ntemplate vocabulary terms in nanopub-java (both added
// there in Nanopublication/nanopub-java#156; drop them once that release is picked up):
/**
* 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");

/**
* Type marking a URI placeholder whose value names a resource that does not exist yet,
* applied alongside the placeholder's own type. The publish form refuses to publish when
* such an identifier is already in use (#646).
* <p>
* This is the template author's statement of intent, and the only thing that turns the
* check on: nothing is checked for an untagged placeholder, however its value is formed.
* It says more than {@link org.nanopub.vocabulary.NTEMPLATE#INTRODUCED_RESOURCE}, which a
* template also attaches when the user supplies the IRI of a thing that already exists.
*/
public static final IRI NEW_URI_PLACEHOLDER = vf.createIRI("https://w3id.org/np/o/ntemplate/NewUriPlaceholder");

private final Nanopub nanopub;
private String label;
private String description;
Expand Down Expand Up @@ -582,10 +594,22 @@ public boolean isTrustyUriPlaceholder(IRI iri) {
}

/**
* Checks if the IRI is an auto-escape URI placeholder.
* Checks if the IRI is a placeholder for a URI that does not exist yet, i.e. one the
* template marks with {@link #NEW_URI_PLACEHOLDER}.
*
* @param iri the IRI to check.
* @return true if the IRI is an auto-escape URI placeholder, false otherwise.
* @return true if the IRI is a new-URI placeholder, false otherwise.
*/
public boolean isNewUriPlaceholder(IRI iri) {
iri = transform(iri);
return typeMap.containsKey(iri) && typeMap.get(iri).contains(NEW_URI_PLACEHOLDER);
}

/**
* Checks if the IRI is an auto-escape URI placeholder.
*
* @param iri the IRI to check
* @return true if it is an auto-escape placeholder
*/
public boolean isAutoEscapePlaceholder(IRI iri) {
iri = transform(iri);
Expand Down
Loading