Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 7
JCL-417: Validate LDP containment data in SolidContainer::getResources#571
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -20,21 +20,26 @@ | ||
| */ | ||
| package com.inrupt.client.solid; | ||
| import com.inrupt.client.ValidationResult; | ||
| import com.inrupt.client.vocabulary.LDP; | ||
| import com.inrupt.client.vocabulary.RDF; | ||
| import com.inrupt.rdf.wrapping.commons.ValueMappings; | ||
| import com.inrupt.rdf.wrapping.commons.WrapperIRI; | ||
| import java.net.URI; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.function.Predicate; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
| import org.apache.commons.rdf.api.Dataset; | ||
| import org.apache.commons.rdf.api.Graph; | ||
| import org.apache.commons.rdf.api.IRI; | ||
| import org.apache.commons.rdf.api.RDFTerm; | ||
| import org.apache.commons.rdf.api.Triple; | ||
| /** | ||
| * A Solid Container Object. | ||
| @@ -77,15 +82,43 @@ public SolidContainer(final URI identifier, final Dataset dataset, final Metadat | ||
| * @return the contained resources | ||
| */ | ||
| public Set<SolidResource> getResources() { | ||
| final Node node = new Node(rdf.createIRI(getIdentifier().toString()), getGraph()); | ||
| try (final Stream<Node.TypedNode> stream = node.getResources()) { | ||
| return stream.map(child -> { | ||
| final Metadata.Builder builder = Metadata.newBuilder(); | ||
| getMetadata().getStorage().ifPresent(builder::storage); | ||
| child.getTypes().forEach(builder::type); | ||
| return new SolidResourceReference(URI.create(child.getIRIString()), builder.build()); | ||
| }).collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); | ||
| final String container = normalize(getIdentifier()); | ||
| // As defined by the Solid Protocol, containers always end with a slash. | ||
| if (container.endsWith("/")) { | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we could have a dedicated error thrown here and upon creation too, if the URI does not end in a slash.
| ||
| final Node node = new Node(rdf.createIRI(getIdentifier().toString()), getGraph()); | ||
| try (final Stream<Node.TypedNode> stream = node.getResources()) { | ||
| return stream.filter(child -> verifyContainmentIri(container, child)).map(child -> { | ||
| final Metadata.Builder builder = Metadata.newBuilder(); | ||
| getMetadata().getStorage().ifPresent(builder::storage); | ||
| child.getTypes().forEach(builder::type); | ||
| return new SolidResourceReference(URI.create(child.getIRIString()), builder.build()); | ||
| }).collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); | ||
| } | ||
| } | ||
| return Collections.emptySet(); | ||
| } | ||
| @Override | ||
| public ValidationResult validate() { | ||
| // Get the normalized container URI | ||
| final String container = normalize(getIdentifier()); | ||
| final List<String> messages = new ArrayList<>(); | ||
| // Verify that the container URI path ends with a slash | ||
| if (!container.endsWith("/")) { | ||
| messages.add("Container URI does not end in a slash"); | ||
| } | ||
| // Verify that all ldp:contains triples align with Solid expectations | ||
| getGraph().stream(null, rdf.createIRI(LDP.contains.toString()), null) | ||
| .collect(Collectors.partitioningBy(verifyContainmentTriple(container))) | ||
| .get(false) // we are only concerned with the invalid triples | ||
| .forEach(triple -> messages.add("Invalid containment triple: " + triple.getSubject().ntriplesString() + | ||
| " ldp:contains " + triple.getObject().ntriplesString() + " .")); | ||
| if (messages.isEmpty()) { | ||
| return new ValidationResult(true); | ||
| } | ||
| return new ValidationResult(false, messages); | ||
| } | ||
| /** | ||
| @@ -99,6 +132,49 @@ public Stream<SolidResource> getContainedResources() { | ||
| return getResources().stream(); | ||
| } | ||
| static String normalize(final IRI iri) { | ||
| return normalize(URI.create(iri.getIRIString())); | ||
| } | ||
| static String normalize(final URI uri) { | ||
| return uri.normalize().toString().split("#")[0].split("\\?")[0]; | ||
| } | ||
| static Predicate<Triple> verifyContainmentTriple(final String container) { | ||
| final IRI subject = rdf.createIRI(container); | ||
| return triple -> { | ||
| if (!triple.getSubject().equals(subject)) { | ||
| // Out-of-domain containment triple subject | ||
| return false; | ||
| } | ||
| if (triple.getObject() instanceof IRI) { | ||
| return verifyContainmentIri(container, (IRI) triple.getObject()); | ||
| } | ||
| // Non-URI containment triple object | ||
| return false; | ||
| }; | ||
| } | ||
| static boolean verifyContainmentIri(final String container, final IRI object) { | ||
| if (!object.getIRIString().startsWith(container)) { | ||
| // Out-of-domain containment triple object | ||
| return false; | ||
| } else { | ||
| final String relativePath = object.getIRIString().substring(container.length()); | ||
| final String normalizedPath = relativePath.endsWith("/") ? | ||
| relativePath.substring(0, relativePath.length() - 1) : relativePath; | ||
| if (normalizedPath.isEmpty()) { | ||
| // Containment triple subject and object cannot be the same | ||
| return false; | ||
| } | ||
| if (normalizedPath.contains("/")) { | ||
| // Containment cannot skip intermediate nodes | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| @SuppressWarnings("java:S2160") // Wrapper equality is correctly delegated to underlying node | ||
| static final class Node extends WrapperIRI { | ||
| private final IRI ldpContains = rdf.createIRI(LDP.contains.toString()); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| @prefix dct: <http://purl.org/dc/terms/>. | ||
| @prefix ldp: <http://www.w3.org/ns/ldp#>. | ||
| @prefix xsd: <http://www.w3.org/2001/XMLSchema#>. | ||
| @prefix pl: <http://www.w3.org/ns/iana/media-types/text/plain#>. | ||
| <> | ||
| a ldp:BasicContainer ; | ||
| dct:modified "2022-11-25T10:36:36Z"^^xsd:dateTime; | ||
| ldp:contains <newContainer/>, <test.txt>, <test2.txt> . | ||
| <newContainer/> | ||
| a ldp:BasicContainer ; | ||
| dct:modified "2022-11-25T10:36:36Z"^^xsd:dateTime . | ||
| <test.txt> | ||
| a pl:Resource, ldp:NonRDFSource; | ||
| dct:modified "2022-11-25T10:34:14Z"^^xsd:dateTime . | ||
| <test2.txt> | ||
| a pl:Resource, ldp:NonRDFSource; | ||
| dct:modified "2022-11-25T10:37:06Z"^^xsd:dateTime . | ||
| # These containment triples should not be included in a getResources response | ||
| <> | ||
| ldp:contains <https://example.com/other> , <newContainer/child> , <> , <./> . | ||
| <https://example.test/container/> | ||
| a ldp:BasicContainer ; | ||
| ldp:contains <https://example.test/container/external> . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe now you can call validate() here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not
validate()directly, as that performs a number of checks that are irrelevant here. You will see that in https://github.com/inrupt/solid-client-java/pull/571/files#diff-2c9b192110a924fa828ba14eaa2e7359afa8b3b811c8ad12fd6563e9161d7ba0R90, we re-use some of the same code as validate in this pipeline