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