Repository: marmotta Updated Branches: refs/heads/ldp e66e59d75 -> 370a49b68
MARMOTTA-440: implemented HTTP PATCH operation Project: http://git-wip-us.apache.org/repos/asf/marmotta/repo Commit: http://git-wip-us.apache.org/repos/asf/marmotta/commit/05e03798 Tree: http://git-wip-us.apache.org/repos/asf/marmotta/tree/05e03798 Diff: http://git-wip-us.apache.org/repos/asf/marmotta/diff/05e03798 Branch: refs/heads/ldp Commit: 05e03798f53469d58bc5543de974b4b1a41d4e98 Parents: e66e59d Author: Jakob Frank <[email protected]> Authored: Tue Feb 25 17:57:42 2014 +0100 Committer: Jakob Frank <[email protected]> Committed: Tue Feb 25 18:01:10 2014 +0100 ---------------------------------------------------------------------- .../marmotta/platform/ldp/api/LdpService.java | 4 ++ .../InvalidModificationException.java | 40 +++++++++++++++++ .../patch/InvalidPatchDocumentException.java | 40 +++++++++++++++++ .../platform/ldp/patch/RdfPatchUtil.java | 25 ++++++++--- .../platform/ldp/services/LdpServiceImpl.java | 36 +++++++++++++++ .../platform/ldp/webservices/LdpWebService.java | 46 +++++++++++++++++--- .../platform/ldp/patch/RdfPatchUtilTest.java | 19 ++++++++ .../ldp/webservices/LdpWebServiceTest.java | 6 +-- 8 files changed, 199 insertions(+), 17 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/marmotta/blob/05e03798/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/api/LdpService.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/api/LdpService.java b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/api/LdpService.java index 887aa8c..3429599 100644 --- a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/api/LdpService.java +++ b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/api/LdpService.java @@ -17,6 +17,9 @@ */ package org.apache.marmotta.platform.ldp.api; +import org.apache.marmotta.platform.ldp.exceptions.InvalidModificationException; +import org.apache.marmotta.platform.ldp.patch.InvalidPatchDocumentException; +import org.apache.marmotta.platform.ldp.patch.parser.ParseException; import org.openrdf.model.Statement; import org.openrdf.model.URI; import org.openrdf.repository.RepositoryException; @@ -62,4 +65,5 @@ public interface LdpService { boolean deleteResource(String resource) throws RepositoryException; + void patchResource(String resource, InputStream patchData) throws RepositoryException, ParseException, InvalidModificationException, InvalidPatchDocumentException; } http://git-wip-us.apache.org/repos/asf/marmotta/blob/05e03798/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/exceptions/InvalidModificationException.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/exceptions/InvalidModificationException.java b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/exceptions/InvalidModificationException.java new file mode 100644 index 0000000..32e1bde --- /dev/null +++ b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/exceptions/InvalidModificationException.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.marmotta.platform.ldp.exceptions; + +/** + * Created by jakob on 2/25/14. + */ +public class InvalidModificationException extends Exception { + public InvalidModificationException() { + super(); + } + + public InvalidModificationException(String message) { + super(message); + } + + public InvalidModificationException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidModificationException(Throwable cause) { + super(cause); + } + +} http://git-wip-us.apache.org/repos/asf/marmotta/blob/05e03798/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/patch/InvalidPatchDocumentException.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/patch/InvalidPatchDocumentException.java b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/patch/InvalidPatchDocumentException.java new file mode 100644 index 0000000..7e5a873 --- /dev/null +++ b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/patch/InvalidPatchDocumentException.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.marmotta.platform.ldp.patch; + +/** + * Created by jakob on 2/25/14. + */ +public class InvalidPatchDocumentException extends Exception { + + public InvalidPatchDocumentException() { + super(); + } + + public InvalidPatchDocumentException(String message) { + super(message); + } + + public InvalidPatchDocumentException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidPatchDocumentException(Throwable cause) { + super(cause); + } +} http://git-wip-us.apache.org/repos/asf/marmotta/blob/05e03798/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtil.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtil.java b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtil.java index be43762..8be7652 100644 --- a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtil.java +++ b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtil.java @@ -18,6 +18,7 @@ package org.apache.marmotta.platform.ldp.patch; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.marmotta.platform.ldp.patch.model.PatchLine; import org.apache.marmotta.platform.ldp.patch.parser.ParseException; @@ -39,21 +40,21 @@ import java.util.List; */ public class RdfPatchUtil { - public static void applyPatch(Repository repository, String patch, Resource... contexts) throws RepositoryException, ParseException { + public static void applyPatch(Repository repository, String patch, Resource... contexts) throws RepositoryException, ParseException, InvalidPatchDocumentException { applyPatch(repository, getPatch(patch), contexts); } - public static void applyPatch(Repository repository, InputStream patchSource, Resource... contexts) throws RepositoryException, ParseException { + public static void applyPatch(Repository repository, InputStream patchSource, Resource... contexts) throws RepositoryException, ParseException, InvalidPatchDocumentException { applyPatch(repository, getPatch(patchSource), contexts); } - public static void applyPatch(RepositoryConnection con, String patch, Resource... contexts) throws RepositoryException, ParseException { + public static void applyPatch(RepositoryConnection con, String patch, Resource... contexts) throws RepositoryException, ParseException, InvalidPatchDocumentException { applyPatch(con, getPatch(patch), contexts); } - public static void applyPatch(RepositoryConnection con, InputStream patchSource, Resource... contexts) throws RepositoryException, ParseException { + public static void applyPatch(RepositoryConnection con, InputStream patchSource, Resource... contexts) throws RepositoryException, ParseException, InvalidPatchDocumentException { applyPatch(con, getPatch(patchSource), contexts); } - public static void applyPatch(Repository repository, List<PatchLine> patch, Resource... contexts) throws RepositoryException { + public static void applyPatch(Repository repository, List<PatchLine> patch, Resource... contexts) throws RepositoryException, InvalidPatchDocumentException { RepositoryConnection con = repository.getConnection(); try { con.begin(); @@ -67,7 +68,7 @@ public class RdfPatchUtil { } } - public static void applyPatch(RepositoryConnection con, List<PatchLine> patch, Resource... contexts) throws RepositoryException { + public static void applyPatch(RepositoryConnection con, List<PatchLine> patch, Resource... contexts) throws RepositoryException, InvalidPatchDocumentException { Resource subject = null; URI predicate = null; Value object = null; @@ -78,6 +79,18 @@ public class RdfPatchUtil { predicate = statement.getPredicate()!=null?statement.getPredicate():predicate; object = statement.getObject()!=null?statement.getObject():object; + if (subject == null || predicate == null || object == null) { + if (subject == null) { + throw new InvalidPatchDocumentException("Cannot resolve 'R' - subject was never set"); + } + if (predicate == null) { + throw new InvalidPatchDocumentException("Cannot resolve 'R' - predicate was never set"); + } + if (object == null) { + throw new InvalidPatchDocumentException("Cannot resolve 'R' - object was never set"); + } + } + switch (patchLine.getOperator()) { case ADD: con.add(subject, predicate, object, contexts); http://git-wip-us.apache.org/repos/asf/marmotta/blob/05e03798/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/services/LdpServiceImpl.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/services/LdpServiceImpl.java b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/services/LdpServiceImpl.java index b49f9d2..030d930 100644 --- a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/services/LdpServiceImpl.java +++ b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/services/LdpServiceImpl.java @@ -23,6 +23,12 @@ import org.apache.marmotta.commons.vocabulary.LDP; import org.apache.marmotta.platform.core.api.config.ConfigurationService; import org.apache.marmotta.platform.core.api.triplestore.SesameService; import org.apache.marmotta.platform.ldp.api.LdpService; +import org.apache.marmotta.platform.ldp.exceptions.InvalidModificationException; +import org.apache.marmotta.platform.ldp.patch.InvalidPatchDocumentException; +import org.apache.marmotta.platform.ldp.patch.RdfPatchUtil; +import org.apache.marmotta.platform.ldp.patch.model.PatchLine; +import org.apache.marmotta.platform.ldp.patch.parser.ParseException; +import org.apache.marmotta.platform.ldp.patch.parser.RdfPatchParser; import org.apache.marmotta.platform.ldp.util.LdpWebServiceUtils; import org.openrdf.model.Literal; import org.openrdf.model.Statement; @@ -235,6 +241,36 @@ public class LdpServiceImpl implements LdpService { } @Override + public void patchResource(String resource, InputStream patchData) throws RepositoryException, ParseException, InvalidModificationException, InvalidPatchDocumentException { + final RepositoryConnection conn = sesameService.getConnection(); + final URI rUri = conn.getValueFactory().createURI(resource); + final URI ldpContext = conn.getValueFactory().createURI(LDP.NAMESPACE); + + final Literal now = conn.getValueFactory().createLiteral(new Date()); + + + log.trace("parsing patch"); + List<PatchLine> patch = new RdfPatchParser(patchData).parsePatch(); + + // we are allowed to restrict the patch contents (Sec. ???) + log.trace("checking for invalid patch statements"); + for (PatchLine patchLine : patch) { + if (LDP.contains.equals(patchLine.getStatement().getPredicate())) { + throw new InvalidModificationException("must not change <" + LDP.contains.stringValue() + "> via PATCH"); + } + } + + log.debug("patching <{}> ({} changes)", resource, patch.size()); + + RdfPatchUtil.applyPatch(conn, patch, rUri); + + log.trace("update resource meta"); + conn.remove(rUri, DCTERMS.modified, null, ldpContext); + conn.add(rUri, DCTERMS.modified, now, ldpContext); + + } + + @Override public boolean deleteResource(String resource) throws RepositoryException { return deleteResource(buildURI(resource)); } http://git-wip-us.apache.org/repos/asf/marmotta/blob/05e03798/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/webservices/LdpWebService.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/webservices/LdpWebService.java b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/webservices/LdpWebService.java index 9fe07e6..9163b67 100644 --- a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/webservices/LdpWebService.java +++ b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/webservices/LdpWebService.java @@ -21,6 +21,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.marmotta.commons.vocabulary.LDP; import org.apache.marmotta.platform.core.api.config.ConfigurationService; import org.apache.marmotta.platform.ldp.api.LdpService; +import org.apache.marmotta.platform.ldp.exceptions.InvalidModificationException; +import org.apache.marmotta.platform.ldp.patch.InvalidPatchDocumentException; +import org.apache.marmotta.platform.ldp.patch.parser.ParseException; import org.apache.marmotta.platform.ldp.util.EntityTagUtils; import org.apache.marmotta.platform.ldp.util.LdpWebServiceUtils; import org.openrdf.model.Statement; @@ -163,7 +166,7 @@ public class LdpWebService { @PUT public Response PUT(@Context UriInfo uriInfo, @Context Request request, @HeaderParam(HttpHeaders.IF_MATCH) EntityTag eTag, - InputStream postBody, @HeaderParam(HttpHeaders.CONTENT_TYPE) MediaType type) + @HeaderParam(HttpHeaders.CONTENT_TYPE) MediaType type, InputStream postBody) throws RepositoryException { /* * Handle PUT (Sec. 5.5, Sec. 6.5) @@ -215,13 +218,42 @@ public class LdpWebService { } @PATCH - public Response PATCH(@Context UriInfo uriInfo, InputStream postBody, @HeaderParam(HttpHeaders.CONTENT_TYPE) MediaType type) { + public Response PATCH(@Context UriInfo uriInfo, + @HeaderParam(HttpHeaders.IF_MATCH) EntityTag eTag, + @HeaderParam(HttpHeaders.CONTENT_TYPE) MediaType type, InputStream postBody) throws RepositoryException { + final String resource = getResourceUri(uriInfo); + log.debug("PATCH to <{}>", resource); + + if (!ldpService.exists(resource)) { + return createResponse(Response.Status.NOT_FOUND, uriInfo).build(); + } + + if (eTag != null) { + // check ETag if present + log.trace("Checking If-Match: {}", eTag); + EntityTag hasTag = ldpService.generateETag(resource); + if (!EntityTagUtils.equals(eTag, hasTag)) { + log.trace("If-Match header did not match, expected {}", hasTag); + return createResponse(Response.Status.PRECONDITION_FAILED, uriInfo).build(); + } + } + // Check for the supported mime-type if (!type.toString().equals(APPLICATION_RDF_PATCH)) { - return createResponse(Response.Status.BAD_REQUEST, uriInfo).entity("Unknown Content-Type: " + type + "\n").build(); - }; + log.trace("Incompatible Content-Type for PATCH: {}", type); + return createResponse(Response.Status.UNSUPPORTED_MEDIA_TYPE, uriInfo).entity("Unknown Content-Type: " + type + "\n").build(); + } + + try { + ldpService.patchResource(resource, postBody); + } catch (ParseException | InvalidPatchDocumentException e) { + return createResponse(Response.Status.BAD_REQUEST, uriInfo).entity(e.getMessage() + "\n").build(); + } catch (InvalidModificationException e) { + return createResponse(422, uriInfo).entity(e.getMessage() + "\n").build(); + } + + return createResponse(Response.Status.NO_CONTENT, uriInfo).build(); - return Response.status(Response.Status.NOT_IMPLEMENTED).build(); } @OPTIONS @@ -237,13 +269,13 @@ public class LdpWebService { Response.ResponseBuilder builder = createResponse(Response.Status.OK, uriInfo); // Sec. 5.9.2 - builder.allow("GET", "HEAD", "POST", "OPTIONS"); + builder.allow("GET", "HEAD", "POST", "PATCH", "OPTIONS"); // Sec. 6.4.14 / Sec. 8.1 // builder.header("Accept-Post", "text/turtle, */*"); builder.header("Accept-Post", "text/turtle"); - // TODO: Sec. 5.8.2 + // Sec. 5.8.2 builder.header("Accept-Patch", APPLICATION_RDF_PATCH); http://git-wip-us.apache.org/repos/asf/marmotta/blob/05e03798/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtilTest.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtilTest.java b/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtilTest.java index ba55337..6094968 100644 --- a/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtilTest.java +++ b/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/patch/RdfPatchUtilTest.java @@ -18,6 +18,7 @@ package org.apache.marmotta.platform.ldp.patch; import org.apache.marmotta.commons.vocabulary.FOAF; +import org.apache.marmotta.platform.ldp.patch.parser.ParseException; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -28,6 +29,7 @@ import org.openrdf.model.impl.LiteralImpl; import org.openrdf.model.impl.URIImpl; import org.openrdf.repository.Repository; import org.openrdf.repository.RepositoryConnection; +import org.openrdf.repository.RepositoryException; import org.openrdf.repository.sail.SailRepository; import org.openrdf.rio.RDFFormat; import org.openrdf.sail.memory.MemoryStore; @@ -95,4 +97,21 @@ public class RdfPatchUtilTest { con.close(); } } + + @Test(expected = InvalidPatchDocumentException.class) + public void testInvalidPatchDocumentException() throws RepositoryException, ParseException, InvalidPatchDocumentException { + RepositoryConnection con = repository.getConnection(); + try { + final String invalidPatch = "A <http://example/foo> R <http://example/bar> ."; + + RdfPatchUtil.applyPatch(con, invalidPatch); + + Assert.fail("applyPatch should throw an InvalidPatchDocumentException"); + } catch (final Throwable t) { + con.rollback(); + throw t; + } finally { + con.close(); + } + } } http://git-wip-us.apache.org/repos/asf/marmotta/blob/05e03798/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/webservices/LdpWebServiceTest.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/webservices/LdpWebServiceTest.java b/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/webservices/LdpWebServiceTest.java index 947d594..3e0a3d6 100644 --- a/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/webservices/LdpWebServiceTest.java +++ b/platform/marmotta-ldp/src/test/java/org/apache/marmotta/platform/ldp/webservices/LdpWebServiceTest.java @@ -60,10 +60,8 @@ public class LdpWebServiceTest { } @Test - public void test() { - // Nothing is implemented so far... - Assert.assertEquals(501, RestAssured.get("/ldp").statusCode()); - Assert.assertEquals(501, RestAssured.get("/ldp/foo/bar/and/some/more").statusCode()); + public void testCRUD() { + } @AfterClass
