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