Skip to content

Commit f82fd98

Browse files
committed
Add support for dataset expansion
1 parent afd31be commit f82fd98

File tree

9 files changed

+259
-7
lines changed

9 files changed

+259
-7
lines changed

acp/pom.xml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
<artifactId>inrupt-client-vocabulary</artifactId>
2525
<version>${project.version}</version>
2626
</dependency>
27+
<dependency>
28+
<groupId>com.inrupt.client</groupId>
29+
<artifactId>inrupt-client-solid</artifactId>
30+
<version>${project.version}</version>
31+
</dependency>
2732
<dependency>
2833
<groupId>org.slf4j</groupId>
2934
<artifactId>slf4j-api</artifactId>
@@ -79,12 +84,6 @@
7984
<version>${project.version}</version>
8085
<scope>test</scope>
8186
</dependency>
82-
<dependency>
83-
<groupId>com.inrupt.client</groupId>
84-
<artifactId>inrupt-client-solid</artifactId>
85-
<version>${project.version}</version>
86-
<scope>test</scope>
87-
</dependency>
8887
<dependency>
8988
<groupId>org.slf4j</groupId>
9089
<artifactId>slf4j-simple</artifactId>

acp/src/main/java/com/inrupt/client/acp/AccessControl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public static IRI asResource(final AccessControl accessControl, final Graph grap
4949
graph.add(accessControl, rdf.createIRI(ACP.apply.toString()), policy);
5050
Policy.asResource(policy, graph);
5151
});
52+
5253
return accessControl;
5354
}
5455

acp/src/main/java/com/inrupt/client/acp/AccessControlResource.java

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
package com.inrupt.client.acp;
2222

2323
import com.inrupt.client.RDFSource;
24+
import com.inrupt.client.solid.SolidClient;
25+
import com.inrupt.client.solid.SolidSyncClient;
2426
import com.inrupt.client.vocabulary.ACP;
2527
import com.inrupt.client.vocabulary.RDF;
2628
import com.inrupt.rdf.wrapping.commons.ValueMappings;
@@ -38,12 +40,16 @@
3840
import org.apache.commons.rdf.api.IRI;
3941
import org.apache.commons.rdf.api.Quad;
4042
import org.apache.commons.rdf.api.RDFTerm;
43+
import org.slf4j.Logger;
44+
import org.slf4j.LoggerFactory;
4145

