diff --git a/.gitignore b/.gitignore index 02f4d13c8..8e0a8f99e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build .classpath .project .settings/ +.DS_Store diff --git a/trellis-api/src/main/java/org/trellisldp/api/AppendService.java b/trellis-api/src/main/java/org/trellisldp/api/AppendService.java new file mode 100644 index 000000000..1f4dddad7 --- /dev/null +++ b/trellis-api/src/main/java/org/trellisldp/api/AppendService.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.trellisldp.api; + +import java.util.concurrent.Future; + +/** + * A service that persists resources by appending to their records. Nothing that + * is recorded by {@link #add(Object)} will be deleted by using {@code add} again. + * + * @author ajs6f + * + * @param the type of resource that can be persisted by this service + */ +public interface AppendService { + + /** + * @param resource a resource to persist + * @return whether the resource was successfully persisted + */ + Future add(T resource); +} \ No newline at end of file diff --git a/trellis-api/src/main/java/org/trellisldp/api/AuditService.java b/trellis-api/src/main/java/org/trellisldp/api/AuditService.java index 72f157936..b9daefcc0 100644 --- a/trellis-api/src/main/java/org/trellisldp/api/AuditService.java +++ b/trellis-api/src/main/java/org/trellisldp/api/AuditService.java @@ -24,7 +24,7 @@ * * @author acoburn */ -public interface AuditService { +public interface AuditService extends AppendService { /** * Generate the audit quads for a Create event. diff --git a/trellis-api/src/main/java/org/trellisldp/api/RDFUtils.java b/trellis-api/src/main/java/org/trellisldp/api/RDFUtils.java index 500ff163d..e5b97642a 100644 --- a/trellis-api/src/main/java/org/trellisldp/api/RDFUtils.java +++ b/trellis-api/src/main/java/org/trellisldp/api/RDFUtils.java @@ -13,11 +13,22 @@ */ package org.trellisldp.api; +import static java.util.Collections.newSetFromMap; +import static java.util.Collections.unmodifiableSet; +import static java.util.EnumSet.of; +import static java.util.stream.Collector.Characteristics.CONCURRENT; +import static java.util.stream.Collector.Characteristics.IDENTITY_FINISH; import static java.util.stream.Collector.Characteristics.UNORDERED; -import static java.util.stream.Collector.of; import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collector; +import java.util.stream.Stream; import org.apache.commons.rdf.api.Dataset; import org.apache.commons.rdf.api.Graph; @@ -71,7 +82,7 @@ public static RDF getInstance() { * @return a graph */ public static Collector toGraph() { - return of(rdf::createGraph, Graph::add, (left, right) -> { + return Collector.of(rdf::createGraph, Graph::add, (left, right) -> { right.iterate().forEach(left::add); return left; }, UNORDERED); @@ -80,13 +91,88 @@ public static RDF getInstance() { /** * Collect a stream of Quads into a Dataset. * - * @return a dataset + * @return a {@link Collector} that accumulates a {@link Stream} of + * {@link Quad}s into a {@link Dataset} */ - public static Collector toDataset() { - return of(rdf::createDataset, Dataset::add, (left, right) -> { - right.iterate().forEach(left::add); - return left; - }, UNORDERED); + public static DatasetCollector toDataset() { + return new DatasetCollector(); + } + + static class DatasetCollector implements Collector { + + @Override + public Supplier supplier() { + return rdf::createDataset; + } + + @Override + public BiConsumer accumulator() { + return Dataset::add; + } + + @Override + public BinaryOperator combiner() { + return (left, right) -> { + right.iterate().forEach(left::add); + return left; + }; + } + + @Override + public Function finisher() { + return x -> x; + } + + @Override + public Set characteristics() { + return unmodifiableSet(of(UNORDERED, IDENTITY_FINISH)); + } + + /** + * Collect a stream of {@link Quad}s into a {@link Dataset} with concurrent + * operation. + * + * @return a {@link Collector} that accumulates a {@link Stream} of + * {@link Quad}s into a {@link Dataset} + */ + public ConcurrentDatasetCollector concurrent() { + return new ConcurrentDatasetCollector(); + } + } + + private static class ConcurrentDatasetCollector implements Collector, Dataset> { + + @Override + public Supplier> supplier() { + return () -> newSetFromMap(new ConcurrentHashMap<>()); + } + + @Override + public BiConsumer, Quad> accumulator() { + return Set::add; + } + + @Override + public BinaryOperator> combiner() { + return (s1, s2) -> { + s1.addAll(s2); + return s1; + }; + } + + @Override + public Function, Dataset> finisher() { + return set -> { + final Dataset dataset = rdf.createDataset(); + set.forEach(dataset::add); + return dataset; + }; + } + + @Override + public Set characteristics() { + return unmodifiableSet(of(UNORDERED, CONCURRENT)); + } } private RDFUtils() { diff --git a/trellis-api/src/main/java/org/trellisldp/api/ReplaceService.java b/trellis-api/src/main/java/org/trellisldp/api/ReplaceService.java new file mode 100644 index 000000000..e3299b444 --- /dev/null +++ b/trellis-api/src/main/java/org/trellisldp/api/ReplaceService.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.trellisldp.api; + +import java.util.concurrent.Future; + +/** + * A service that persists resources by replacing their records. + * + * @author ajs6f + * + * @param the type of resource that can be persisted by this service + */ +public interface ReplaceService extends RetrievalService { + + /** + * @param resource a resource to persist + * @return whether the resource was successfully persisted + */ + Future put(T resource); + +} diff --git a/trellis-api/src/main/java/org/trellisldp/api/ResourceService.java b/trellis-api/src/main/java/org/trellisldp/api/ResourceService.java index 5874b0ef0..d7986508a 100644 --- a/trellis-api/src/main/java/org/trellisldp/api/ResourceService.java +++ b/trellis-api/src/main/java/org/trellisldp/api/ResourceService.java @@ -17,6 +17,7 @@ import static org.trellisldp.api.RDFUtils.TRELLIS_BNODE_PREFIX; import static org.trellisldp.api.RDFUtils.TRELLIS_DATA_PREFIX; import static org.trellisldp.api.RDFUtils.getInstance; +import static org.trellisldp.api.RDFUtils.toDataset; import java.time.Instant; import java.util.Collection; @@ -40,24 +41,12 @@ * * @author acoburn */ -public interface ResourceService { +public interface ResourceService extends ReplaceService { - /** - * Get a resource from the given location. - * - * @param identifier the resource identifier - * @return the resource - */ - Optional get(IRI identifier); - - /** - * Get a resource from the given location and time. - * - * @param identifier the resource identifier - * @param time the time - * @return the resource - */ - Optional get(IRI identifier, Instant time); + @Override + default Future put(Resource res) { + return put(res.getIdentifier(), res.getInteractionModel(), res.stream().collect(toDataset().concurrent())); + } /** * Put a resource into the server. diff --git a/trellis-api/src/main/java/org/trellisldp/api/RetrievalService.java b/trellis-api/src/main/java/org/trellisldp/api/RetrievalService.java new file mode 100644 index 000000000..342cee321 --- /dev/null +++ b/trellis-api/src/main/java/org/trellisldp/api/RetrievalService.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.trellisldp.api; + +import java.time.Instant; +import java.util.Optional; + +import org.apache.commons.rdf.api.IRI; + +/** + * A service that can retrieve resources of some type, featuring optional + * retrieval by time. + * + * @author ajs6f + * + * @param the type of resource available from this service + */ +public interface RetrievalService { + + /** + * Get a resource by the given identifier. + * + * @param identifier the resource identifier + * @return the resource + */ + Optional get(IRI identifier); + + /** + * Get a resource by the given identifier and time. + * + * @param identifier the resource identifier + * @param time the time + * @return the resource + */ + default Optional get(IRI identifier, Instant time) { + return get(identifier); + } +} diff --git a/trellis-audit/build.gradle b/trellis-audit/build.gradle index 7bbeeb1c0..26dd7480e 100644 --- a/trellis-audit/build.gradle +++ b/trellis-audit/build.gradle @@ -39,3 +39,9 @@ jar { } } +javadoc { + options.tags = ["apiNote:a:API Note:", + "implSpec:a:Implementation Requirements:", + "implNote:a:Implementation Note:"] +} + diff --git a/trellis-audit/src/main/java/org/trellisldp/audit/DefaultAuditService.java b/trellis-audit/src/main/java/org/trellisldp/audit/DefaultAuditService.java index f6af4e3e8..3130911d0 100644 --- a/trellis-audit/src/main/java/org/trellisldp/audit/DefaultAuditService.java +++ b/trellis-audit/src/main/java/org/trellisldp/audit/DefaultAuditService.java @@ -20,23 +20,32 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; import org.apache.commons.rdf.api.BlankNode; import org.apache.commons.rdf.api.IRI; import org.apache.commons.rdf.api.Quad; import org.apache.commons.rdf.api.RDF; import org.trellisldp.api.AuditService; +import org.trellisldp.api.Resource; import org.trellisldp.api.Session; import org.trellisldp.vocabulary.AS; import org.trellisldp.vocabulary.PROV; import org.trellisldp.vocabulary.XSD; /** - * An {@link AuditService} that generates Audit-related {@link Quad}s for various write operations. - * - *

This class makes use of the {@link PROV} vocabulary and {@link BlankNode} objects in a - * {@code http://www.trellisldp.org/ns/trellis#PreferAudit} named graph. + * An {@link AuditService} that generates Audit-related {@link Quad}s for + * various write operations. * + *

This class makes use of the {@link PROV} vocabulary and {@link BlankNode} + * objects in a {@code http://www.trellisldp.org/ns/trellis#PreferAudit} named + * graph. + * + * @implSpec This implementation of AuditService does not persist audit + * information. Subclass and override {@link #add(Resource)} to add + * persistence. + * * @author acoburn */ public class DefaultAuditService implements AuditService { @@ -70,4 +79,14 @@ private List auditData(final IRI subject, final Session session, final Lis data.add(rdf.createQuad(PreferAudit, bnode, PROV.actedOnBehalfOf, delegate))); return data; } + + /* + * Override to provide persistence for audit information. + * + * @see org.trellisldp.api.AppendService#add(java.lang.Object) + */ + @Override + public Future add(final Resource resource) { + return CompletableFuture.completedFuture(false); + } } diff --git a/trellis-karaf/src/main/resources/features.xml b/trellis-karaf/src/main/resources/features.xml index b12fbe099..8295d7df3 100644 --- a/trellis-karaf/src/main/resources/features.xml +++ b/trellis-karaf/src/main/resources/features.xml @@ -131,6 +131,7 @@ commons-rdf-jena trellis-api + trellis-audit trellis-vocabulary mvn:org.trellisldp/trellis-triplestore/${project.version} diff --git a/trellis-test/src/test/java/org/trellisldp/test/OSGiTest.java b/trellis-test/src/test/java/org/trellisldp/test/OSGiTest.java index 1e824824c..9f496f265 100644 --- a/trellis-test/src/test/java/org/trellisldp/test/OSGiTest.java +++ b/trellis-test/src/test/java/org/trellisldp/test/OSGiTest.java @@ -97,7 +97,14 @@ public void testCoreInstallation() throws Exception { } @Test - public void testTriplestoreInstallation() throws Exception { + public void testAuditAndTriplestoreInstallation() throws Exception { + // test these two together because trellis-triplestore depends on trellis-audit + assertFalse(featuresService.isInstalled(featuresService.getFeature("trellis-audit"))); + featuresService.installFeature("trellis-audit"); + assertTrue(featuresService.isInstalled(featuresService.getFeature("trellis-audit"))); + featuresService.uninstallFeature("trellis-audit"); + assertFalse(featuresService.isInstalled(featuresService.getFeature("trellis-audit"))); + assertFalse(featuresService.isInstalled(featuresService.getFeature("trellis-triplestore"))); featuresService.installFeature("trellis-triplestore"); assertTrue(featuresService.isInstalled(featuresService.getFeature("trellis-triplestore"))); @@ -131,13 +138,6 @@ public void testAgentInstallation() throws Exception { assertTrue(featuresService.isInstalled(featuresService.getFeature("trellis-agent"))); } - @Test - public void testAuditInstallation() throws Exception { - assertFalse(featuresService.isInstalled(featuresService.getFeature("trellis-audit"))); - featuresService.installFeature("trellis-audit"); - assertTrue(featuresService.isInstalled(featuresService.getFeature("trellis-audit"))); - } - @Test public void testFileInstallation() throws Exception { assertFalse(featuresService.isInstalled(featuresService.getFeature("trellis-file"))); @@ -166,17 +166,16 @@ public void testNamespacesInstallation() throws Exception { assertTrue(featuresService.isInstalled(featuresService.getFeature("trellis-namespaces"))); } - @SuppressWarnings("unchecked") protected T getOsgiService(final Class type, final String filter, final long timeout) { try { - final ServiceTracker tracker = new ServiceTracker(bundleContext, + final ServiceTracker tracker = new ServiceTracker<>(bundleContext, createFilter("(&(" + OBJECTCLASS + "=" + type.getName() + ")" + filter + ")"), null); tracker.open(true); - final Object svc = type.cast(tracker.waitForService(timeout)); + final T svc = tracker.waitForService(timeout); if (svc == null) { throw new RuntimeException("Gave up waiting for service " + filter); } - return type.cast(svc); + return svc; } catch (InvalidSyntaxException e) { throw new IllegalArgumentException("Invalid filter", e); } catch (InterruptedException e) { diff --git a/trellis-triplestore/build.gradle b/trellis-triplestore/build.gradle index f248bb7cd..52767b180 100644 --- a/trellis-triplestore/build.gradle +++ b/trellis-triplestore/build.gradle @@ -13,13 +13,13 @@ dependencies { implementation group: 'org.apache.commons', name: 'commons-rdf-jena', version: commonsRdfVersion implementation group: 'org.apache.jena', name: 'jena-osgi', version: jenaVersion implementation project(':trellis-vocabulary') + implementation project(':trellis-audit'); testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion testImplementation group: 'org.apiguardian', name: 'apiguardian-api', version: apiguardianVersion testImplementation group: 'org.junit.platform', name: 'junit-platform-runner', version: junitPlatformVersion testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junitVersion testImplementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion - testImplementation project(':trellis-audit'); testImplementation project(':trellis-id'); } diff --git a/trellis-triplestore/src/main/java/org/trellisldp/triplestore/TriplestoreResourceService.java b/trellis-triplestore/src/main/java/org/trellisldp/triplestore/TriplestoreResourceService.java index dcb84cbe5..4dd26b2a2 100644 --- a/trellis-triplestore/src/main/java/org/trellisldp/triplestore/TriplestoreResourceService.java +++ b/trellis-triplestore/src/main/java/org/trellisldp/triplestore/TriplestoreResourceService.java @@ -84,6 +84,7 @@ import org.trellisldp.api.MementoService; import org.trellisldp.api.Resource; import org.trellisldp.api.ResourceService; +import org.trellisldp.audit.DefaultAuditService; import org.trellisldp.vocabulary.ACL; import org.trellisldp.vocabulary.AS; import org.trellisldp.vocabulary.DC; @@ -96,7 +97,7 @@ /** * A triplestore-based implementation of the Trellis ResourceService API. */ -public class TriplestoreResourceService implements ResourceService { +public class TriplestoreResourceService extends DefaultAuditService implements ResourceService { private static final Var PARENT = Var.alloc("parent"); private static final Var MODIFIED = Var.alloc("modified"); diff --git a/trellis-triplestore/src/test/java/org/trellisldp/triplestore/TriplestoreResourceTest.java b/trellis-triplestore/src/test/java/org/trellisldp/triplestore/TriplestoreResourceTest.java index 81b26d683..f496648e7 100644 --- a/trellis-triplestore/src/test/java/org/trellisldp/triplestore/TriplestoreResourceTest.java +++ b/trellis-triplestore/src/test/java/org/trellisldp/triplestore/TriplestoreResourceTest.java @@ -68,6 +68,8 @@ public class TriplestoreResourceTest { private static final IRI aclId = rdf.createIRI("trellis:resource?ext=acl"); private static final IRI aclSubject = rdf.createIRI("trellis:resource#auth"); private static final IRI other = rdf.createIRI("trellis:other"); + + private static final AuditService auditService = new DefaultAuditService(); private final Instant created = now();