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