4246
/**
4347
* An Access Control Resource type.
4448
*/
4549
public class AccessControlResource extends RDFSource {
4650

51+
private static final Logger LOGGER = LoggerFactory.getLogger(AccessControlResource.class);
52+
4753
public static final URI SOLID_ACCESS_GRANT = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant");
4854

4955
/**
@@ -79,6 +85,104 @@ public Set<AccessControl> memberAccessControl() {
7985
return new ACPNode(asIRI(getIdentifier()), getGraph()).memberAccessControl();
8086
}
8187

88+
/**
89+
* Expand the internal data using a synchronous client.
90+
*
91+
* @param client the solid client
92+
* @return an expanded access control resource
93+
*/
94+
public AccessControlResource expand(final SolidSyncClient client) {
95+
// Copy the data from the existing ACR into a new dataset
96+
final var dataset = rdf.createDataset();
97+
stream().forEach(dataset::add);
98+
99+
final var cache = rdf.createDataset();
100+
expandType(dataset, cache, asIRI(ACP.accessControl), asIRI(ACP.AccessControl),
101+
uri -> populateCache(client, uri, cache));
102+
expandType(dataset, cache, asIRI(ACP.memberAccessControl), asIRI(ACP.AccessControl),
103+
uri -> populateCache(client, uri, cache));
104+
expandType(dataset, cache, asIRI(ACP.apply), asIRI(ACP.Policy), uri -> populateCache(client, uri, cache));
105+
expandType(dataset, cache, asIRI(ACP.allOf), asIRI(ACP.Matcher), uri -> populateCache(client, uri, cache));
106+
expandType(dataset, cache, asIRI(ACP.anyOf), asIRI(ACP.Matcher), uri -> populateCache(client, uri, cache));
107+
expandType(dataset, cache, asIRI(ACP.noneOf), asIRI(ACP.Matcher), uri -> populateCache(client, uri, cache));
108+
109+
return new AccessControlResource(getIdentifier(), dataset);
110+
}
111+
112+
/**
113+
* Expand the internal data using an asynchronous client.
114+
*
115+
* @param client the solid client
116+
* @return an expanded access control resource
117+
*/
118+
public AccessControlResource expand(final SolidClient client) {
119+
// Copy the data from the existing ACR into a new dataset
120+
final var dataset = rdf.createDataset();
121+
stream().forEach(dataset::add);
122+
123+
final var cache = rdf.createDataset();
124+
expandType(dataset, cache, asIRI(ACP.accessControl), asIRI(ACP.AccessControl),
125+
uri -> populateCache(client, uri, cache));
126+
expandType(dataset, cache, asIRI(ACP.memberAccessControl), asIRI(ACP.AccessControl),
127+
uri -> populateCache(client, uri, cache));
128+
expandType(dataset, cache, asIRI(ACP.apply), asIRI(ACP.Policy), uri -> populateCache(client, uri, cache));
129+
expandType(dataset, cache, asIRI(ACP.allOf), asIRI(ACP.Matcher), uri -> populateCache(client, uri, cache));
130+
expandType(dataset, cache, asIRI(ACP.anyOf), asIRI(ACP.Matcher), uri -> populateCache(client, uri, cache));
131+
expandType(dataset, cache, asIRI(ACP.noneOf), asIRI(ACP.Matcher), uri -> populateCache(client, uri, cache));
132+
133+
return new AccessControlResource(getIdentifier(), dataset);
134+
}
135+
136+
void expandType(final Dataset dataset, final Dataset cache, final IRI predicate, final IRI type,
137+
final Consumer<URI> handler) {
138+
final var subjects = dataset.stream(null, null, predicate, null)
139+
.map(Quad::getObject).filter(IRI.class::isInstance).map(IRI.class::cast)
140+
.filter(subject -> !dataset.contains(null, subject, asIRI(RDF.type), type)).toList();
141+
142+
for (final var subject : subjects) {
143+
if (!cache.contains(null, subject, asIRI(RDF.type), type)) {
144+
handler.accept(URI.create(subject.getIRIString()));
145+
}
146+
cache.stream(null, subject, null, null).forEach(dataset::add);
147+
}
148+
}
149+
150+
void populateCache(final SolidSyncClient client, final URI uri, final Dataset cache) {
151+
try (final var acr = client.read(uri, AccessControlResource.class)) {
152+
acr.stream()
153+
.filter(quad -> !isAccessControlResourceType(quad))
154+
.forEach(cache::add);
155+
} catch (final Exception ex) {
156+
LOGGER.atDebug()
157+
.setMessage("Unable to fetch access control resource from {}: {}")
158+
.addArgument(uri)
159+
.addArgument(ex::getMessage)
160+
.log();
161+
}
162+
}
163+
164+
void populateCache(final SolidClient client, final URI uri, final Dataset cache) {
165+
client.read(uri, AccessControlResource.class).thenAccept(res -> {
166+
try (final var acr = res) {
167+
acr.stream()
168+
.filter(quad -> !isAccessControlResourceType(quad))
169+
.forEach(cache::add);
170+
}
171+
})
172+
.exceptionally(err -> {
173+
LOGGER.atDebug()
174+
.setMessage("Unable to fetch access control resource from {}: {}")
175+
.addArgument(uri)
176+
.addArgument(err::getMessage)
177+
.log();
178+
return null;
179+
}).toCompletableFuture().join();
180+
}
181+
182+
static boolean isAccessControlResourceType(final Quad quad) {
183+
return asIRI(RDF.type).equals(quad.getPredicate()) && asIRI(ACP.AccessControlResource).equals(quad.getObject());
184+
}
185+
82186
/**
83187
* Compact the internal data.
84188
*/
@@ -135,7 +239,7 @@ public enum MatcherType {
135239
}
136240

137241
public IRI asIRI() {
138-
return AccessControlResource.asIRI(predicate);
242+
return AccessControlResource.asIRI(asURI());
139243
}
140244

141245
public URI asURI() {

acp/src/test/java/com/inrupt/client/acp/AccessControlResourceTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static java.nio.charset.StandardCharsets.UTF_8;
2424
import static org.junit.jupiter.api.Assertions.*;
2525

26+
import com.inrupt.client.solid.SolidClient;
2627
import com.inrupt.client.solid.SolidSyncClient;
2728
import com.inrupt.client.spi.RDFFactory;
2829
import com.inrupt.client.vocabulary.ACL;
@@ -283,4 +284,55 @@ void buildAcrWithExistingPolicies() {
283284

284285
assertEquals(38, acr.size());
285286
}
287+
288+
@Test
289+
void expandAcr3Sync() {
290+
final var uri = mockHttpServer.acr3();
291+
try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
292+
assertEquals(2, acr.accessControl().size());
293+
assertEquals(3, acr.memberAccessControl().size());
294+
295+
// Check dataset size
296+
assertEquals(12, acr.size());
297+
298+
final var expanded = acr.expand(client);
299+
assertEquals(29, expanded.size());
300+
assertEquals(12, acr.size());
301+
}
302+
}
303+
304+
@Test
305+
void expandAcr3Async() {
306+
final var uri = mockHttpServer.acr3();
307+
final var asyncClient = SolidClient.getClient();
308+
asyncClient.read(uri, AccessControlResource.class).thenAccept(res -> {
309+
try (final var acr = res) {
310+
assertEquals(2, acr.accessControl().size());
311+
assertEquals(3, acr.memberAccessControl().size());
312+
313+
// Check dataset size
314+
assertEquals(12, acr.size());
315+
316+
final var expanded = acr.expand(asyncClient);
317+
assertEquals(29, expanded.size());
318+
assertEquals(12, acr.size());
319+
}
320+
}).toCompletableFuture().join();
321+
}
322+
323+
@Test
324+
void expandAcr4Sync() {
325+
final var uri = mockHttpServer.acr4();
326+
try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
327+
assertEquals(2, acr.accessControl().size());
328+
assertEquals(3, acr.memberAccessControl().size());
329+
330+
// Check dataset size
331+
assertEquals(20, acr.size());
332+
333+
final var expanded = acr.expand(client);
334+
assertEquals(22, expanded.size());
335+
assertEquals(20, acr.size());
336+
}
337+
}
286338
}

