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