Repository: marmotta Updated Branches: refs/heads/ldp b5a90a928 -> 0a791f426
MARMOTTA-461: Added initial support for the Prefer header in LDP MARMOTTA-521: test preferContainmentTriples passes MARMOTTA-514: test regression: implementing MARMOTTA-461 causes some tests to fail... Project: http://git-wip-us.apache.org/repos/asf/marmotta/repo Commit: http://git-wip-us.apache.org/repos/asf/marmotta/commit/0a791f42 Tree: http://git-wip-us.apache.org/repos/asf/marmotta/tree/0a791f42 Diff: http://git-wip-us.apache.org/repos/asf/marmotta/diff/0a791f42 Branch: refs/heads/ldp Commit: 0a791f4260f9cfd8ca74265c0ff0d6fa94f3b164 Parents: 87623b1 Author: Jakob Frank <[email protected]> Authored: Tue Sep 16 17:44:22 2014 +0200 Committer: Jakob Frank <[email protected]> Committed: Tue Sep 16 17:45:07 2014 +0200 ---------------------------------------------------------------------- .../apache/marmotta/commons/vocabulary/LDP.java | 18 + .../marmotta/platform/ldp/api/LdpService.java | 4 + .../marmotta/platform/ldp/api/Preference.java | 219 ++++++++++++ .../platform/ldp/services/LdpServiceImpl.java | 53 ++- .../marmotta/platform/ldp/util/LdpUtils.java | 37 +- .../platform/ldp/webservices/LdpWebService.java | 54 ++- .../platform/ldp/webservices/PreferHeader.java | 347 +++++++++++++++++++ 7 files changed, 701 insertions(+), 31 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/marmotta/blob/0a791f42/commons/marmotta-sesame-tools/marmotta-model-vocabs/src/main/java/org/apache/marmotta/commons/vocabulary/LDP.java ---------------------------------------------------------------------- diff --git a/commons/marmotta-sesame-tools/marmotta-model-vocabs/src/main/java/org/apache/marmotta/commons/vocabulary/LDP.java b/commons/marmotta-sesame-tools/marmotta-model-vocabs/src/main/java/org/apache/marmotta/commons/vocabulary/LDP.java index 1293c1b..fb7498f 100644 --- a/commons/marmotta-sesame-tools/marmotta-model-vocabs/src/main/java/org/apache/marmotta/commons/vocabulary/LDP.java +++ b/commons/marmotta-sesame-tools/marmotta-model-vocabs/src/main/java/org/apache/marmotta/commons/vocabulary/LDP.java @@ -221,10 +221,27 @@ public class LDP { * written to automatically exclude those new classes of triples. * * @see <a href="http://www.w3.org/ns/ldp#PreferEmptyContainer">PreferEmptyContainer</a> + * @deprecated use {@link #PreferMinimalContainer} instead */ + @Deprecated public static final URI PreferEmptyContainer; /** + * PreferMinimalContainer + * <p> + * {@code http://www.w3.org/ns/ldp#PreferMinimalContainer}. + * <p> + * URI identifying the subset of a LDPC's triples present in an empty + * LDPC, for example to allow clients to express interest in receiving + * them. Currently this excludes containment and membership triples, but + * in the future other exclusions might be added. This definition is + * written to automatically exclude those new classes of triples. + * + * @see <a href="http://www.w3.org/ns/ldp#PreferMinimalContainer">PreferMinimalContainer</a> + */ + public static final URI PreferMinimalContainer; + + /** * PreferMembership * <p> * {@code http://www.w3.org/ns/ldp#PreferMembership}. @@ -278,6 +295,7 @@ public class LDP { PreferContainment = factory.createURI(LDP.NAMESPACE, "PreferContainment"); PreferEmptyContainer = factory.createURI(LDP.NAMESPACE, "PreferEmptyContainer"); PreferMembership = factory.createURI(LDP.NAMESPACE, "PreferMembership"); + PreferMinimalContainer = factory.createURI(LDP.NAMESPACE, "PreferMinimalContainer"); RDFSource = factory.createURI(LDP.NAMESPACE, "RDFSource"); Resource = factory.createURI(LDP.NAMESPACE, "Resource"); } http://git-wip-us.apache.org/repos/asf/marmotta/blob/0a791f42/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 f2ab2e0..20ee27d 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 @@ -270,6 +270,10 @@ public interface LdpService { void exportResource(RepositoryConnection connection, URI resource, OutputStream output, RDFFormat format) throws RepositoryException, RDFHandlerException; + void exportResource(RepositoryConnection outputConn, String resource, OutputStream output, RDFFormat format, Preference preference) throws RDFHandlerException, RepositoryException; + + void exportResource(RepositoryConnection outputConn, URI resource, OutputStream output, RDFFormat format, Preference preference) throws RepositoryException, RDFHandlerException; + void exportBinaryResource(RepositoryConnection connection, String resource, OutputStream out) throws RepositoryException, IOException; void exportBinaryResource(RepositoryConnection connection, URI resource, OutputStream out) throws RepositoryException, IOException; http://git-wip-us.apache.org/repos/asf/marmotta/blob/0a791f42/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/api/Preference.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/api/Preference.java b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/api/Preference.java new file mode 100644 index 0000000..8a32168 --- /dev/null +++ b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/api/Preference.java @@ -0,0 +1,219 @@ +/* + * 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.api; + +import org.apache.marmotta.commons.vocabulary.LDP; + +/** + * Preferences, which triples to include in a response. + * + * @see <a href="http://www.w3.org/TR/ldp/#prefer-parameters">http://www.w3.org/TR/ldp/#prefer-parameters</a> + * + * @author Jakob Frank + */ +public class Preference { + + private boolean content; + private boolean minimal; + private boolean membership; + private boolean containment; + private boolean minimalContainer; + + private Preference(boolean minimal, boolean elements) { + this.content = !minimal; + this.minimal = minimal; + this.membership = elements; + this.containment = elements; + this.minimalContainer = elements; + } + + /** + * Reflects a {@code Prefer: return="minimal"} header + * @return {@code true} if the minimal representation is preferred by the client + */ + public boolean isMinimal() { + return minimal; + } + + /** + * Should the LDP-RS content be included in the response? + * @return {@code true} if the content triples should be included in the response + */ + public boolean includeContent() { + return content; + } + + /** + * Should membership triples be included in the response? + * + * @return {@code true} if membership triples should be included in the response + * @see <a href="http://www.w3.org/TR/ldp/#dfn-membership-triples">http://www.w3.org/TR/ldp/#dfn-membership-triples</a> + */ + public boolean includeMembership() { + return membership; + } + + /** + * Should containment triples be included in the response? + * + * @return {@code true} if containment triples should be included in the response + * @see <a href="http://www.w3.org/TR/ldp/#dfn-containment-triples">http://www.w3.org/TR/ldp/#dfn-containment-triples</a> + */ + public boolean includeContainment() { + return containment; + } + + /** + * Should minimal container triples be included in the response? + * + * @return {@code true} if minimal container triples should be included in the response + * @see <a href="http://www.w3.org/TR/ldp/#dfn-minimal-container-triples">http://www.w3.org/TR/ldp/#dfn-minimal-container-triples</a> + */ + public boolean includeMinimalContainer() { + return minimalContainer; + } + + /** + * If minimal is set to {@code true}, all other settings will be set to false. + * @param minimal whether minimal representation is preferred. + */ + public void setMinimal(boolean minimal) { + this.minimal = minimal; + if (minimal) { + this.content = this.membership = this.containment = this.minimalContainer = false; + } + } + + /** + * Setting content to {@code true} will also set minimal to {@code false} + * @param content should the LDP-RS content be included in the response? + */ + public void setContent(boolean content) { + this.content = content; + if (content) { + this.minimal = false; + } + } + + /** + * Setting membership to {@code true} will also set minimal to {@code false} + * + * @param membership should membership triples be included in the response? + * @see <a href="http://www.w3.org/TR/ldp/#dfn-membership-triples">http://www.w3.org/TR/ldp/#dfn-membership-triples</a> + */ + public void setMembership(boolean membership) { + this.membership = membership; + if (membership) { + this.minimal = false; + } + } + + /** + * Setting containment to {@code true} will also set minimal to {@code false} + * + * @param containment should containment triples be included in the response? + * @see <a href="http://www.w3.org/TR/ldp/#dfn-containment-triples">http://www.w3.org/TR/ldp/#dfn-containment-triples</a> + */ + public void setContainment(boolean containment) { + this.containment = containment; + if (containment) { + this.minimal = false; + } + } + + /** + * Setting minimal container to {@code true} will also set minimal to {@code false} + * + * @param minimalContainer should minimal container triples be included in the response? + * @see <a href="http://www.w3.org/TR/ldp/#dfn-minimal-container-triples">http://www.w3.org/TR/ldp/#dfn-minimal-container-triples</a> + */ + public void setMinimalContainer(boolean minimalContainer) { + this.minimalContainer = minimalContainer; + if (minimalContainer) { + this.minimal = false; + } + } + + /** + * The default preference: not minimal, all included. + * @return the default preference. + */ + public static Preference defaultPreference() { + return new Preference(false, true); + } + + /** + * The minimal preference: minimal is {@code true}, nothing included. + * @return the minimal preference. + */ + public static Preference minimalPreference() { + return new Preference(true, false); + } + + /** + * Non-minimal preference, with only the args included. + * + * @param includes LDP-Preference URIs to include in the response + * @return a non-minimal preference, with only the provided parts included. + * @see <a href="http://www.w3.org/TR/ldp/#h5_prefer-uris">http://www.w3.org/TR/ldp/#h5_prefer-uris</a> + */ + public static Preference includePreference(String... includes) { + final Preference pref = new Preference(false, false); + pref.content = false; + for (String i: includes) { + if (LDP.PreferContainment.stringValue().equals(i)) { + pref.setContainment(true); + } else if (LDP.PreferMembership.stringValue().equals(i)) { + pref.setMembership(true); + } else if (LDP.PreferMinimalContainer.stringValue().equals(i)) { + pref.setMinimalContainer(true); + } else if (LDP.PreferEmptyContainer.stringValue().equals(i)) { + pref.setMinimalContainer(true); + } else { + // ignore unknown includes + } + } + return pref; + } + + /** + * Non-minimal preference, with all the provided args omitted. + * + * @param omits LDP-Preference URIs to omit in the response + * @return a non-minimal preference, with all provided parts excluded. + * @see <a href="http://www.w3.org/TR/ldp/#h5_prefer-uris">http://www.w3.org/TR/ldp/#h5_prefer-uris</a> + */ + public static Preference omitPreference(String... omits) { + final Preference pref = new Preference(false, true); + pref.content = true; + for (String e: omits) { + if (LDP.PreferContainment.stringValue().equals(e)) { + pref.setContainment(false); + } else if (LDP.PreferMembership.stringValue().equals(e)) { + pref.setMembership(false); + } else if (LDP.PreferMinimalContainer.stringValue().equals(e)) { + pref.setMinimalContainer(false); + } else if (LDP.PreferEmptyContainer.stringValue().equals(e)) { + pref.setMinimalContainer(false); + } else { + // ignore unknown omits + } + } + return pref; + } + +} http://git-wip-us.apache.org/repos/asf/marmotta/blob/0a791f42/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 a336c6b..624ff84 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 @@ -17,15 +17,14 @@ */ package org.apache.marmotta.platform.ldp.services; -import info.aduna.iteration.FilterIteration; -import info.aduna.iteration.Iterations; -import info.aduna.iteration.UnionIteration; +import info.aduna.iteration.*; import org.apache.commons.io.IOUtils; import org.apache.marmotta.commons.vocabulary.DCTERMS; import org.apache.marmotta.commons.vocabulary.LDP; import org.apache.marmotta.platform.core.api.config.ConfigurationService; import org.apache.marmotta.platform.ldp.api.LdpBinaryStoreService; import org.apache.marmotta.platform.ldp.api.LdpService; +import org.apache.marmotta.platform.ldp.api.Preference; import org.apache.marmotta.platform.ldp.exceptions.IncompatibleResourceTypeException; import org.apache.marmotta.platform.ldp.exceptions.InvalidInteractionModelException; import org.apache.marmotta.platform.ldp.exceptions.InvalidModificationException; @@ -252,17 +251,51 @@ public class LdpServiceImpl implements LdpService { @Override public void exportResource(RepositoryConnection connection, URI resource, OutputStream output, RDFFormat format) throws RepositoryException, RDFHandlerException { + exportResource(connection, resource, output, format, null); + } + + @Override + public void exportResource(RepositoryConnection connection, String resource, OutputStream output, RDFFormat format, Preference preference) throws RDFHandlerException, RepositoryException { + exportResource(connection, buildURI(resource), output, format, preference); + } + + @Override + public void exportResource(RepositoryConnection connection, final URI resource, OutputStream output, RDFFormat format, final Preference preference) throws RepositoryException, RDFHandlerException { // TODO: this should be a little more sophisticated... // TODO: non-membership triples flag / Prefer-header - RDFWriter writer = Rio.createWriter(format, output); - UnionIteration<Statement, RepositoryException> union = new UnionIteration<>( - connection.getStatements(resource, null, null, false, ldpContext), - connection.getStatements(null, null, null, false, resource) - ); + final RDFWriter writer = Rio.createWriter(format, output); + final CloseableIteration<Statement, RepositoryException> contentStatements; + if (preference == null || preference.includeContent()) { + contentStatements = connection.getStatements(null, null, null, false, resource); + } else { + contentStatements = new EmptyIteration<>(); + } try { - LdpUtils.exportIteration(writer, resource, union); + CloseableIteration<Statement, RepositoryException> ldpStatements = connection.getStatements(resource, null, null, false, ldpContext); + if (preference != null) { + // FIXME: Get the membership predicate from the container. See http://www.w3.org/TR/ldp/#h5_ldpdc-containtriples + final URI membershipPred = null; + ldpStatements = new FilterIteration<Statement, RepositoryException>(ldpStatements) { + @Override + protected boolean accept(Statement stmt) throws RepositoryException { + final URI p = stmt.getPredicate(); + final Resource s = stmt.getSubject(); + final Value o = stmt.getObject(); + + + if (p.equals(LDP.contains)) return preference.includeContainment(); + if (p.equals(membershipPred)) return preference.includeMembership(); + + return preference.includeMinimalContainer(); + } + }; + } + final CloseableIteration<Statement, RepositoryException> statements = new UnionIteration<>( + ldpStatements, contentStatements + ); + LdpUtils.exportIteration(writer, resource, statements); } finally { - union.close(); + contentStatements.close(); } } http://git-wip-us.apache.org/repos/asf/marmotta/blob/0a791f42/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/util/LdpUtils.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/util/LdpUtils.java b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/util/LdpUtils.java index 9a81086..e316d18 100644 --- a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/util/LdpUtils.java +++ b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/util/LdpUtils.java @@ -21,6 +21,8 @@ import info.aduna.iteration.CloseableIteration; import org.apache.commons.lang3.StringUtils; import org.apache.marmotta.commons.vocabulary.LDP; import org.apache.marmotta.commons.vocabulary.XSD; +import org.apache.marmotta.platform.ldp.api.Preference; +import org.apache.marmotta.platform.ldp.webservices.PreferHeader; import org.apache.tika.mime.MimeTypeException; import org.apache.tika.mime.MimeTypes; import org.openrdf.model.Statement; @@ -36,12 +38,8 @@ import org.openrdf.rio.RDFWriter; import org.slf4j.Logger; import javax.ws.rs.core.MediaType; -import java.io.File; import java.net.MalformedURLException; import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Paths; -import java.util.Iterator; import java.util.Set; /** @@ -157,6 +155,37 @@ public class LdpUtils { return new URIImpl(resource.getNamespace()); } + /** + * Convert a PreferHeader into a LDP Preference. + * @param prefer the PreferHeader to parse + * @return the Preference + */ + public static Preference parsePreferHeader(PreferHeader prefer) { + if (prefer == null) return null; + // we only support "return"-prefers + if (!PreferHeader.PREFERENCE_RETURN.equals(prefer.getPreference())) { + return null; + } + if (PreferHeader.RETURN_MINIMAL.equals(prefer.getPreferenceValue())) { + return Preference.minimalPreference(); + } + + if (PreferHeader.RETURN_REPRESENTATION.equals(prefer.getPreferenceValue())) { + final String include = prefer.getParamValue(PreferHeader.RETURN_PARAM_INCLUDE); + final String omit = prefer.getParamValue(PreferHeader.RETURN_PARAM_OMIT); + if (StringUtils.isNotBlank(include) && StringUtils.isBlank(omit)) { + return Preference.includePreference(include.split("\\s+")); + } else if (StringUtils.isNotBlank(omit) && StringUtils.isBlank(include)) { + return Preference.omitPreference(omit.split("\\s+")); + } else if (StringUtils.isBlank(include) && StringUtils.isBlank(omit)) { + return Preference.defaultPreference(); + } else { + return null; + } + } + return null; + } + private LdpUtils() { // Static access only } http://git-wip-us.apache.org/repos/asf/marmotta/blob/0a791f42/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 08599e0..eb32545 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 @@ -26,6 +26,7 @@ import org.apache.marmotta.platform.core.api.exporter.ExportService; import org.apache.marmotta.platform.core.api.triplestore.SesameService; import org.apache.marmotta.platform.core.events.SesameStartupEvent; import org.apache.marmotta.platform.ldp.api.LdpService; +import org.apache.marmotta.platform.ldp.api.Preference; import org.apache.marmotta.platform.ldp.exceptions.IncompatibleResourceTypeException; import org.apache.marmotta.platform.ldp.exceptions.InvalidInteractionModelException; import org.apache.marmotta.platform.ldp.exceptions.InvalidModificationException; @@ -81,6 +82,8 @@ public class LdpWebService { static final String HTTP_HEADER_SLUG = "Slug"; static final String HTTP_HEADER_ACCEPT_POST = "Accept-Post"; static final String HTTP_HEADER_ACCEPT_PATCH = "Accept-Patch"; + static final String HTTP_HEADER_PREFER = "Prefer"; + static final String HTTP_HEADER_PREFERENCE_APPLIED = "Preference-Applied"; static final String HTTP_METHOD_PATCH = "PATCH"; private Logger log = org.slf4j.LoggerFactory.getLogger(this.getClass()); @@ -149,24 +152,26 @@ public class LdpWebService { } @GET - public Response GET(@Context final UriInfo uriInfo, @Context Request r, - @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.WILDCARD) String type) + public Response GET(@Context final UriInfo uriInfo, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.WILDCARD) String type, + @HeaderParam(HTTP_HEADER_PREFER) PreferHeader preferHeader) throws RepositoryException { final String resource = ldpService.getResourceUri(uriInfo); log.debug("GET to LDPR <{}>", resource); - return buildGetResponse(resource, r, MarmottaHttpUtils.parseAcceptHeader(type)).build(); + return buildGetResponse(resource, MarmottaHttpUtils.parseAcceptHeader(type), preferHeader).build(); } @HEAD - public Response HEAD(@Context final UriInfo uriInfo, @Context Request r, - @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.WILDCARD) String type) + public Response HEAD(@Context final UriInfo uriInfo, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.WILDCARD) String type, + @HeaderParam(HTTP_HEADER_PREFER) PreferHeader preferHeader) throws RepositoryException { final String resource = ldpService.getResourceUri(uriInfo); log.debug("HEAD to LDPR <{}>", resource); - return buildGetResponse(resource, r, MarmottaHttpUtils.parseAcceptHeader(type)).entity(null).build(); + return buildGetResponse(resource, MarmottaHttpUtils.parseAcceptHeader(type), preferHeader).entity(null).build(); } - private Response.ResponseBuilder buildGetResponse(final String resource, Request r, List<ContentType> acceptedContentTypes) throws RepositoryException { + private Response.ResponseBuilder buildGetResponse(final String resource, List<ContentType> acceptedContentTypes, PreferHeader preferHeader) throws RepositoryException { log.trace("LDPR requested media type {}", acceptedContentTypes); final RepositoryConnection conn = sesameService.getConnection(); try { @@ -198,7 +203,7 @@ public class LdpWebService { if (MarmottaHttpUtils.bestContentType(MarmottaHttpUtils.parseAcceptHeader("*/*"), acceptedContentTypes) != null) { log.trace("Unknown type of LDP-NR <{}> is compatible with wildcard - sending back LDP-NR without Content-Type", resource); // Client will accept anything, send back LDP-NR - final Response.ResponseBuilder resp = buildGetResponseBinaryResource(conn, resource); + final Response.ResponseBuilder resp = buildGetResponseBinaryResource(conn, resource, preferHeader); conn.commit(); return resp; } else if (rdfContentType == null) { @@ -210,7 +215,7 @@ public class LdpWebService { return resp; } else { log.debug("Client is asking for a RDF-Serialisation of LDP-NS <{}>, sending meta-data", resource); - final Response.ResponseBuilder resp = buildGetResponseSourceResource(conn, resource, Rio.getWriterFormatForMIMEType(rdfContentType.getMime(), RDFFormat.TURTLE)); + final Response.ResponseBuilder resp = buildGetResponseSourceResource(conn, resource, Rio.getWriterFormatForMIMEType(rdfContentType.getMime(), RDFFormat.TURTLE), preferHeader); conn.commit(); return resp; } @@ -225,12 +230,12 @@ public class LdpWebService { return resp; } else { log.debug("Client is asking for a RDF-Serialisation of LDP-NS <{}>, sending meta-data", resource); - final Response.ResponseBuilder resp = buildGetResponseSourceResource(conn, resource, Rio.getWriterFormatForMIMEType(rdfContentType.getMime(), RDFFormat.TURTLE)); + final Response.ResponseBuilder resp = buildGetResponseSourceResource(conn, resource, Rio.getWriterFormatForMIMEType(rdfContentType.getMime(), RDFFormat.TURTLE), preferHeader); conn.commit(); return resp; } } else { - final Response.ResponseBuilder resp = buildGetResponseBinaryResource(conn, resource); + final Response.ResponseBuilder resp = buildGetResponseBinaryResource(conn, resource, preferHeader); conn.commit(); return resp; } @@ -243,7 +248,7 @@ public class LdpWebService { conn.commit(); return resp; } else { - final Response.ResponseBuilder resp = buildGetResponseSourceResource(conn, resource, Rio.getWriterFormatForMIMEType(bestType.getMime(), RDFFormat.TURTLE)); + final Response.ResponseBuilder resp = buildGetResponseSourceResource(conn, resource, Rio.getWriterFormatForMIMEType(bestType.getMime(), RDFFormat.TURTLE), preferHeader); conn.commit(); return resp; } @@ -267,9 +272,10 @@ public class LdpWebService { return addOptionsHeader(connection, resource, response); } - private Response.ResponseBuilder buildGetResponseBinaryResource(RepositoryConnection connection, final String resource) throws RepositoryException { + private Response.ResponseBuilder buildGetResponseBinaryResource(RepositoryConnection connection, final String resource, PreferHeader preferHeader) throws RepositoryException { final String realType = ldpService.getMimeType(connection, resource); log.debug("Building response for LDP-NR <{}> with format {}", resource, realType); + final Preference preference = LdpUtils.parsePreferHeader(preferHeader); final StreamingOutput entity = new StreamingOutput() { @Override public void write(OutputStream out) throws IOException, WebApplicationException { @@ -291,12 +297,19 @@ public class LdpWebService { } }; // Sec. 4.2.2.2 - return addOptionsHeader(connection, resource, createResponse(connection, Response.Status.OK, resource).entity(entity).type(realType)); + final Response.ResponseBuilder resp = addOptionsHeader(connection, resource, createResponse(connection, Response.Status.OK, resource).entity(entity).type(realType)); + if (preferHeader != null) { + if (preference.isMinimal()) { + resp.status(Response.Status.NO_CONTENT).entity(null).header(HTTP_HEADER_PREFERENCE_APPLIED, PreferHeader.fromPrefer(preferHeader).parameters(null).build()); + } + } + return resp; } - private Response.ResponseBuilder buildGetResponseSourceResource(RepositoryConnection conn, final String resource, final RDFFormat format) throws RepositoryException { + private Response.ResponseBuilder buildGetResponseSourceResource(RepositoryConnection conn, final String resource, final RDFFormat format, final PreferHeader preferHeader) throws RepositoryException { // Deliver all triples from the <subject> context. log.debug("Building response for LDP-RS <{}> with RDF format {}", resource, format.getDefaultMIMEType()); + final Preference preference = LdpUtils.parsePreferHeader(preferHeader); final StreamingOutput entity = new StreamingOutput() { @Override public void write(OutputStream output) throws IOException, WebApplicationException { @@ -304,7 +317,7 @@ public class LdpWebService { final RepositoryConnection outputConn = sesameService.getConnection(); try { outputConn.begin(); - ldpService.exportResource(outputConn, resource, output, format); + ldpService.exportResource(outputConn, resource, output, format, preference); outputConn.commit(); } catch (RDFHandlerException e) { outputConn.rollback(); @@ -321,7 +334,14 @@ public class LdpWebService { } }; // Sec. 4.2.2.2 - return addOptionsHeader(conn, resource, createResponse(conn, Response.Status.OK, resource).entity(entity).type(format.getDefaultMIMEType())); + final Response.ResponseBuilder resp = addOptionsHeader(conn, resource, createResponse(conn, Response.Status.OK, resource).entity(entity).type(format.getDefaultMIMEType())); + if (preference != null) { + if (preference.isMinimal()) { + resp.status(Response.Status.NO_CONTENT).entity(null); + } + resp.header(HTTP_HEADER_PREFERENCE_APPLIED, PreferHeader.fromPrefer(preferHeader).parameters(null).build()); + } + return resp; } /** http://git-wip-us.apache.org/repos/asf/marmotta/blob/0a791f42/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/webservices/PreferHeader.java ---------------------------------------------------------------------- diff --git a/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/webservices/PreferHeader.java b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/webservices/PreferHeader.java new file mode 100644 index 0000000..7406c81 --- /dev/null +++ b/platform/marmotta-ldp/src/main/java/org/apache/marmotta/platform/ldp/webservices/PreferHeader.java @@ -0,0 +1,347 @@ +/* + * 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.webservices; + +import org.apache.commons.lang3.StringUtils; +import org.apache.marmotta.platform.core.exception.InvalidArgumentException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * HTML Prefer Header. + * + * @author Jakob Frank + * @see <a href="http://www.ietf.org/rfc/rfc7240.txt">http://www.ietf.org/rfc/rfc7240.txt</a> + */ +public class PreferHeader { + + public static final String PREFERENCE_RESPOND_ASYNC ="respond-async"; + public static final String PREFERENCE_RETURN = "return"; + public static final String RETURN_REPRESENTATION = "representation"; + public static final String RETURN_MINIMAL = "minimal"; + public static final String PREFERENCE_WAIT = "wait"; + public static final String PREFERENCE_HANDLING = "handling"; + public static final String HANDLING_STRICT = "strict"; + public static final String HANDLING_LENIENT = "lenient"; + + public static final String RETURN_PARAM_INCLUDE = "include"; + public static final String RETURN_PARAM_OMIT = "omit"; + + public static Logger log = LoggerFactory.getLogger(PreferHeader.class); + + private String preference, preferenceValue; + + private Map<String, String> params; + + private PreferHeader(String preference) { + this.preference = preference; + this.params = new LinkedHashMap<>(); + } + + /** + * Get the preference, + * e.g. {@code foo} from {@code Prefer: foo="bar"} + * + * @return the preference of the prefer-header + */ + public String getPreference() { + return preference; + } + + /** + * Get the value of the preference, + * e.g. {@code bar} from {@code Prefer: foo="bar"}. + * @return the preference value of the prefer-header + */ + public String getPreferenceValue() { + return preferenceValue; + } + + /** + * Get the parameters of the prefer-header. + * @return the prefer-parameters + */ + public Map<String, String> getParams() { + return Collections.unmodifiableMap(params); + } + + /** + * Get the parameter value of the prefer-header, + * e,g, {@code val2} from {@code Prefer: foo="bar"; a1="val1"; a2="val2"} for {@code header.getParamValue("a2")}. + * @param param the param to get the value of + * @return the value of the requested parameter, or {@code null} + */ + public String getParamValue(String param) { + return params.get(param); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(preference); + if (StringUtils.isNotBlank(preferenceValue)) { + sb.append("=\"").append(preferenceValue).append("\""); + } + for (String param: params.keySet()) { + sb.append("; ").append(param); + final String value = params.get(param); + if (StringUtils.isNotBlank(value)) { + sb.append("=\"").append(value).append("\""); + } + } + + return sb.toString(); + } + + /** + * Parse a PreferHeader. + * @param headerValue the header value to parse + * @return the parsed PreferHeader + */ + public static PreferHeader valueOf(String headerValue) { + if (StringUtils.isBlank(headerValue)) { + log.error("Empty Prefer-Header - what should I do now?"); + throw new InvalidArgumentException(); + } + + String pref = null, val = null; + final Map<String, String> params = new LinkedHashMap<>(); + final String[] parts = headerValue.split("\\s*;\\s"); + for (int i = 0; i < parts.length; i++) { + final String part = parts[i]; + final String[] kv = part.split("\\s*=\\s*", 2); + if (i == 0) { + pref = StringUtils.trimToNull(kv[0]); + if (kv.length > 1) { + val = StringUtils.trimToNull(StringUtils.removeStart(StringUtils.removeEnd(kv[1], "\""), "\"")); + } + } else { + String p, pval = null; + p = StringUtils.trimToNull(kv[0]); + if (kv.length > 1) { + pval = StringUtils.trimToNull(StringUtils.removeStart(StringUtils.removeEnd(kv[1], "\""), "\"")); + } + params.put(p, pval); + } + } + + final PreferHeader header = new PreferHeader(pref); + header.preferenceValue = val; + header.params = params; + + return header; + } + + /** + * Create and initialize a PreferBuilder for a {@code Prefer: respond-async} header. + * @return initialized PreferBuilder + */ + public static PreferBuilder preferRespondAsync() { + return new PreferBuilder(PREFERENCE_RESPOND_ASYNC); + } + + /** + * Create and initialize a PreferBuilder for a {@code Prefer: return="representation"} header. + * @return initialized PreferBuilder + */ + public static PreferBuilder preferReturnRepresentation() { + return new PreferBuilder(PREFERENCE_RETURN, RETURN_REPRESENTATION); + } + + /** + * Create and initialize a PreferBuilder for a {@code Prefer: return="minimal"} header. + * @return initialized PreferBuilder + */ + public static PreferBuilder preferReturnMinimal() { + return new PreferBuilder(PREFERENCE_RETURN, RETURN_MINIMAL); + } + + /** + * Create and initialize a PreferBuilder for a {@code Prefer: wait="X"} header. + * @param seconds seconds to wait, the <em>X</em> in the example. + * @return initialized PreferBuilder + */ + public static PreferBuilder preferWait(int seconds) { + return new PreferBuilder(PREFERENCE_WAIT, String.valueOf(seconds)); + } + + /** + * Create and initialize a PreferBuilder for a {@code Prefer: handling="strict"} header. + * @return initialized PreferBuilder + */ + public static PreferBuilder preferHandlingStrict() { + return new PreferBuilder(PREFERENCE_HANDLING, HANDLING_STRICT); + } + + /** + * Create and initialize a PreferBuilder for a {@code Prefer: handling="lenient"} header. + * @return initialized PreferBuilder + */ + public static PreferBuilder preferHandlingLenient() { + return new PreferBuilder(PREFERENCE_HANDLING, HANDLING_LENIENT); + } + + /** + * Create a PreferBuilder initialized with the provided PreferHeader. + * @param prefer the PreferHeader used for initialisation + * @return initialized PreferBuilder + */ + public static PreferBuilder fromPrefer(PreferHeader prefer) { + final PreferBuilder builder = new PreferBuilder(prefer.preference, prefer.preferenceValue); + builder.params.putAll(prefer.params); + return builder; + } + + /** + * Create a PreferBuilder for an arbitrary preference + * @param preference the preference + * @return initialized PreferBuilder + */ + public static PreferBuilder prefer(String preference) { + return prefer(preference, null); + } + + /** + * Create a PreferBuilder for an arbitrary preference + * @param preference the preference + * @param value the value of the preference + * @return initialized PreferBuilder + */ + private static PreferBuilder prefer(String preference, String value) { + return new PreferBuilder(preference, value); + } + + /** + * Builder for PreferHeader + */ + public static class PreferBuilder { + + private String preference; + private String preferenceValue; + private Map<String, String> params; + + private PreferBuilder(String preference) { + this(preference, null); + } + + private PreferBuilder(String preference, String preferenceValue) { + this.preference = preference; + this.preferenceValue = preferenceValue; + this.params = new HashMap<>(); + } + + private PreferBuilder preference(String preference) { + return preference(preference, null); + } + + private PreferBuilder preference(String preference, String value) { + this.preference = preference; + this.preferenceValue = value; + return this; + } + + /** + * Add a parameter (without value) + * @param parameter the parameter to add + * @return the PreferBuilder for chaining + */ + public PreferBuilder parameter(String parameter) { + this.params.put(parameter, null); + return this; + } + + /** + * Add a parameter with the provided value. If the value is {@code null}, the parameter is removed. + * @param parameter the parameter to add (or remove) + * @param value the parameter value (or {@code null} to remove + * @return the PreferBuilder for chaining + */ + public PreferBuilder parameter(String parameter, String value) { + if (value == null) { + this.params.remove(parameter); + } else { + this.params.put(parameter, value); + } + return this; + } + + /** + * Add the provided parameters and their values. If the argument is {@code null}, all parameters will be removed. + * @param params the parameters to add (or {@code null} to remove all parameters) + * @return the PreferBuilder for chaining + */ + public PreferBuilder parameters(Map<String, String> params) { + if (params == null) { + this.params.clear(); + } else { + this.params.putAll(params); + } + return this; + } + + /** + * <strong>LDP specific:</strong> Add a "include" parameter for the given URIs. + * @param ldpPreferUri the URIs to "include" + * @return the PreferBuilder for chaining + */ + public PreferBuilder include(String... ldpPreferUri) { + return _ldp(RETURN_PARAM_INCLUDE, ldpPreferUri); + } + + /** + * <strong>LDP specific:</strong> Add a "omit" parameter for the given URIs. + * @param ldpPreferUri the URIs to "omit" + * @return the PreferBuilder for chaining + */ + public PreferBuilder omit(String... ldpPreferUri) { + return _ldp(RETURN_PARAM_OMIT, ldpPreferUri); + } + + private PreferBuilder _ldp(String param, String... values) { + if (values == null) { + this.params.remove(param); + } else { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + sb.append(" "); + } + sb.append(values[i]); + } + this.params.put(param, sb.toString()); + } + return this; + } + + /** + * Create the PreferHeader. The builder can be reused. + * @return the PreferHeader + */ + public PreferHeader build() { + final PreferHeader header = new PreferHeader(this.preference); + header.preferenceValue = preferenceValue; + header.params.putAll(params); + return header; + + } + + } +}