acp/src/test/java/com/inrupt/client/acp/AcpMockHttpService.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ public URI acr2() {
4343
return URI.create(wireMockServer.baseUrl() + "/acr-2");
4444
}
4545

46+
public URI acr3() {
47+
return URI.create(wireMockServer.baseUrl() + "/acr-3");
48+
}
49+
50+
public URI acr4() {
51+
return URI.create(wireMockServer.baseUrl() + "/acr-4");
52+
}
53+
4654
private void setupMocks() {
4755
wireMockServer.stubFor(get(urlEqualTo("/acr-1"))
4856
.willReturn(aResponse()
@@ -58,6 +66,34 @@ private void setupMocks() {
5866
.withBodyFile("acr-2.ttl")
5967
)
6068
);
69+
wireMockServer.stubFor(get(urlEqualTo("/acr-3"))
70+
.willReturn(aResponse()
71+
.withStatus(200)
72+
.withHeader("Content-Type", "text/turtle")
73+
.withBodyFile("acr-3.ttl")
74+
)
75+
);
76+
wireMockServer.stubFor(get(urlEqualTo("/acr-4"))
77+
.willReturn(aResponse()
78+
.withStatus(200)
79+
.withHeader("Content-Type", "text/turtle")
80+
.withBodyFile("acr-4.ttl")
81+
)
82+
);
83+
wireMockServer.stubFor(get(urlEqualTo("/not-an-acr"))
84+
.willReturn(aResponse()
85+
.withStatus(200)
86+
.withHeader("Content-Type", "text/turtle")
87+
.withBodyFile("not-an-acr.ttl")
88+
)
89+
);
90+
wireMockServer.stubFor(get(urlEqualTo("/also-not-an-acr"))
91+
.willReturn(aResponse()
92+
.withStatus(200)
93+
.withHeader("Content-Type", "application/json")
94+
.withBodyFile("not-an-acr.json")
95+
)
96+
);
6197
}
6298

6399
public String start() {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@prefix acp: <http://www.w3.org/ns/solid/acp#> .
2+
3+
<>
4+
a acp:AccessControlResource ;
5+
acp:accessControl <#owner-access-control> , <#indexer-access-control> ;
6+
acp:memberAccessControl <#owner-access-control> , <#indexer-access-control> , <#vc-access-control> .
7+
8+
<#owner-access-control>
9+
a acp:AccessControl ;
10+
acp:apply </acr-2#owner-policy> .
11+
12+
<#indexer-access-control>
13+
a acp:AccessControl ;
14+
acp:apply </acr-2#indexer-policy> .
15+
16+
<#vc-access-control>
17+
a acp:AccessControl ;
18+
acp:apply </acr-2#vc-policy> .
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@prefix acp: <http://www.w3.org/ns/solid/acp#> .
2+
@prefix acl: <http://www.w3.org/ns/auth/acl#> .
3+
@prefix vc: <http://www.w3.org/ns/solid/vc#> .
4+
5+
<>
6+
a acp:AccessControlResource ;
7+
acp:accessControl <#owner-access-control> , <#indexer-access-control> ;
8+
acp:memberAccessControl <#owner-access-control> , <#indexer-access-control> , <#vc-access-control> .
9+
10+
<#owner-access-control>
11+
a acp:AccessControl ;
12+
acp:apply </not-an-acr> .
13+
14+
<#indexer-access-control>
15+
a acp:AccessControl ;
16+
acp:apply <#indexer-policy> .
17+
<#indexer-policy>
18+
a acp:Policy ;
19+
acp:allOf </also-not-an-acr> ;
20+
acp:allow acl:Read .
21+
22+
<#vc-access-control>
23+
a acp:AccessControl ;
24+
acp:apply <#vc-policy> .
25+
<#vc-policy>
26+
a acp:Policy ;
27+
acp:allOf <#vc-matcher> ;
28+
acp:allow acl:Read .
29+
<#vc-matcher>
30+
a acp:Matcher ;
31+
acp:vc vc:SolidAccessGrant .
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Type",
3+
"title": "Example resource"
4+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@prefix dc: <http://purl.org/dc/terms/> .
2+
@prefix ex: <http://example.com/> .
3+
4+
<>
5+
a ex:Type ;
6+
dc:title "Example resource" .
7+

0 commit comments

Comments
 (0)