MARMOTT-514: Only one test is now failing, and that one is only a SHOULD! This solves MARMOTTA-515, MARMOTTA-516, MARMOTTA-517, MARMOTTA-518, MARMOTTA-519, MARMOTTA-520
Project: http://git-wip-us.apache.org/repos/asf/marmotta/repo Commit: http://git-wip-us.apache.org/repos/asf/marmotta/commit/30227f31 Tree: http://git-wip-us.apache.org/repos/asf/marmotta/tree/30227f31 Diff: http://git-wip-us.apache.org/repos/asf/marmotta/diff/30227f31 Branch: refs/heads/develop Commit: 30227f31c12babe6716bc3e43df68b3aba53a20a Parents: 13d8e64 Author: Jakob Frank <[email protected]> Authored: Fri Sep 5 09:55:41 2014 +0200 Committer: Jakob Frank <[email protected]> Committed: Fri Sep 5 10:22:48 2014 +0200 ---------------------------------------------------------------------- .../commons/http/MarmottaHttpUtils.java | 2 + .../platform/ldp/services/LdpServiceImpl.java | 14 +- .../marmotta/platform/ldp/util/LdpUtils.java | 24 +- .../platform/ldp/webservices/LdpWebService.java | 371 +++++++++++++------ 4 files changed, 282 insertions(+), 129 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/marmotta/blob/30227f31/commons/marmotta-commons/src/main/java/org/apache/marmotta/commons/http/MarmottaHttpUtils.java ---------------------------------------------------------------------- diff --git a/commons/marmotta-commons/src/main/java/org/apache/marmotta/commons/http/MarmottaHttpUtils.java b/commons/marmotta-commons/src/main/java/org/apache/marmotta/commons/http/MarmottaHttpUtils.java index 568829d..d1e070c 100644 --- a/commons/marmotta-commons/src/main/java/org/apache/marmotta/commons/http/MarmottaHttpUtils.java +++ b/commons/marmotta-commons/src/main/java/org/apache/marmotta/commons/http/MarmottaHttpUtils.java @@ -105,6 +105,8 @@ public class MarmottaHttpUtils { public static ContentType parseContentType(String c) { + if (StringUtils.isBlank(c)) return null; + String mt[] = c.split(";"); String[] tst = mt[0].split("/"); http://git-wip-us.apache.org/repos/asf/marmotta/blob/30227f31/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 c592dea..cdb48cc 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 @@ -108,7 +108,7 @@ public class LdpServiceImpl implements LdpService { uriBuilder.path(uriInfo.getPathParameters().getFirst("local")); // uriBuilder.path(uriInfo.getPath().replaceFirst("/$", "")); String uri = uriBuilder.build().toString(); - log.debug("Request URI: {}", uri); + log.debug("=== Request URI: {}", uri); return uri; } @@ -197,7 +197,7 @@ public class LdpServiceImpl implements LdpService { } else { return null; } - }finally { + } finally { it.close(); } } @@ -228,7 +228,7 @@ public class LdpServiceImpl implements LdpService { } else { return null; } - }finally { + } finally { it.close(); } } @@ -323,6 +323,7 @@ public class LdpServiceImpl implements LdpService { connection.add(container, DCTERMS.modified, now, ldpContext); connection.add(resource, RDF.TYPE, LDP.Resource, ldpContext); + connection.add(resource, RDF.TYPE, LDP.RDFSource, ldpContext); connection.add(resource, ldpInteractionModelProperty, interactionModel.getUri(), ldpContext); connection.add(resource, DCTERMS.created, now, ldpContext); connection.add(resource, DCTERMS.modified, now, ldpContext); @@ -331,9 +332,11 @@ public class LdpServiceImpl implements LdpService { // TODO: find a better way to ingest n-triples (text/plain) while still supporting regular text files final RDFFormat rdfFormat = ("text/plain".equals(type) ? null : Rio.getParserFormatForMIMEType(type)); if (rdfFormat == null) { - log.debug("POST creates new LDP-NR, because no suitable RDF parser found for type {}", type); + log.debug("Creating new LDP-NR, because no suitable RDF parser found for type {}", type); final Literal format = valueFactory.createLiteral(type); final URI binaryResource = valueFactory.createURI(resource.stringValue() + LdpUtils.getExtension(type)); + log.debug("LDP-NR is <{}>", binaryResource); + log.debug("Corresponding LDP-RS is <{}>", resource); connection.add(container, LDP.contains, binaryResource, ldpContext); @@ -354,8 +357,7 @@ public class LdpServiceImpl implements LdpService { return binaryResource.stringValue(); } else { - log.debug("POST creates new LDP-RS, data provided as {}", rdfFormat.getName()); - connection.add(resource, RDF.TYPE, LDP.RDFSource, ldpContext); + log.debug("Creating new LDP-RS, data provided as {}", rdfFormat.getName()); connection.add(container, LDP.contains, resource, ldpContext); // FIXME: We are (are we?) allowed to filter out server-managed properties here http://git-wip-us.apache.org/repos/asf/marmotta/blob/30227f31/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 4ec9fe7..9a81086 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 @@ -33,6 +33,7 @@ import org.openrdf.rio.RDFFormat; import org.openrdf.rio.RDFHandlerException; import org.openrdf.rio.RDFParserRegistry; import org.openrdf.rio.RDFWriter; +import org.slf4j.Logger; import javax.ws.rs.core.MediaType; import java.io.File; @@ -48,6 +49,8 @@ import java.util.Set; */ public class LdpUtils { + private static Logger log = org.slf4j.LoggerFactory.getLogger(LdpUtils.class); + /** * Urify the Slug: header value, i.e. replace all non-url chars with a single dash. * @@ -78,12 +81,19 @@ public class LdpUtils { * @return file extension (already including '.') */ public static String getExtension(String mimeType) { - MimeTypes allTypes = MimeTypes.getDefaultMimeTypes(); + final String defaultExt = ".bin"; + final MimeTypes allTypes = MimeTypes.getDefaultMimeTypes(); try { - return allTypes.forName(mimeType).getExtension(); + final String ext = allTypes.forName(mimeType).getExtension(); + log.trace("Tika's file-extension for {} is '{}'", mimeType, ext); + if (StringUtils.isNotBlank(ext)) { + return ext; + } } catch (MimeTypeException e) { - return null; //FIXME + log.trace("MimeTypeException: {}. Not critical, recovering...", e.getMessage()); } + log.trace("Using fallback file-extension '{}' for {}", defaultExt, mimeType); + return defaultExt; } /** @@ -137,14 +147,18 @@ public class LdpUtils { return sb.toString(); } - public static URI getContainer(String resource) throws MalformedURLException, URISyntaxException { + public static String getContainer(String resource) throws MalformedURLException, URISyntaxException { java.net.URI uri = new java.net.URI(resource); java.net.URI parent = uri.getPath().endsWith("/") ? uri.resolve("..") : uri.resolve("."); - return new URIImpl(parent.toASCIIString()); + return parent.toASCIIString(); } public static URI getContainer(URI resource) throws MalformedURLException, URISyntaxException { return new URIImpl(resource.getNamespace()); } + private LdpUtils() { + // Static access only + } + } http://git-wip-us.apache.org/repos/asf/marmotta/blob/30227f31/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 7a8e772..29c9ca9 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 @@ -43,7 +43,6 @@ import org.openrdf.repository.RepositoryException; import org.openrdf.rio.*; import org.slf4j.Logger; -import javax.annotation.PostConstruct; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; import javax.inject.Inject; @@ -53,6 +52,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.UUID; @@ -85,6 +86,39 @@ public class LdpWebService { @Inject private SesameService sesameService; + private final List<ContentType> producedRdfTypes; + + public LdpWebService() { + producedRdfTypes = new ArrayList<>(); + + for(RDFFormat format : RDFWriterRegistry.getInstance().getKeys()) { + final String primaryQ; + if (format == RDFFormat.TURTLE) { + primaryQ = ";q=1.0"; + } else if (format == RDFFormat.JSONLD) { + primaryQ = ";q=0.9"; + } else if (format == RDFFormat.RDFXML) { + primaryQ = ";q=0.8"; + } else { + primaryQ = ";q=0.5"; + } + final String secondaryQ = ";q=0.3"; + final List<String> mimeTypes = format.getMIMETypes(); + for (int i = 0; i < mimeTypes.size(); i++) { + final String mime = mimeTypes.get(i); + if (i == 0) { + // first mimetype is the default + producedRdfTypes.add(MarmottaHttpUtils.parseContentType(mime + primaryQ)); + } else { + producedRdfTypes.add(MarmottaHttpUtils.parseContentType(mime + secondaryQ)); + } + } + } + Collections.sort(producedRdfTypes); + + log.debug("Available RDF Serializer: {}", producedRdfTypes); + } + protected void initialize(@Observes SesameStartupEvent event) { log.info("Starting up LDP WebService Endpoint"); String root = UriBuilder.fromUri(configurationService.getBaseUri()).path(LdpWebService.PATH).build().toASCIIString(); @@ -105,23 +139,25 @@ public class LdpWebService { @GET public Response GET(@Context final UriInfo uriInfo, @Context Request r, - @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.WILDCARD) MediaType type) + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.WILDCARD) String type) throws RepositoryException { final String resource = ldpService.getResourceUri(uriInfo); log.debug("GET to LDPR <{}>", resource); - return buildGetResponse(resource, r, type).build(); + return buildGetResponse(resource, r, MarmottaHttpUtils.parseAcceptHeader(type)).build(); } @HEAD public Response HEAD(@Context final UriInfo uriInfo, @Context Request r, - @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.WILDCARD) MediaType type) + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.WILDCARD) String type) throws RepositoryException { final String resource = ldpService.getResourceUri(uriInfo); log.debug("HEAD to LDPR <{}>", resource); - return buildGetResponse(resource, r, type).entity(null).build(); + return buildGetResponse(resource, r, MarmottaHttpUtils.parseAcceptHeader(type)).entity(null).build(); } - private Response.ResponseBuilder buildGetResponse(final String resource, Request r, MediaType type) throws RepositoryException { + private Response.ResponseBuilder buildGetResponse(final String resource, Request r, List<ContentType> acceptedContentTypes) throws RepositoryException { + log.trace("LDPR requested media type {}", acceptedContentTypes); + MediaType type = MediaType.valueOf(acceptedContentTypes.get(0).toString()); final RepositoryConnection conn = sesameService.getConnection(); try { conn.begin(); @@ -136,75 +172,59 @@ public class LdpWebService { log.trace("{} exists, continuing", resource); } - final RDFFormat format; - final RDFFormat fallback = (ldpService.isNonRdfSourceResource(conn, resource) ? null : RDFFormat.TURTLE); - final String mimeType = LdpUtils.getMimeType(type); - if (StringUtils.isBlank(mimeType) || "text/plain".equals(mimeType)) { - // TODO: find a better way to support n-triples (text/plain) - // while still supporting regular text files - format = null; - } else if (type.isWildcardSubtype() && "text".equals(type.getType())) { - format = RDFFormat.TURTLE; - } else { - ContentType contentType = MarmottaHttpUtils.performContentNegotiation(mimeType, exportService.getProducedTypes()); - format = (contentType != null ? Rio.getWriterFormatForMIMEType(contentType.getMime(), fallback) : fallback); - } - - if (format == null) { - log.debug("GET to <{}> with non-RDF format {} of a LDP-NR", resource, type); - final StreamingOutput entity = new StreamingOutput() { - @Override - public void write(OutputStream out) throws IOException, WebApplicationException { - try { - final RepositoryConnection outputConn = sesameService.getConnection(); - try { - outputConn.begin(); - ldpService.exportBinaryResource(outputConn, resource, out); - outputConn.commit(); - } catch (RepositoryException | IOException e) { - outputConn.rollback(); - throw new WebApplicationException(e, createResponse(Response.status(Response.Status.INTERNAL_SERVER_ERROR)).entity(e).build()); - } finally { - outputConn.close(); - } - } catch (RepositoryException e) { - throw new WebApplicationException(e, createResponse(Response.status(Response.Status.INTERNAL_SERVER_ERROR)).entity(e).build()); + // Content-Neg + if (ldpService.isNonRdfSourceResource(conn, resource)) { + log.trace("<{}> is marked as LDP-NR", resource); + // LDP-NR + final ContentType realType = MarmottaHttpUtils.parseContentType(ldpService.getMimeType(conn, resource)); + if (realType == null) { + // Fallback to related LDP-RS + log.trace("<{}> does not look like a LDP-NR (no format found), trying the corresponding LDP-RS", resource); + final URI ldpRS = ldpService.getRdfSourceForNonRdfSource(conn, resource); + if (ldpRS == null) { + log.debug("No corresponding LDP-RS found for <{}>, sending 406", resource); + final Response.ResponseBuilder resp = build406Response(conn, resource, Collections.<ContentType>emptyList()); + conn.commit(); + return resp; + } else { + // Sending back the corresponding LDP-RS + log.trace("Using corresponding LDP-RS <{}> for LDP-NR <{}>", ldpRS, resource); + final ContentType bestType = MarmottaHttpUtils.bestContentType(producedRdfTypes, acceptedContentTypes); + if (bestType == null) { + log.trace("Available formats {} do not match any of the requested formats {} for <{}>, sending 406", producedRdfTypes, acceptedContentTypes, resource); + final Response.ResponseBuilder resp = build406Response(conn, resource, producedRdfTypes); + conn.commit(); + return resp; + } else { + log.trace("Sending corresponding LDP-RS <{}> for requested LDP-NR <{}> using {}", ldpRS, resource, bestType); + final Response.ResponseBuilder resp = buildGetResponseSourceResource(conn, ldpRS.stringValue(), Rio.getWriterFormatForMIMEType(bestType.getMime(), RDFFormat.TURTLE)); + conn.commit(); + return resp; } } - }; - final String realType = ldpService.getMimeType(conn, resource); - final Response.ResponseBuilder resp = createResponse(conn, Response.Status.OK, resource).entity(entity).type(realType != null ? MediaType.valueOf(realType) : type); - conn.commit(); - return resp; + } else if (MarmottaHttpUtils.bestContentType(Collections.singletonList(realType), acceptedContentTypes) == null) { + log.debug("Can't send <{}> ({}) in any of the accepted formats: {}, sending 406"); + final Response.ResponseBuilder resp = build406Response(conn, resource, Collections.singletonList(realType)); + conn.commit(); + return resp; + } else { + final Response.ResponseBuilder resp = buildGetResponseBinaryResource(conn, resource); + conn.commit(); + return resp; + } } else { - // Deliver all triples from the <subject> context. - log.debug("GET to <{}> with RDF format {} of a LPD-R", resource, format.getDefaultMIMEType()); - final StreamingOutput entity = new StreamingOutput() { - @Override - public void write(OutputStream output) throws IOException, WebApplicationException { - try { - final RepositoryConnection outputConn = sesameService.getConnection(); - try { - outputConn.begin(); - ldpService.exportResource(outputConn, resource, output, format); - outputConn.commit(); - } catch (RDFHandlerException e) { - outputConn.rollback(); - throw new NoLogWebApplicationException(e, createResponse(Response.status(Response.Status.INTERNAL_SERVER_ERROR)).entity(e.getMessage()).build()); - } catch (final Throwable t) { - outputConn.rollback(); - throw t; - } finally { - outputConn.close(); - } - } catch (RepositoryException e) { - throw new WebApplicationException(e, createResponse(Response.status(Response.Status.INTERNAL_SERVER_ERROR)).entity(e).build()); - } - } - }; - final Response.ResponseBuilder resp = createResponse(conn, Response.Status.OK, resource).entity(entity).type(format.getDefaultMIMEType()); - conn.commit(); - return resp; + // Requested Resource is a LDP-RS + final ContentType bestType = MarmottaHttpUtils.bestContentType(producedRdfTypes, acceptedContentTypes); + if (bestType == null) { + log.trace("Available formats {} do not match any of the requested formats {} for <{}>, sending 406", producedRdfTypes, acceptedContentTypes, resource); + final Response.ResponseBuilder resp = build406Response(conn, resource, producedRdfTypes); + conn.commit(); + return resp; + } else { + final Response.ResponseBuilder resp = buildGetResponseSourceResource(conn, resource, Rio.getWriterFormatForMIMEType(bestType.getMime(), RDFFormat.TURTLE)); + conn.commit(); + return resp; + } } } catch (final Throwable t) { conn.rollback(); @@ -214,6 +234,74 @@ public class LdpWebService { } } + private Response.ResponseBuilder build406Response(RepositoryConnection connection, String resource, List<ContentType> availableContentTypes) throws RepositoryException { + final Response.ResponseBuilder response = createResponse(connection, Response.Status.NOT_ACCEPTABLE, resource); + if (availableContentTypes.isEmpty()) { + response.entity(String.format("%s is not available in the requested format%n", resource)); + } else { + response.entity(String.format("%s is only available in the following formats: %s%n", resource, availableContentTypes)); + } + // Sec. 4.2.2.2 + return addOptionsHeader(connection, resource, response); + } + + private Response.ResponseBuilder buildGetResponseBinaryResource(RepositoryConnection connection, final String resource) throws RepositoryException { + final String realType = ldpService.getMimeType(connection, resource); + log.debug("Building response for LDP-NR <{}> with format {}", resource, realType); + final StreamingOutput entity = new StreamingOutput() { + @Override + public void write(OutputStream out) throws IOException, WebApplicationException { + try { + final RepositoryConnection outputConn = sesameService.getConnection(); + try { + outputConn.begin(); + ldpService.exportBinaryResource(outputConn, resource, out); + outputConn.commit(); + } catch (RepositoryException | IOException e) { + outputConn.rollback(); + throw new WebApplicationException(e, createResponse(Response.status(Response.Status.INTERNAL_SERVER_ERROR)).entity(e).build()); + } finally { + outputConn.close(); + } + } catch (RepositoryException e) { + throw new WebApplicationException(e, createResponse(Response.status(Response.Status.INTERNAL_SERVER_ERROR)).entity(e).build()); + } + } + }; + // Sec. 4.2.2.2 + return addOptionsHeader(connection, resource, createResponse(connection, Response.Status.OK, resource).entity(entity).type(realType)); + } + + private Response.ResponseBuilder buildGetResponseSourceResource(RepositoryConnection conn, final String resource, final RDFFormat format) throws RepositoryException { + // Deliver all triples from the <subject> context. + log.debug("Building response for LDP-RS <{}> with RDF format {}", resource, format.getDefaultMIMEType()); + final StreamingOutput entity = new StreamingOutput() { + @Override + public void write(OutputStream output) throws IOException, WebApplicationException { + try { + final RepositoryConnection outputConn = sesameService.getConnection(); + try { + outputConn.begin(); + ldpService.exportResource(outputConn, resource, output, format); + outputConn.commit(); + } catch (RDFHandlerException e) { + outputConn.rollback(); + throw new NoLogWebApplicationException(e, createResponse(Response.status(Response.Status.INTERNAL_SERVER_ERROR)).entity(e.getMessage()).build()); + } catch (final Throwable t) { + outputConn.rollback(); + throw t; + } finally { + outputConn.close(); + } + } catch (RepositoryException e) { + throw new WebApplicationException(e, createResponse(Response.status(Response.Status.INTERNAL_SERVER_ERROR)).entity(e).build()); + } + } + }; + // Sec. 4.2.2.2 + return addOptionsHeader(conn, resource, createResponse(conn, Response.Status.OK, resource).entity(entity).type(format.getDefaultMIMEType())); + } + /** * LDP Post Request * @@ -222,7 +310,7 @@ public class LdpWebService { */ @POST public Response POST(@Context UriInfo uriInfo, @HeaderParam("Slug") String slug, - @HeaderParam("Link") List<Link> linkHeaders, + @HeaderParam(HttpHeaders.LINK) List<Link> linkHeaders, InputStream postBody, @HeaderParam(HttpHeaders.CONTENT_TYPE) MediaType type) throws RepositoryException { @@ -281,25 +369,8 @@ public class LdpWebService { } log.debug("POST to <{}> will create new LDP-R <{}>", container, newResource); - final String mimeType = LdpUtils.getMimeType(type); - //checking if resource (container) exists is done later in the service - try { - String location = ldpService.addResource(conn, container, newResource, ldpInteractionModel, mimeType, postBody); - final Response.ResponseBuilder response = createResponse(conn, Response.Status.CREATED, container).location(java.net.URI.create(location)); - if (newResource.compareTo(location) != 0) { - response.link(newResource, "describedby"); //FIXME: Sec. 5.2.3.12, see also http://www.w3.org/2012/ldp/track/issues/15 - } - conn.commit(); - return response.build(); - } catch (IOException | RDFParseException e) { - final Response.ResponseBuilder resp = createResponse(conn, Response.Status.BAD_REQUEST, container).entity(e.getClass().getSimpleName() + ": " + e.getMessage()); - conn.rollback(); - return resp.build(); - } catch (UnsupportedRDFormatException e) { - final Response.ResponseBuilder resp = createResponse(conn, Response.Status.UNSUPPORTED_MEDIA_TYPE, container).entity(e); - conn.rollback(); - return resp.build(); - } + // connection is closed by buildPostResponse + return buildPostResponse(conn, container, newResource, ldpInteractionModel, postBody, type); } catch (InvalidInteractionModelException e) { log.debug("POST with invalid interaction model <{}> to <{}>", e.getHref(), container); final Response.ResponseBuilder response = createResponse(conn, Response.Status.BAD_REQUEST, container); @@ -314,10 +385,37 @@ public class LdpWebService { } /** + * @param connection the RepositoryConnection (with active transaction) to read extra data from. WILL BE COMMITTED OR ROLLBACKED + * @throws RepositoryException + */ + private Response buildPostResponse(RepositoryConnection connection, String container, String newResource, LdpService.InteractionModel interactionModel, InputStream requestBody, MediaType type) throws RepositoryException { + final String mimeType = LdpUtils.getMimeType(type); + //checking if resource (container) exists is done later in the service + try { + String location = ldpService.addResource(connection, container, newResource, interactionModel, mimeType, requestBody); + final Response.ResponseBuilder response = createResponse(connection, Response.Status.CREATED, container).location(java.net.URI.create(location)); + if (newResource.compareTo(location) != 0) { + response.link(newResource, "describedby"); //FIXME: Sec. 5.2.3.12, see also http://www.w3.org/2012/ldp/track/issues/15 + } + connection.commit(); + return response.build(); + } catch (IOException | RDFParseException e) { + final Response.ResponseBuilder resp = createResponse(connection, Response.Status.BAD_REQUEST, container).entity(e.getClass().getSimpleName() + ": " + e.getMessage()); + connection.rollback(); + return resp.build(); + } catch (UnsupportedRDFormatException e) { + final Response.ResponseBuilder resp = createResponse(connection, Response.Status.UNSUPPORTED_MEDIA_TYPE, container).entity(e); + connection.rollback(); + return resp.build(); + } + } + + /** * Handle PUT (Sec. 4.2.4, Sec. 5.2.4) */ @PUT public Response PUT(@Context UriInfo uriInfo, @Context Request request, + @HeaderParam(HttpHeaders.LINK) List<Link> linkHeaders, @HeaderParam(HttpHeaders.IF_MATCH) EntityTag eTag, @HeaderParam(HttpHeaders.CONTENT_TYPE) MediaType type, InputStream postBody) throws RepositoryException, IOException, InvalidModificationException, RDFParseException, IncompatibleResourceTypeException, URISyntaxException { @@ -332,7 +430,7 @@ public class LdpWebService { final Response.ResponseBuilder resp; final String newResource; // NOTE: newResource == resource for now, this might change in the future if (ldpService.exists(conn, resource)) { - log.debug("updating resource <{}>", resource); + log.debug("<{}> exists, so this is an UPDATE", resource); if (eTag == null) { // check for If-Match header (ETag) -> 428 Precondition Required (Sec. 4.2.4.5) @@ -355,17 +453,42 @@ public class LdpWebService { newResource = ldpService.updateResource(conn, resource, postBody, mimeType); log.info("PUT update for <{}> successful", newResource); resp = createResponse(conn, Response.Status.OK, resource); + conn.commit(); + return resp.build(); } else { - log.debug("creating resource <{}>", resource); + log.debug("<{}> does not exist, so this is a CREATE", resource); //LDP servers may allow resource creation using PUT (Sec. 4.2.4.6) + + final String container = LdpUtils.getContainer(resource); + try { + // Check that the target container supports the LDPC Interaction Model + final LdpService.InteractionModel containerModel = ldpService.getInteractionModel(conn, container); + if (containerModel != LdpService.InteractionModel.LDPC) { + final Response.ResponseBuilder response = createResponse(conn, Response.Status.METHOD_NOT_ALLOWED, container); + conn.commit(); + return response.entity(String.format("%s only supports %s Interaction Model", container, containerModel)).build(); + } + + // Get the LDP-Interaction Model (Sec. 5.2.3.4 and Sec. 4.2.1.4) + final LdpService.InteractionModel ldpInteractionModel = ldpService.getInteractionModel(linkHeaders); + + // connection is closed by buildPostResponse + return buildPostResponse(conn, container, resource, ldpInteractionModel, postBody, type); + } catch (InvalidInteractionModelException e) { + log.debug("PUT with invalid interaction model <{}> to <{}>", e.getHref(), container); + final Response.ResponseBuilder response = createResponse(conn, Response.Status.BAD_REQUEST, container); + conn.commit(); + return response.entity(e.getMessage()).build(); + } + /* URI uri = conn.getValueFactory().createURI(resource); newResource = ldpService.addResource(conn, LdpUtils.getContainer(uri), uri, LdpService.InteractionModel.LDPR, mimeType, postBody); log.info("PUT on <{}> created new resource", newResource); resp = createResponse(conn, Response.Status.CREATED, newResource).location(java.net.URI.create(newResource)); + conn.commit(); + return resp.build(); + */ } - conn.commit(); - - return resp.build(); } catch (IOException | RDFParseException e) { final Response.ResponseBuilder resp = createResponse(conn, Response.Status.BAD_REQUEST, resource).entity(e.getClass().getSimpleName() + ": " + e.getMessage()); conn.rollback(); @@ -477,6 +600,7 @@ public class LdpWebService { * Handle OPTIONS (Sec. 4.2.8, Sec. 5.2.8) */ @OPTIONS + @Produces("text/plain") public Response OPTIONS(@Context final UriInfo uriInfo) throws RepositoryException { final String resource = ldpService.getResourceUri(uriInfo); log.debug("OPTIONS to <{}>", resource); @@ -489,22 +613,7 @@ public class LdpWebService { Response.ResponseBuilder builder = createResponse(con, Response.Status.OK, resource); - if (ldpService.isNonRdfSourceResource(con, resource)) { - // Sec. 4.2.8.2 - builder.allow("GET", "HEAD", "OPTIONS"); - } else if (ldpService.isRdfSourceResource(con, resource)) { - if (ldpService.getInteractionModel(con, resource) == LdpService.InteractionModel.LDPR) { - // Sec. 4.2.8.2 - builder.allow("GET", "HEAD", "PATCH", "OPTIONS"); - } else { - // Sec. 4.2.8.2 - builder.allow("GET", "HEAD", "POST", "PATCH", "OPTIONS"); - // Sec. 4.2.3 / Sec. 5.2.3 - builder.header("Accept-Post", LdpUtils.getAcceptPostHeader("*/*")); - } - // Sec. 4.2.7.1 - builder.header("Accept-Patch", RdfPatchParser.MIME_TYPE); - } + addOptionsHeader(con, resource, builder); con.commit(); return builder.build(); @@ -517,10 +626,35 @@ public class LdpWebService { } + private Response.ResponseBuilder addOptionsHeader(RepositoryConnection connection, String resource, Response.ResponseBuilder builder) throws RepositoryException { + log.debug("Adding required LDP Headers (OPTIONS, GET); see Sec. 8.2.8 and Sec. 4.2.2.2"); + if (ldpService.isNonRdfSourceResource(connection, resource)) { + // Sec. 4.2.8.2 + log.trace("<{}> is an LDP-NR: GET, HEAD, PUT and OPTIONS allowed", resource); + builder.allow("GET", "HEAD", "PUT", "OPTIONS"); + } else if (ldpService.isRdfSourceResource(connection, resource)) { + if (ldpService.getInteractionModel(connection, resource) == LdpService.InteractionModel.LDPR) { + log.trace("<{}> is a LDP-RS (LDPR interaction model): GET, HEAD, PUT, PATCH and OPTIONS allowed", resource); + // Sec. 4.2.8.2 + builder.allow("GET", "HEAD", "PUT", "PATCH", "OPTIONS"); + } else { + // Sec. 4.2.8.2 + log.trace("<{}> is a LDP-RS (LDPC interaction model): GET, HEAD, POST, PUT, PATCH and OPTIONS allowed", resource); + builder.allow("GET", "HEAD", "POST", "PUT", "PATCH", "OPTIONS"); + // Sec. 4.2.3 / Sec. 5.2.3 + builder.header("Accept-Post", LdpUtils.getAcceptPostHeader("*/*")); + } + // Sec. 4.2.7.1 + builder.header("Accept-Patch", RdfPatchParser.MIME_TYPE); + } + + return builder; + } + /** * Add all the default headers specified in LDP to the Response * - * @param connection + * @param connection the RepositoryConnection (with active transaction) to read extra data from * @param status the StatusCode * @param resource the iri/uri/url of the resouce * @return the provided ResponseBuilder for chaining @@ -532,7 +666,7 @@ public class LdpWebService { /** * Add all the default headers specified in LDP to the Response * - * @param connection + * @param connection the RepositoryConnection (with active transaction) to read extra data from * @param status the status code * @param resource the uri/url of the resouce * @return the provided ResponseBuilder for chaining @@ -544,9 +678,9 @@ public class LdpWebService { /** * Add all the default headers specified in LDP to the Response * - * @param connection + * @param connection the RepositoryConnection (with active transaction) to read extra data from * @param rb the ResponseBuilder - * @param resource the uri/url of the resouce + * @param resource the uri/url of the resource * @return the provided ResponseBuilder for chaining */ protected Response.ResponseBuilder createResponse(RepositoryConnection connection, Response.ResponseBuilder rb, String resource) throws RepositoryException { @@ -566,8 +700,9 @@ public class LdpWebService { if (rdfSource != null) { // Sec. 5.2.8.1 and 5.2.3.12 // FIXME: Sec. 5.2.3.12, see also http://www.w3.org/2012/ldp/track/issues/15 - rb.link(rdfSource.stringValue(), "meta"); rb.link(rdfSource.stringValue(), "describedby"); + // TODO: Propose to LDP-WG? + rb.link(rdfSource.stringValue(), "meta"); } final URI nonRdfSource = ldpService.getNonRdfSourceForRdfSource(connection, resource); if (nonRdfSource != null) {
