001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.http.api;
019
020
021import static com.google.common.base.Strings.nullToEmpty;
022import static java.nio.charset.StandardCharsets.UTF_8;
023import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML;
024import static javax.ws.rs.core.MediaType.APPLICATION_XML;
025import static javax.ws.rs.core.MediaType.TEXT_HTML;
026import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
027import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
028import static javax.ws.rs.core.MediaType.WILDCARD;
029import static javax.ws.rs.core.Response.created;
030import static javax.ws.rs.core.Response.noContent;
031import static javax.ws.rs.core.Response.notAcceptable;
032import static javax.ws.rs.core.Response.ok;
033import static javax.ws.rs.core.Response.temporaryRedirect;
034import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
035import static javax.ws.rs.core.Response.Status.CONFLICT;
036import static javax.ws.rs.core.Response.Status.FORBIDDEN;
037import static javax.ws.rs.core.Response.Status.NOT_IMPLEMENTED;
038import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE;
039import static javax.ws.rs.core.Variant.mediaTypes;
040import static org.apache.commons.lang3.StringUtils.isBlank;
041import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
042import static org.apache.jena.rdf.model.ResourceFactory.createResource;
043import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate;
044import static org.fcrepo.http.commons.domain.RDFMediaType.JSON_LD;
045import static org.fcrepo.http.commons.domain.RDFMediaType.N3;
046import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2;
047import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES;
048import static org.fcrepo.http.commons.domain.RDFMediaType.RDF_XML;
049import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE;
050import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_X;
051import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_BINARY;
052import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_CONTAINER;
053import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_PAIRTREE;
054import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE;
055import static org.fcrepo.kernel.modeshape.services.TransactionServiceImpl.getCurrentTransactionId;
056import static org.slf4j.LoggerFactory.getLogger;
057
058import java.io.IOException;
059import java.io.InputStream;
060import java.io.UnsupportedEncodingException;
061import java.net.URI;
062import java.net.URLDecoder;
063import java.util.Arrays;
064import java.util.Collection;
065import java.util.List;
066import java.util.Map;
067import java.util.stream.Collectors;
068
069import javax.annotation.PostConstruct;
070import javax.inject.Inject;
071import javax.jcr.PathNotFoundException;
072import javax.jcr.RepositoryException;
073import javax.ws.rs.BadRequestException;
074import javax.ws.rs.ClientErrorException;
075import javax.ws.rs.Consumes;
076import javax.ws.rs.DELETE;
077import javax.ws.rs.GET;
078import javax.ws.rs.HEAD;
079import javax.ws.rs.HeaderParam;
080import javax.ws.rs.OPTIONS;
081import javax.ws.rs.POST;
082import javax.ws.rs.PUT;
083import javax.ws.rs.Path;
084import javax.ws.rs.PathParam;
085import javax.ws.rs.Produces;
086import javax.ws.rs.ServerErrorException;
087import javax.ws.rs.core.HttpHeaders;
088import javax.ws.rs.core.Link;
089import javax.ws.rs.core.MediaType;
090import javax.ws.rs.core.Response;
091import javax.ws.rs.core.UriBuilderException;
092import javax.ws.rs.core.Variant.VariantListBuilder;
093
094import org.apache.commons.io.IOUtils;
095import org.apache.commons.lang3.StringUtils;
096import org.fcrepo.http.api.PathLockManager.AcquiredLock;
097import org.fcrepo.http.commons.domain.ContentLocation;
098import org.fcrepo.http.commons.domain.PATCH;
099import org.fcrepo.http.commons.responses.RdfNamespacedStream;
100import org.fcrepo.kernel.api.RdfStream;
101import org.fcrepo.kernel.api.exception.AccessDeniedException;
102import org.fcrepo.kernel.api.exception.InvalidChecksumException;
103import org.fcrepo.kernel.api.exception.MalformedRdfException;
104import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
105import org.fcrepo.kernel.api.models.Container;
106import org.fcrepo.kernel.api.models.FedoraBinary;
107import org.fcrepo.kernel.api.models.FedoraResource;
108import org.fcrepo.kernel.api.models.NonRdfSourceDescription;
109import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
110import org.fcrepo.kernel.api.utils.ContentDigest;
111import org.glassfish.jersey.media.multipart.ContentDisposition;
112import org.slf4j.Logger;
113import org.springframework.context.annotation.Scope;
114
115import com.codahale.metrics.annotation.Timed;
116import com.google.common.annotations.VisibleForTesting;
117import com.google.common.base.Splitter;
118import com.google.common.collect.ImmutableList;
119
120/**
121 * @author cabeer
122 * @author ajs6f
123 * @since 9/25/14
124 */
125
126@Scope("request")
127@Path("/{path: .*}")
128public class FedoraLdp extends ContentExposingResource {
129
130    private static final Logger LOGGER = getLogger(FedoraLdp.class);
131
132    private static final Splitter.MapSplitter RFC3230_SPLITTER =
133        Splitter.on(',').omitEmptyStrings().trimResults().
134        withKeyValueSeparator(Splitter.on('=').limit(2));
135
136    @PathParam("path") protected String externalPath;
137
138    @Inject private FedoraHttpConfiguration httpConfiguration;
139
140    /**
141     * Default JAX-RS entry point
142     */
143    public FedoraLdp() {
144        super();
145    }
146
147    /**
148     * Create a new FedoraNodes instance for a given path
149     * @param externalPath the external path
150     */
151    @VisibleForTesting
152    public FedoraLdp(final String externalPath) {
153        this.externalPath = externalPath;
154    }
155
156    /**
157     * Run these actions after initializing this resource
158     */
159    @PostConstruct
160    public void postConstruct() {
161        setUpJMSInfo(uriInfo, headers);
162    }
163
164    /**
165     * Retrieve the node headers
166     * @return response
167     */
168    @HEAD
169    @Timed
170    public Response head() {
171        LOGGER.info("HEAD for: {}", externalPath);
172
173        checkCacheControlHeaders(request, servletResponse, resource(), session);
174
175        addResourceHttpHeaders(resource());
176
177        Response.ResponseBuilder builder = ok();
178
179        if (resource() instanceof FedoraBinary) {
180            final MediaType mediaType = MediaType.valueOf(((FedoraBinary) resource()).getMimeType());
181
182            if (isExternalBody(mediaType)) {
183                builder = temporaryRedirect(URI.create(mediaType.getParameters().get("URL")));
184            }
185
186            // we set the content-type explicitly to avoid content-negotiation from getting in the way
187            builder.type(mediaType.toString());
188        }
189
190        return builder.build();
191    }
192
193    /**
194     * Outputs information about the supported HTTP methods, etc.
195     * @return the outputs information about the supported HTTP methods, etc.
196     */
197    @OPTIONS
198    @Timed
199    public Response options() {
200        LOGGER.info("OPTIONS for '{}'", externalPath);
201        addLinkAndOptionsHttpHeaders();
202        return ok().build();
203    }
204
205
206    /**
207     * Retrieve the node profile
208     *
209     * @param rangeValue the range value
210     * @return a binary or the triples for the specified node
211     * @throws IOException if IO exception occurred
212     */
213    @GET
214    @Produces({TURTLE + ";qs=1.0", JSON_LD + ";qs=0.8",
215            N3, N3_ALT2, RDF_XML, NTRIPLES, APPLICATION_XML, TEXT_PLAIN, TURTLE_X,
216            TEXT_HTML, APPLICATION_XHTML_XML})
217    public Response getResource(@HeaderParam("Range") final String rangeValue) throws IOException {
218        checkCacheControlHeaders(request, servletResponse, resource(), session);
219
220        LOGGER.info("GET resource '{}'", externalPath);
221        final AcquiredLock readLock = lockManager.lockForRead(resource().getPath());
222        try (final RdfStream rdfStream = new DefaultRdfStream(asNode(resource()))) {
223
224            // If requesting a binary, check the mime-type if "Accept:" header is present.
225            // (This needs to be done before setting up response headers, as getContent
226            // returns a response - so changing headers after that won't work so nicely.)
227            final ImmutableList<MediaType> acceptableMediaTypes = ImmutableList.copyOf(headers
228                    .getAcceptableMediaTypes());
229
230            if (resource() instanceof FedoraBinary && acceptableMediaTypes.size() > 0) {
231                final MediaType mediaType = MediaType.valueOf(((FedoraBinary) resource()).getMimeType());
232
233                if (!acceptableMediaTypes.stream().anyMatch(t -> t.isCompatible(mediaType))) {
234                    return notAcceptable(VariantListBuilder.newInstance().mediaTypes(mediaType).build()).build();
235                }
236            }
237
238            addResourceHttpHeaders(resource());
239            return getContent(rangeValue, getChildrenLimit(), rdfStream);
240        } finally {
241            readLock.release();
242        }
243    }
244
245    private int getChildrenLimit() {
246        final List<String> acceptHeaders = headers.getRequestHeader(HttpHeaders.ACCEPT);
247        if (acceptHeaders != null && acceptHeaders.size() > 0) {
248            final List<String> accept = Arrays.asList(acceptHeaders.get(0).split(","));
249            if (accept.contains(TEXT_HTML) || accept.contains(APPLICATION_XHTML_XML)) {
250                // Magic number '100' is tied to common-metadata.vsl display of ellipses
251                return 100;
252            }
253        }
254
255        final List<String> limits = headers.getRequestHeader("Limit");
256        if (null != limits && limits.size() > 0) {
257            try {
258                return Integer.parseInt(limits.get(0));
259
260            } catch (final NumberFormatException e) {
261                LOGGER.warn("Invalid 'Limit' header value: {}", limits.get(0));
262                throw new ClientErrorException("Invalid 'Limit' header value: " + limits.get(0), SC_BAD_REQUEST, e);
263            }
264        }
265        return -1;
266    }
267
268    /**
269     * Deletes an object.
270     *
271     * @return response
272     */
273    @DELETE
274    @Timed
275    public Response deleteObject() {
276        evaluateRequestPreconditions(request, servletResponse, resource(), session);
277
278        LOGGER.info("Delete resource '{}'", externalPath);
279
280        final AcquiredLock lock = lockManager.lockForDelete(resource().getPath());
281
282        try {
283            resource().delete();
284
285            try {
286                session.save();
287            } catch (final RepositoryException e) {
288                throw new RepositoryRuntimeException(e);
289            }
290
291            return noContent().build();
292        } finally {
293            lock.release();
294        }
295    }
296
297    /**
298     * Create a resource at a specified path, or replace triples with provided RDF.
299     *
300     * @param requestContentType the request content type
301     * @param requestBodyStream the request body stream
302     * @param contentDisposition the content disposition value
303     * @param ifMatch the if-match value
304     * @param link the link value
305     * @param digest the digest header
306     * @return 204
307     * @throws InvalidChecksumException if invalid checksum exception occurred
308     * @throws MalformedRdfException if malformed rdf exception occurred
309     */
310    @PUT
311    @Consumes
312    @Timed
313    public Response createOrReplaceObjectRdf(
314            @HeaderParam("Content-Type") final MediaType requestContentType,
315            @ContentLocation final InputStream requestBodyStream,
316            @HeaderParam("Content-Disposition") final ContentDisposition contentDisposition,
317            @HeaderParam("If-Match") final String ifMatch,
318            @HeaderParam("Link") final String link,
319            @HeaderParam("Digest") final String digest)
320            throws InvalidChecksumException, MalformedRdfException {
321
322        checkLinkForLdpResourceCreation(link);
323
324        final FedoraResource resource;
325
326        final String path = toPath(translator(), externalPath);
327
328        final AcquiredLock lock = lockManager.lockForWrite(path, session, nodeService);
329
330        try {
331
332            final Collection<String> checksums = parseDigestHeader(digest);
333
334            final MediaType contentType = getSimpleContentType(requestContentType);
335
336
337            if (nodeService.exists(session, path)) {
338                resource = resource();
339            } else {
340                final MediaType effectiveContentType
341                        = requestBodyStream == null || requestContentType == null ? null : contentType;
342                resource = createFedoraResource(path, effectiveContentType, contentDisposition);
343            }
344
345
346            if (httpConfiguration.putRequiresIfMatch() && StringUtils.isBlank(ifMatch) && !resource.isNew()) {
347                throw new ClientErrorException("An If-Match header is required", 428);
348            }
349
350            evaluateRequestPreconditions(request, servletResponse, resource, session);
351            final boolean created = resource.isNew();
352
353            try (final RdfStream resourceTriples =
354                    created ? new DefaultRdfStream(asNode(resource())) : getResourceTriples()) {
355
356                LOGGER.info("PUT resource '{}'", externalPath);
357                if (resource instanceof FedoraBinary) {
358                    replaceResourceBinaryWithStream((FedoraBinary) resource,
359                            requestBodyStream, contentDisposition, requestContentType, checksums);
360                } else if (isRdfContentType(contentType.toString())) {
361                    replaceResourceWithStream(resource, requestBodyStream, contentType, resourceTriples);
362                } else if (!created) {
363                    boolean emptyRequest = true;
364                    try {
365                        emptyRequest = requestBodyStream.read() == -1;
366                    } catch (final IOException ex) {
367                        LOGGER.debug("Error checking for request body content", ex);
368                    }
369
370                    if (requestContentType == null && emptyRequest) {
371                        throw new ClientErrorException("Resource Already Exists", CONFLICT);
372                    }
373                    throw new ClientErrorException("Invalid Content Type " + requestContentType,
374                            UNSUPPORTED_MEDIA_TYPE);
375                }
376            }
377
378            try {
379                session.save();
380            } catch (final RepositoryException e) {
381                throw new RepositoryRuntimeException(e);
382            }
383
384            return createUpdateResponse(resource, created);
385        } finally {
386            lock.release();
387        }
388    }
389
390    /**
391     * Update an object using SPARQL-UPDATE
392     *
393     * @param requestBodyStream the request body stream
394     * @return 201
395     * @throws IOException if IO exception occurred
396     */
397    @PATCH
398    @Consumes({contentTypeSPARQLUpdate})
399    @Timed
400    public Response updateSparql(@ContentLocation final InputStream requestBodyStream)
401            throws IOException {
402
403        if (null == requestBodyStream) {
404            throw new BadRequestException("SPARQL-UPDATE requests must have content!");
405        }
406
407        if (resource() instanceof FedoraBinary) {
408            throw new BadRequestException(resource().getPath() + " is not a valid object to receive a PATCH");
409        }
410
411        final AcquiredLock lock = lockManager.lockForWrite(resource().getPath(), session, nodeService);
412
413        try {
414            final String requestBody = IOUtils.toString(requestBodyStream, UTF_8);
415            if (isBlank(requestBody)) {
416                throw new BadRequestException("SPARQL-UPDATE requests must have content!");
417            }
418
419            evaluateRequestPreconditions(request, servletResponse, resource(), session);
420
421            try (final RdfStream resourceTriples =
422                    resource().isNew() ? new DefaultRdfStream(asNode(resource())) : getResourceTriples()) {
423                LOGGER.info("PATCH for '{}'", externalPath);
424                patchResourcewithSparql(resource(), requestBody, resourceTriples);
425            }
426            session.save();
427
428            addCacheControlHeaders(servletResponse, resource().getDescription(), session);
429
430            return noContent().build();
431        } catch (final IllegalArgumentException iae) {
432            throw new BadRequestException(iae.getMessage());
433        } catch (final AccessDeniedException e) {
434            throw e;
435        } catch ( final RuntimeException ex ) {
436            final Throwable cause = ex.getCause();
437            if (cause instanceof PathNotFoundException) {
438                // the sparql update referred to a repository resource that doesn't exist
439                throw new BadRequestException(cause.getMessage());
440            }
441            throw ex;
442        }  catch (final RepositoryException e) {
443            throw new RepositoryRuntimeException(e);
444        } finally {
445            lock.release();
446        }
447    }
448
449    /**
450     * Creates a new object.
451     *
452     * This originally used application/octet-stream;qs=1001 as a workaround
453     * for JERSEY-2636, to ensure requests without a Content-Type get routed here.
454     * This qs value does not parse with newer versions of Jersey, as qs values
455     * must be between 0 and 1.  We use qs=1.000 to mark where this historical
456     * anomaly had been.
457     *
458     * @param contentDisposition the content Disposition value
459     * @param requestContentType the request content type
460     * @param slug the slug value
461     * @param requestBodyStream the request body stream
462     * @param link the link value
463     * @param digest the digest header
464     * @return 201
465     * @throws InvalidChecksumException if invalid checksum exception occurred
466     * @throws IOException if IO exception occurred
467     * @throws MalformedRdfException if malformed rdf exception occurred
468     */
469    @POST
470    @Consumes({MediaType.APPLICATION_OCTET_STREAM + ";qs=1.000", WILDCARD})
471    @Timed
472    @Produces({TURTLE + ";qs=1.0", JSON_LD + ";qs=0.8",
473            N3, N3_ALT2, RDF_XML, NTRIPLES, APPLICATION_XML, TEXT_PLAIN, TURTLE_X,
474            TEXT_HTML, APPLICATION_XHTML_XML, "*/*"})
475    public Response createObject(@HeaderParam("Content-Disposition") final ContentDisposition contentDisposition,
476                                 @HeaderParam("Content-Type") final MediaType requestContentType,
477                                 @HeaderParam("Slug") final String slug,
478                                 @ContentLocation final InputStream requestBodyStream,
479                                 @HeaderParam("Link") final String link,
480                                 @HeaderParam("Digest") final String digest)
481            throws InvalidChecksumException, IOException, MalformedRdfException {
482
483        checkLinkForLdpResourceCreation(link);
484
485        if (!(resource() instanceof Container)) {
486            throw new ClientErrorException("Object cannot have child nodes", CONFLICT);
487        } else if (resource().hasType(FEDORA_PAIRTREE)) {
488            throw new ClientErrorException("Objects cannot be created under pairtree nodes", FORBIDDEN);
489        }
490
491        final MediaType contentType = getSimpleContentType(requestContentType);
492
493        final String contentTypeString = contentType.toString();
494
495        final String newObjectPath = mintNewPid(slug);
496
497        final AcquiredLock lock = lockManager.lockForWrite(newObjectPath, session, nodeService);
498
499        try {
500
501            final Collection<String> checksum = parseDigestHeader(digest);
502
503            LOGGER.info("Ingest with path: {}", newObjectPath);
504
505            final MediaType effectiveContentType
506                    = requestBodyStream == null || requestContentType == null ? null : contentType;
507            resource = createFedoraResource(newObjectPath, effectiveContentType, contentDisposition);
508
509            try (final RdfStream resourceTriples =
510                    resource.isNew() ? new DefaultRdfStream(asNode(resource())) : getResourceTriples()) {
511
512                if (requestBodyStream == null) {
513                    LOGGER.trace("No request body detected");
514                } else {
515                    LOGGER.trace("Received createObject with a request body and content type \"{}\"",
516                            contentTypeString);
517
518                    if ((resource instanceof Container) && isRdfContentType(contentTypeString)) {
519                        replaceResourceWithStream(resource, requestBodyStream, contentType, resourceTriples);
520                    } else if (resource instanceof FedoraBinary) {
521                        LOGGER.trace("Created a datastream and have a binary payload.");
522                        replaceResourceBinaryWithStream((FedoraBinary) resource,
523                                requestBodyStream, contentDisposition, requestContentType, checksum);
524
525                    } else if (contentTypeString.equals(contentTypeSPARQLUpdate)) {
526                        LOGGER.trace("Found SPARQL-Update content, applying..");
527                        patchResourcewithSparql(resource, IOUtils.toString(requestBodyStream, UTF_8), resourceTriples);
528                    } else {
529                        if (requestBodyStream.read() != -1) {
530                            throw new ClientErrorException("Invalid Content Type " + contentTypeString,
531                                    UNSUPPORTED_MEDIA_TYPE);
532                        }
533                    }
534                }
535                session.save();
536            } catch (final RepositoryException e) {
537                throw new RepositoryRuntimeException(e);
538            }
539
540            LOGGER.debug("Finished creating resource with path: {}", newObjectPath);
541            return createUpdateResponse(resource, true);
542        } finally {
543            lock.release();
544        }
545    }
546
547    /**
548     * Create the appropriate response after a create or update request is processed.  When a resource is created,
549     * examine the Prefer and Accept headers to determine whether to include a representation.  By default, the
550     * URI for the created resource is return as plain text.  If a minimal response is requested, then no body is
551     * returned.  If a non-minimal return is requested, return the RDF for the created resource in the appropriate
552     * RDF serialization.
553     *
554     * @param resource The created or updated Fedora resource.
555     * @param created True for a newly-created resource, false for an updated resource.
556     * @return 204 No Content (for updated resources), 201 Created (for created resources) including the resource
557     *    URI or content depending on Prefer headers.
558     */
559    @SuppressWarnings("resource")
560    private Response createUpdateResponse(final FedoraResource resource, final boolean created) {
561        addCacheControlHeaders(servletResponse, resource, session);
562        addResourceLinkHeaders(resource, created);
563        if (!created) {
564            return noContent().build();
565        }
566
567        final URI location = getUri(resource);
568        final Response.ResponseBuilder builder = created(location);
569
570        if (prefer == null || !prefer.hasReturn()) {
571            final MediaType acceptablePlainText = acceptabePlainTextMediaType();
572            if (acceptablePlainText != null) {
573                return builder.type(acceptablePlainText).entity(location.toString()).build();
574            }
575            return notAcceptable(mediaTypes(TEXT_PLAIN_TYPE).build()).build();
576        } else if (prefer.getReturn().getValue().equals("minimal")) {
577            return builder.build();
578        } else {
579            servletResponse.addHeader("Vary", "Accept, Range, Accept-Encoding, Accept-Language");
580            if (prefer != null) {
581                prefer.getReturn().addResponseHeaders(servletResponse);
582            }
583            final RdfNamespacedStream rdfStream = new RdfNamespacedStream(
584                new DefaultRdfStream(asNode(resource()), getResourceTriples()),
585                    namespaceService.getNamespaces(session()));
586            return builder.entity(rdfStream).build();
587        }
588    }
589
590    /**
591     * Returns an acceptable plain text media type if possible, or null if not.
592     */
593    private MediaType acceptabePlainTextMediaType() {
594        final List<MediaType> acceptable = headers.getAcceptableMediaTypes();
595        if (acceptable == null || acceptable.size() == 0) {
596            return TEXT_PLAIN_TYPE;
597        }
598        for (final MediaType type : acceptable ) {
599            if (type.isWildcardType() || (type.isCompatible(TEXT_PLAIN_TYPE) && type.isWildcardSubtype())) {
600                return TEXT_PLAIN_TYPE;
601            } else if (type.isCompatible(TEXT_PLAIN_TYPE)) {
602                return type;
603            }
604        }
605        return null;
606    }
607
608    @Override
609    protected void addResourceHttpHeaders(final FedoraResource resource) {
610        super.addResourceHttpHeaders(resource);
611
612        if (getCurrentTransactionId(session) != null) {
613            final String canonical = translator().reverse()
614                    .convert(resource)
615                    .toString()
616                    .replaceFirst("/tx:[^/]+", "");
617
618
619            servletResponse.addHeader("Link", "<" + canonical + ">;rel=\"canonical\"");
620
621        }
622
623        addLinkAndOptionsHttpHeaders();
624    }
625
626    @Override
627    protected String externalPath() {
628        return externalPath;
629    }
630
631    private void addLinkAndOptionsHttpHeaders() {
632        // Add Link headers
633        addResourceLinkHeaders(resource());
634
635        // Add Options headers
636        final String options;
637
638        if (resource() instanceof FedoraBinary) {
639            options = "DELETE,HEAD,GET,PUT,OPTIONS";
640
641        } else if (resource() instanceof NonRdfSourceDescription) {
642            options = "MOVE,COPY,DELETE,POST,HEAD,GET,PUT,PATCH,OPTIONS";
643            servletResponse.addHeader("Accept-Patch", contentTypeSPARQLUpdate);
644
645        } else if (resource() instanceof Container) {
646            options = "MOVE,COPY,DELETE,POST,HEAD,GET,PUT,PATCH,OPTIONS";
647            servletResponse.addHeader("Accept-Patch", contentTypeSPARQLUpdate);
648
649            final String rdfTypes = TURTLE + "," + N3 + ","
650                    + N3_ALT2 + "," + RDF_XML + "," + NTRIPLES;
651            servletResponse.addHeader("Accept-Post", rdfTypes + "," + MediaType.MULTIPART_FORM_DATA
652                    + "," + contentTypeSPARQLUpdate);
653        } else {
654            options = "";
655        }
656
657        servletResponse.addHeader("Allow", options);
658    }
659
660    private static String getRequestedObjectType(final MediaType requestContentType,
661                                          final ContentDisposition contentDisposition) {
662
663        if (requestContentType != null) {
664            final String s = requestContentType.toString();
665            if (!s.equals(contentTypeSPARQLUpdate) && !isRdfContentType(s) || s.equals(TEXT_PLAIN)) {
666                return FEDORA_BINARY;
667            }
668        }
669
670        if (contentDisposition != null && contentDisposition.getType().equals("attachment")) {
671            return FEDORA_BINARY;
672        }
673
674        return FEDORA_CONTAINER;
675    }
676
677    private FedoraResource createFedoraResource(final String path,
678                                                final MediaType requestContentType,
679                                                final ContentDisposition contentDisposition) {
680        final String objectType = getRequestedObjectType(requestContentType, contentDisposition);
681
682        final FedoraResource result;
683
684        if (objectType.equals(FEDORA_BINARY)) {
685            result = binaryService.findOrCreate(session, path);
686        } else {
687            result = containerService.findOrCreate(session, path);
688        }
689
690        return result;
691    }
692
693    private String mintNewPid(final String slug) {
694        String pid;
695
696        if (slug != null && !slug.isEmpty()) {
697            pid = slug;
698        } else if (pidMinter != null) {
699            pid = pidMinter.get();
700        } else {
701            pid = defaultPidMinter.get();
702        }
703        // reverse translate the proffered or created identifier
704        LOGGER.trace("Using external identifier {} to create new resource.", pid);
705        LOGGER.trace("Using prefixed external identifier {} to create new resource.", uriInfo.getBaseUri() + "/"
706                + pid);
707
708        final URI newResourceUri = uriInfo.getAbsolutePathBuilder().clone().path(FedoraLdp.class)
709                .resolveTemplate("path", pid, false).build();
710
711        pid = translator().asString(createResource(newResourceUri.toString()));
712        try {
713            pid = URLDecoder.decode(pid, "UTF-8");
714        } catch (final UnsupportedEncodingException e) {
715            // noop
716        }
717        // remove leading slash left over from translation
718        LOGGER.trace("Using internal identifier {} to create new resource.", pid);
719
720        if (nodeService.exists(session, pid)) {
721            LOGGER.trace("Resource with path {} already exists; minting new path instead", pid);
722            return mintNewPid(null);
723        }
724
725        return pid;
726    }
727
728    private static void checkLinkForLdpResourceCreation(final String link) {
729        if (link != null) {
730            try {
731                final Link linq = Link.valueOf(link);
732                if ("type".equals(linq.getRel()) && (LDP_NAMESPACE + "Resource").equals(linq.getUri().toString())) {
733                    LOGGER.info("Unimplemented LDPR creation requested with header link: {}", link);
734                    throw new ServerErrorException("LDPR creation not implemented", NOT_IMPLEMENTED);
735                }
736            } catch (final RuntimeException e) {
737                if (e instanceof IllegalArgumentException | e instanceof UriBuilderException) {
738                    throw new ClientErrorException("Invalid link specified: " + link, BAD_REQUEST);
739                }
740                throw e;
741            }
742        }
743    }
744
745    /**
746     * Parse the RFC-3230 Digest response header value.  Look for a
747     * sha1 checksum and return it as a urn, if missing or malformed
748     * an empty string is returned.
749     * @param digest The Digest header value
750     * @return the sha1 checksum value
751     * @throws InvalidChecksumException if an unsupported digest is used
752     */
753    private static Collection<String> parseDigestHeader(final String digest) throws InvalidChecksumException {
754        try {
755            final Map<String,String> digestPairs = RFC3230_SPLITTER.split(nullToEmpty(digest));
756            final boolean allSupportedAlgorithms = digestPairs.keySet().stream().allMatch(
757                    ContentDigest.DIGEST_ALGORITHM::isSupportedAlgorithm);
758
759            // If you have one or more digests that are all valid or no digests.
760            if (digestPairs.isEmpty() || allSupportedAlgorithms) {
761                return digestPairs.entrySet().stream()
762                    .filter(entry -> ContentDigest.DIGEST_ALGORITHM.isSupportedAlgorithm(entry.getKey()))
763                    .map(entry -> ContentDigest.asURI(entry.getKey(), entry.getValue()).toString())
764                    .collect(Collectors.toSet());
765            } else {
766                throw new InvalidChecksumException(String.format("Unsupported Digest Algorithim: %1$s", digest));
767            }
768        } catch (final RuntimeException e) {
769            if (e instanceof IllegalArgumentException) {
770                throw new ClientErrorException("Invalid Digest header: " + digest + "\n", BAD_REQUEST);
771            }
772            throw e;
773        }
774    }
775}