Skip to content

Commit d3fd371

Browse files
committed
Use verify mechanism
1 parent 403e066 commit d3fd371

File tree

5 files changed

+148
-30
lines changed

5 files changed

+148
-30
lines changed

api/src/main/java/com/inrupt/client/ValidationResult.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,18 @@ public class ValidationResult {
3838
* @param messages the messages from validation method
3939
*/
4040
public ValidationResult(final boolean valid, final String... messages) {
41+
this(valid, Arrays.asList(messages));
42+
}
43+
44+
/**
45+
* Create a ValidationResult object.
46+
*
47+
* @param valid the result from validation
48+
* @param messages the messages from validation method
49+
*/
50+
public ValidationResult(final boolean valid, final List<String> messages) {
4151
this.valid = valid;
42-
this.result = Arrays.asList(messages);
52+
this.result = messages;
4353
}
4454

4555
/**

solid/src/main/java/com/inrupt/client/solid/SolidContainer.java

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,26 @@
2020
*/
2121
package com.inrupt.client.solid;
2222

23+
import com.inrupt.client.ValidationResult;
2324
import com.inrupt.client.vocabulary.LDP;
2425
import com.inrupt.client.vocabulary.RDF;
2526
import com.inrupt.rdf.wrapping.commons.ValueMappings;
2627
import com.inrupt.rdf.wrapping.commons.WrapperIRI;
2728

2829
import java.net.URI;
30+
import java.util.ArrayList;
2931
import java.util.Collections;
32+
import java.util.List;
3033
import java.util.Set;
34+
import java.util.function.Predicate;
3135
import java.util.stream.Collectors;
3236
import java.util.stream.Stream;
3337

3438
import org.apache.commons.rdf.api.Dataset;
3539
import org.apache.commons.rdf.api.Graph;
3640
import org.apache.commons.rdf.api.IRI;
3741
import org.apache.commons.rdf.api.RDFTerm;
42+
import org.apache.commons.rdf.api.Triple;
3843

3944
/**
4045
* A Solid Container Object.
@@ -77,35 +82,45 @@ public SolidContainer(final URI identifier, final Dataset dataset, final Metadat
7782
* @return the contained resources
7883
*/
7984
public Set<SolidResource> getResources() {
80-
final String container = getIdentifier().toString();
85+
final String container = normalize(getIdentifier());
8186
// As defined by the Solid Protocol, containers always end with a slash.
8287
if (container.endsWith("/")) {
8388
final Node node = new Node(rdf.createIRI(getIdentifier().toString()), getGraph());
8489
try (final Stream<Node.TypedNode> stream = node.getResources()) {
85-
return stream.flatMap(child -> {
86-
final URI childLocation = URI.create(child.getIRIString()).normalize();
87-
// Solid containment is based on URI path hierarchy,
88-
// so all child resources must start with the URL of the parent
89-
if (childLocation.toString().startsWith(container)) {
90-
final String relativePath = childLocation.toString().substring(container.length());
91-
final String normalizedPath = relativePath.endsWith("/") ?
92-
relativePath.substring(0, relativePath.length() - 1) : relativePath;
93-
// Solid containment occurs via direct decent,
94-
// so any recursively contained resources must not be included
95-
if (!normalizedPath.isEmpty() && !normalizedPath.contains("/")) {
96-
final Metadata.Builder builder = Metadata.newBuilder();
97-
getMetadata().getStorage().ifPresent(builder::storage);
98-
child.getTypes().forEach(builder::type);
99-
return Stream.of(new SolidResourceReference(childLocation, builder.build()));
100-
}
101-
}
102-
return Stream.empty();
90+
return stream.filter(child -> verifyContainmentIri(container, child)).map(child -> {
91+
final Metadata.Builder builder = Metadata.newBuilder();
92+
getMetadata().getStorage().ifPresent(builder::storage);
93+
child.getTypes().forEach(builder::type);
94+
return new SolidResourceReference(URI.create(child.getIRIString()), builder.build());
10395
}).collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));
10496
}
10597
}
10698
return Collections.emptySet();
10799
}
108100

101+
@Override
102+
public ValidationResult validate() {
103+
// Get the normalized container URI
104+
final String container = normalize(getIdentifier());
105+
final List<String> messages = new ArrayList<>();
106+
// Verify that the container URI path ends with a slash
107+
if (!container.endsWith("/")) {
108+
messages.add("Container URI does not end in a slash");
109+
}
110+
111+
// Verify that all ldp:contains triples align with Solid expectations
112+
getGraph().stream(null, rdf.createIRI(LDP.contains.toString()), null)
113+
.collect(Collectors.partitioningBy(verifyContainmentTriple(container)))
114+
.get(false) // we are only concerned with the invalid triples
115+
.forEach(triple -> messages.add("Invalid containment triple: " + triple.getSubject().ntriplesString() +
116+
" ldp:contains " + triple.getObject().ntriplesString() + " ."));
117+
118+
if (messages.isEmpty()) {
119+
return new ValidationResult(true);
120+
}
121+
return new ValidationResult(false, messages);
122+
}
123+
109124
/**
110125
* Retrieve the resources contained in this SolidContainer.
111126
*
@@ -117,6 +132,49 @@ public Stream<SolidResource> getContainedResources() {
117132
return getResources().stream();
118133
}
119134

135+
static String normalize(final IRI iri) {
136+
return normalize(URI.create(iri.getIRIString()));
137+
}
138+
139+
static String normalize(final URI uri) {
140+
return uri.normalize().toString().split("#")[0].split("\\?")[0];
141+
}
142+
143+
static Predicate<Triple> verifyContainmentTriple(final String container) {
144+
final IRI subject = rdf.createIRI(container);
145+
return triple -> {
146+
if (!triple.getSubject().equals(subject)) {
147+
// Out-of-domain containment triple subject
148+
return false;
149+
}
150+
if (triple.getObject() instanceof IRI) {
151+
return verifyContainmentIri(container, (IRI) triple.getObject());
152+
}
153+
// Non-URI containment triple object
154+
return false;
155+
};
156+
}
157+
158+
static boolean verifyContainmentIri(final String container, final IRI object) {
159+
if (!object.getIRIString().startsWith(container)) {
160+
// Out-of-domain containment triple object
161+
return false;
162+
} else {
163+
final String relativePath = object.getIRIString().substring(container.length());
164+
final String normalizedPath = relativePath.endsWith("/") ?
165+
relativePath.substring(0, relativePath.length() - 1) : relativePath;
166+
if (normalizedPath.isEmpty()) {
167+
// Containment triple subject and object cannot be the same
168+
return false;
169+
}
170+
if (normalizedPath.contains("/")) {
171+
// Containment cannot skip intermediate nodes
172+
return false;
173+
}
174+
}
175+
return true;
176+
}
177+
120178
@SuppressWarnings("java:S2160") // Wrapper equality is correctly delegated to underlying node
121179
static final class Node extends WrapperIRI {
122180
private final IRI ldpContains = rdf.createIRI(LDP.contains.toString());

solid/src/test/java/com/inrupt/client/solid/SolidClientTest.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import com.inrupt.client.ClientProvider;
2727
import com.inrupt.client.Headers;
28+
import com.inrupt.client.Request;
2829
import com.inrupt.client.Response;
2930
import com.inrupt.client.auth.Session;
3031
import com.inrupt.client.spi.RDFFactory;
@@ -163,7 +164,7 @@ void testGetResource() throws IOException, InterruptedException {
163164

164165
@Test
165166
void testGetContainer() throws IOException, InterruptedException {
166-
final URI uri = URI.create(config.get("solid_resource_uri") + "/playlist");
167+
final URI uri = URI.create(config.get("solid_resource_uri") + "/playlists/");
167168

168169
client.read(uri, SolidContainer.class).thenAccept(container -> {
169170
try (final SolidContainer c = container) {
@@ -219,19 +220,28 @@ void testGetBinaryCreate() {
219220
}
220221

221222
@Test
222-
void testSolidContainer() {
223+
void testSolidContainerWithInvalidData() {
224+
final URI uri = URI.create(config.get("solid_resource_uri") + "/container/");
225+
final CompletionException err = assertThrows(CompletionException.class,
226+
client.read(uri, SolidContainer.class).toCompletableFuture()::join);
227+
assertInstanceOf(DataMappingException.class, err.getCause());
228+
}
229+
230+
@Test
231+
void testLowLevelSolidContainer() {
223232
final URI uri = URI.create(config.get("solid_resource_uri") + "/container/");
233+
224234
final Set<URI> expected = new HashSet<>();
225235
expected.add(URIBuilder.newBuilder(uri).path("newContainer/").build());
226236
expected.add(URIBuilder.newBuilder(uri).path("test.txt").build());
227237
expected.add(URIBuilder.newBuilder(uri).path("test2.txt").build());
228238

229-
client.read(uri, SolidContainer.class).thenAccept(container -> {
230-
try (final SolidContainer c = container) {
231-
assertEquals(expected,
232-
c.getResources().stream().map(SolidResource::getIdentifier).collect(Collectors.toSet()));
233-
}
234-
}).toCompletableFuture().join();
239+
client.send(Request.newBuilder(uri).build(), SolidResourceHandlers.ofSolidContainer())
240+
.thenAccept(response -> {
241+
final SolidContainer container = response.body();
242+
assertEquals(expected, container.getResources().stream()
243+
.map(SolidResource::getIdentifier).collect(Collectors.toSet()));
244+
}).toCompletableFuture().join();
235245
}
236246

237247
@Test

solid/src/test/java/com/inrupt/client/solid/SolidMockHttpService.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,40 @@ private void setupMocks() {
149149
.willReturn(aResponse()
150150
.withStatus(204)));
151151

152+
wireMockServer.stubFor(get(urlEqualTo("/playlists/"))
153+
.withHeader("User-Agent", equalTo(USER_AGENT))
154+
.willReturn(aResponse()
155+
.withStatus(200)
156+
.withHeader("Content-Type", "text/turtle")
157+
.withHeader("Link", Link.of(LDP.BasicContainer, "type").toString())
158+
.withHeader("Link", Link.of(URI.create("http://storage.example/"),
159+
PIM.storage).toString())
160+
.withHeader("Link", Link.of(URI.create("https://history.test/"), "timegate").toString())
161+
.withHeader("Link", Link.of(URI.create("http://acl.example/playlists"), "acl").toString())
162+
.withHeader("WAC-Allow", "user=\"read write\",public=\"read\"")
163+
.withHeader("Allow", "POST, PUT, PATCH")
164+
.withHeader("Accept-Post", "application/ld+json, text/turtle")
165+
.withHeader("Accept-Put", "application/ld+json, text/turtle")
166+
.withHeader("Accept-Patch", "application/sparql-update, text/n3")
167+
.withBodyFile("playlist.ttl")
168+
)
169+
);
170+
171+
wireMockServer.stubFor(put(urlEqualTo("/playlists/"))
172+
.withHeader("User-Agent", equalTo(USER_AGENT))
173+
.withHeader("Content-Type", containing("text/turtle"))
174+
.withRequestBody(containing(
175+
"<https://library.test/12345/song1.mp3>"))
176+
.willReturn(aResponse()
177+
.withStatus(204)));
178+
179+
wireMockServer.stubFor(delete(urlEqualTo("/playlists/"))
180+
.withHeader("User-Agent", equalTo(USER_AGENT))
181+
.willReturn(aResponse()
182+
.withStatus(204)));
183+
184+
185+
152186

153187
wireMockServer.stubFor(get(urlEqualTo("/playlist"))
154188
.withHeader("User-Agent", equalTo(USER_AGENT))
@@ -159,7 +193,7 @@ private void setupMocks() {
159193
.withHeader("Link", Link.of(URI.create("http://storage.example/"),
160194
PIM.storage).toString())
161195
.withHeader("Link", Link.of(URI.create("https://history.test/"), "timegate").toString())
162-
.withHeader("Link", Link.of(URI.create("http://acl.example/playlist"), "acl").toString())
196+
.withHeader("Link", Link.of(URI.create("http://acl.example/playlists"), "acl").toString())
163197
.withHeader("WAC-Allow", "user=\"read write\",public=\"read\"")
164198
.withHeader("Allow", "POST, PUT, PATCH")
165199
.withHeader("Accept-Post", "application/ld+json, text/turtle")

solid/src/test/java/com/inrupt/client/solid/SolidSyncClientTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ void testGetResource() {
134134

135135
@Test
136136
void testGetContainer() {
137-
final URI uri = URI.create(config.get("solid_resource_uri") + "/playlist");
137+
final URI uri = URI.create(config.get("solid_resource_uri") + "/playlists/");
138138

139139
try (final SolidContainer container = client.read(uri, SolidContainer.class)) {
140140
assertEquals(uri, container.getIdentifier());
@@ -156,6 +156,12 @@ void testGetContainer() {
156156
}
157157
}
158158

159+
@Test
160+
void testGetContainerDataMappingError() {
161+
final URI uri = URI.create(config.get("solid_resource_uri") + "/playlist");
162+
assertThrows(DataMappingException.class, () -> client.read(uri, SolidContainer.class));
163+
}
164+
159165
@Test
160166
void testGetInvalidType() {
161167
final URI uri = URI.create(config.get("solid_resource_uri") + "/playlist");

0 commit comments

Comments
 (0)