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