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
020import static com.google.common.base.Strings.nullToEmpty;
021import static java.text.MessageFormat.format;
022import static java.util.EnumSet.of;
023import static java.util.stream.Stream.concat;
024import static java.util.stream.Stream.empty;
025import static javax.ws.rs.core.HttpHeaders.ACCEPT;
026import static javax.ws.rs.core.HttpHeaders.CACHE_CONTROL;
027import static javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION;
028import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
029import static javax.ws.rs.core.HttpHeaders.CONTENT_LOCATION;
030import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
031import static javax.ws.rs.core.HttpHeaders.LINK;
032import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
033import static javax.ws.rs.core.MediaType.TEXT_HTML;
034import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
035import static javax.ws.rs.core.Response.created;
036import static javax.ws.rs.core.Response.noContent;
037import static javax.ws.rs.core.Response.notAcceptable;
038import static javax.ws.rs.core.Response.ok;
039import static javax.ws.rs.core.Response.status;
040import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
041import static javax.ws.rs.core.Response.Status.PARTIAL_CONTENT;
042import static javax.ws.rs.core.Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE;
043import static javax.ws.rs.core.Variant.mediaTypes;
044import static org.apache.commons.lang3.StringUtils.isBlank;
045import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
046import static org.apache.jena.graph.NodeFactory.createURI;
047import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
048import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
049import static org.apache.jena.rdf.model.ResourceFactory.createResource;
050import static org.apache.jena.rdf.model.ResourceFactory.createStatement;
051import static org.apache.jena.riot.RDFLanguages.contentTypeToLang;
052import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate;
053import static org.apache.jena.vocabulary.RDF.type;
054import static org.fcrepo.http.api.FedoraVersioning.MEMENTO_DATETIME_HEADER;
055import static org.fcrepo.http.commons.domain.RDFMediaType.JSON_LD;
056import static org.fcrepo.http.commons.domain.RDFMediaType.N3;
057import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2;
058import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES;
059import static org.fcrepo.http.commons.domain.RDFMediaType.RDF_XML;
060import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE;
061import static org.fcrepo.kernel.api.FedoraExternalContent.COPY;
062import static org.fcrepo.kernel.api.FedoraExternalContent.PROXY;
063import static org.fcrepo.kernel.api.FedoraExternalContent.REDIRECT;
064import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL;
065import static org.fcrepo.kernel.api.FedoraTypes.LDP_BASIC_CONTAINER;
066import static org.fcrepo.kernel.api.FedoraTypes.LDP_DIRECT_CONTAINER;
067import static org.fcrepo.kernel.api.FedoraTypes.LDP_INDIRECT_CONTAINER;
068import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER;
069import static org.fcrepo.kernel.api.RdfLexicon.CONTAINER;
070import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
071import static org.fcrepo.kernel.api.RdfLexicon.HAS_MEMBER_RELATION;
072import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER;
073import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE;
074import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE;
075import static org.fcrepo.kernel.api.RdfLexicon.RDF_SOURCE;
076import static org.fcrepo.kernel.api.RdfLexicon.VERSIONED_RESOURCE;
077import static org.fcrepo.kernel.api.RdfLexicon.VERSIONING_TIMEGATE_TYPE;
078import static org.fcrepo.kernel.api.RdfLexicon.VERSIONING_TIMEMAP_TYPE;
079import static org.fcrepo.kernel.api.RdfLexicon.WEBAC_NAMESPACE_VALUE;
080import static org.fcrepo.kernel.api.RdfLexicon.isManagedNamespace;
081import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate;
082import static org.fcrepo.kernel.api.RequiredRdfContext.EMBED_RESOURCES;
083import static org.fcrepo.kernel.api.RequiredRdfContext.INBOUND_REFERENCES;
084import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT;
085import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP;
086import static org.fcrepo.kernel.api.RequiredRdfContext.MINIMAL;
087import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES;
088import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED;
089import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_RFC_1123_FORMATTER;
090import static org.slf4j.LoggerFactory.getLogger;
091
092import com.fasterxml.jackson.core.JsonParseException;
093import com.google.common.annotations.VisibleForTesting;
094import com.google.common.base.Splitter;
095import java.io.IOException;
096import java.io.InputStream;
097import java.net.URI;
098import java.net.URISyntaxException;
099import java.time.Instant;
100import java.time.ZoneOffset;
101import java.util.Arrays;
102import java.util.ArrayList;
103import java.util.Collection;
104import java.util.Date;
105import java.util.HashSet;
106import java.util.List;
107import java.util.Map;
108import java.util.Set;
109import java.util.function.Predicate;
110import java.util.stream.Collectors;
111import java.util.stream.Stream;
112import javax.annotation.PostConstruct;
113import javax.inject.Inject;
114import javax.servlet.ServletContext;
115import javax.servlet.http.HttpServletResponse;
116import javax.ws.rs.BadRequestException;
117import javax.ws.rs.BeanParam;
118import javax.ws.rs.ClientErrorException;
119import javax.ws.rs.core.CacheControl;
120import javax.ws.rs.core.Context;
121import javax.ws.rs.core.EntityTag;
122import javax.ws.rs.core.Link;
123import javax.ws.rs.core.MediaType;
124import javax.ws.rs.core.Request;
125import javax.ws.rs.core.Response;
126
127import org.apache.http.client.methods.HttpPatch;
128import org.apache.http.client.methods.HttpPut;
129import org.apache.jena.atlas.RuntimeIOException;
130import org.apache.jena.graph.Graph;
131import org.apache.jena.graph.Node;
132import org.apache.jena.graph.Triple;
133import org.apache.jena.rdf.model.Model;
134import org.apache.jena.rdf.model.Property;
135import org.apache.jena.rdf.model.RDFNode;
136import org.apache.jena.rdf.model.Statement;
137import org.apache.jena.riot.Lang;
138import org.apache.jena.riot.RiotException;
139import org.fcrepo.http.commons.api.HttpHeaderInjector;
140import org.fcrepo.http.commons.api.rdf.HttpTripleUtil;
141import org.fcrepo.http.commons.domain.MultiPrefer;
142import org.fcrepo.http.commons.domain.PreferTag;
143import org.fcrepo.http.commons.domain.Range;
144import org.fcrepo.http.commons.domain.ldp.LdpPreferTag;
145import org.fcrepo.http.commons.responses.RangeRequestInputStream;
146import org.fcrepo.http.commons.responses.RdfNamespacedStream;
147import org.fcrepo.http.commons.session.HttpSession;
148import org.fcrepo.kernel.api.RdfStream;
149import org.fcrepo.kernel.api.TripleCategory;
150import org.fcrepo.kernel.api.exception.ACLAuthorizationConstraintViolationException;
151import org.fcrepo.kernel.api.exception.InsufficientStorageException;
152import org.fcrepo.kernel.api.exception.InvalidChecksumException;
153import org.fcrepo.kernel.api.exception.MalformedRdfException;
154import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
155import org.fcrepo.kernel.api.exception.PreconditionException;
156import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
157import org.fcrepo.kernel.api.exception.ServerManagedPropertyException;
158import org.fcrepo.kernel.api.exception.ServerManagedTypeException;
159import org.fcrepo.kernel.api.exception.UnsupportedAlgorithmException;
160import org.fcrepo.kernel.api.models.Container;
161import org.fcrepo.kernel.api.models.FedoraBinary;
162import org.fcrepo.kernel.api.models.FedoraResource;
163import org.fcrepo.kernel.api.models.FedoraTimeMap;
164import org.fcrepo.kernel.api.models.NonRdfSourceDescription;
165import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
166import org.fcrepo.kernel.api.rdf.RdfNamespaceRegistry;
167import org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint;
168import org.fcrepo.kernel.api.utils.ContentDigest;
169import org.glassfish.jersey.media.multipart.ContentDisposition;
170import org.jvnet.hk2.annotations.Optional;
171import org.slf4j.Logger;
172
173/**
174 * An abstract class that sits between AbstractResource and any resource that
175 * wishes to share the routines for building responses containing binary
176 * content.
177 *
178 * @author Mike Durbin
179 * @author ajs6f
180 */
181public abstract class ContentExposingResource extends FedoraBaseResource {
182
183    private static final Logger LOGGER = getLogger(ContentExposingResource.class);
184
185    private static final List<String> VARY_HEADERS = Arrays.asList("Accept", "Range", "Accept-Encoding",
186            "Accept-Language");
187
188    static final String INSUFFICIENT_SPACE_IDENTIFYING_MESSAGE = "No space left on device";
189
190    public static final String ACCEPT_DATETIME = "Accept-Datetime";
191
192    static final String ACCEPT_EXTERNAL_CONTENT = "Accept-External-Content-Handling";
193
194    static final String HTTP_HEADER_ACCEPT_PATCH = "Accept-Patch";
195
196    static final String WEBAC_ACCESS_TO = WEBAC_NAMESPACE_VALUE + "accessTo";
197
198    static final String WEBAC_ACCESS_TO_CLASS = WEBAC_NAMESPACE_VALUE + "accessToClass";
199
200    static final Node WEBAC_ACCESS_TO_URI = createURI(WEBAC_ACCESS_TO);
201
202    static final Node WEBAC_ACCESS_TO_CLASS_URI = createURI(WEBAC_ACCESS_TO_CLASS);
203
204    static final Property WEBAC_ACCESS_TO_PROPERTY = createProperty(WEBAC_ACCESS_TO);
205
206    @Context protected Request request;
207    @Context protected HttpServletResponse servletResponse;
208    @Context protected ServletContext context;
209
210    @Inject
211    @Optional
212    private HttpTripleUtil httpTripleUtil;
213
214    @Inject
215    @Optional
216    private HttpHeaderInjector httpHeaderInject;
217
218    @BeanParam
219    protected MultiPrefer prefer;
220
221    @Inject
222    @Optional
223    StoragePolicyDecisionPoint storagePolicyDecisionPoint;
224
225    private FedoraResource fedoraResource;
226
227    @Inject
228    protected  PathLockManager lockManager;
229
230    @Inject
231    protected ExternalContentHandlerFactory extContentHandlerFactory;
232
233    @Inject
234    protected RdfNamespaceRegistry namespaceRegistry;
235
236    private static final Predicate<Triple> IS_MANAGED_TYPE = t -> t.getPredicate().equals(type.asNode()) &&
237            isManagedNamespace.test(t.getObject().getNameSpace());
238    private static final Predicate<Triple> IS_MANAGED_TRIPLE = IS_MANAGED_TYPE
239        .or(t -> isManagedPredicate.test(createProperty(t.getPredicate().getURI())));
240
241    protected abstract String externalPath();
242
243    protected static final Splitter.MapSplitter RFC3230_SPLITTER =
244        Splitter.on(',').omitEmptyStrings().trimResults().withKeyValueSeparator(Splitter.on('=').limit(2));
245
246    /**
247     * Run these actions after initializing this resource
248     */
249    @PostConstruct
250    public void postConstruct() {
251        setUpJMSInfo(uriInfo, headers);
252    }
253
254    /**
255     * This method returns an HTTP response with content body appropriate to the following arguments.
256     *
257     * @param rangeValue starting and ending byte offsets, see {@link Range}
258     * @param limit is the number of child resources returned in the response, -1 for all
259     * @param rdfStream to which response RDF will be concatenated
260     * @param resource the fedora resource
261     * @return HTTP response
262     * @throws IOException in case of error extracting content
263     */
264    protected Response getContent(final String rangeValue,
265                                  final int limit,
266                                  final RdfStream rdfStream,
267                                  final FedoraResource resource) throws IOException {
268
269        final RdfNamespacedStream outputStream;
270
271        if (resource instanceof FedoraBinary) {
272            return getBinaryContent(rangeValue, resource);
273        } else {
274            outputStream = new RdfNamespacedStream(
275                    new DefaultRdfStream(rdfStream.topic(), concat(rdfStream,
276                        getResourceTriples(limit, resource))),
277                    namespaceRegistry.getNamespaces());
278        }
279        setVaryAndPreferenceAppliedHeaders(servletResponse, prefer, resource);
280        return ok(outputStream).build();
281    }
282
283    protected void setVaryAndPreferenceAppliedHeaders(final HttpServletResponse servletResponse,
284            final MultiPrefer prefer, final FedoraResource resource) {
285        if (prefer != null) {
286            prefer.getReturn().addResponseHeaders(servletResponse);
287        }
288
289        // add vary headers
290        final List<String> varyValues = new ArrayList<>(VARY_HEADERS);
291
292        if (resource.isOriginalResource()) {
293            varyValues.add(ACCEPT_DATETIME);
294        }
295
296        varyValues.forEach(x -> servletResponse.addHeader("Vary", x));
297    }
298
299
300
301
302
303    protected RdfStream getResourceTriples(final FedoraResource resource) {
304        return getResourceTriples(-1, resource);
305    }
306
307    /**
308     * This method returns a stream of RDF triples associated with this target resource
309     *
310     * @param limit is the number of child resources returned in the response, -1 for all
311     * @param resource the fedora resource
312     * @return {@link RdfStream}
313     */
314    private RdfStream getResourceTriples(final int limit, final FedoraResource resource) {
315
316        final PreferTag returnPreference;
317
318        if (prefer != null && prefer.hasReturn()) {
319            returnPreference = prefer.getReturn();
320        } else if (prefer != null && prefer.hasHandling()) {
321            returnPreference = prefer.getHandling();
322        } else {
323            returnPreference = PreferTag.emptyTag();
324        }
325
326        final LdpPreferTag ldpPreferences = new LdpPreferTag(returnPreference);
327
328        final Predicate<Triple> tripleFilter = ldpPreferences.prefersServerManaged() ? x -> true :
329            IS_MANAGED_TRIPLE.negate();
330
331        final List<Stream<Triple>> streams = new ArrayList<>();
332
333
334        if (returnPreference.getValue().equals("minimal")) {
335            streams.add(getTriples(resource, of(PROPERTIES, MINIMAL)).filter(tripleFilter));
336
337            // Mementos already have the server managed properties in the PROPERTIES category
338            // since mementos are immutable and these triples are no longer managed
339            if (ldpPreferences.prefersServerManaged() && !resource.isMemento())  {
340                streams.add(getTriples(resource, of(SERVER_MANAGED, MINIMAL)));
341            }
342        } else {
343            streams.add(getTriples(resource, PROPERTIES).filter(tripleFilter));
344
345            // Additional server-managed triples about this resource
346            // Mementos already have the server managed properties in the PROPERTIES category
347            // since mementos are immutable and these triples are no longer managed
348             if (ldpPreferences.prefersServerManaged() && !resource.isMemento()) {
349                streams.add(getTriples(resource, SERVER_MANAGED));
350            }
351
352            // containment triples about this resource
353            if (ldpPreferences.prefersContainment()) {
354                if (limit == -1) {
355                    streams.add(getTriples(resource, LDP_CONTAINMENT));
356                } else {
357                    streams.add(getTriples(resource, LDP_CONTAINMENT).limit(limit));
358                }
359            }
360
361            // LDP container membership triples for this resource
362            if (ldpPreferences.prefersMembership()) {
363                streams.add(getTriples(resource, LDP_MEMBERSHIP));
364            }
365
366            // Include inbound references to this object
367            if (ldpPreferences.prefersReferences()) {
368                streams.add(getTriples(resource, INBOUND_REFERENCES));
369            }
370
371            // Embed the children of this object
372            if (ldpPreferences.prefersEmbed()) {
373                streams.add(getTriples(resource, EMBED_RESOURCES));
374            }
375        }
376
377        final RdfStream rdfStream = new DefaultRdfStream(
378                asNode(resource), streams.stream().reduce(empty(), Stream::concat));
379
380        if (httpTripleUtil != null && ldpPreferences.prefersServerManaged()) {
381            return httpTripleUtil.addHttpComponentModelsForResourceToStream(rdfStream, resource, uriInfo,
382                    translator());
383        }
384
385        return rdfStream;
386    }
387
388    /**
389     * Get the binary content of a datastream
390     *
391     * @param rangeValue the range value
392     * @param resource the fedora resource
393     * @return Binary blob
394     * @throws IOException if io exception occurred
395     */
396    private Response getBinaryContent(final String rangeValue, final FedoraResource resource)
397            throws IOException {
398            final FedoraBinary binary = (FedoraBinary)resource;
399            final CacheControl cc = new CacheControl();
400            cc.setMaxAge(0);
401            cc.setMustRevalidate(true);
402            final Response.ResponseBuilder builder;
403
404            if (rangeValue != null && rangeValue.startsWith("bytes")) {
405
406                final Range range = Range.convert(rangeValue);
407
408                final long contentSize = binary.getContentSize();
409
410                final String endAsString;
411
412                if (range.end() == -1) {
413                    endAsString = Long.toString(contentSize - 1);
414                } else {
415                    endAsString = Long.toString(range.end());
416                }
417
418                final String contentRangeValue =
419                        String.format("bytes %s-%s/%s", range.start(),
420                                endAsString, contentSize);
421
422                if (range.end() > contentSize ||
423                        (range.end() == -1 && range.start() > contentSize)) {
424
425                    builder = status(REQUESTED_RANGE_NOT_SATISFIABLE)
426                            .header("Content-Range", contentRangeValue);
427                } else {
428                    @SuppressWarnings("resource")
429                    final RangeRequestInputStream rangeInputStream =
430                            new RangeRequestInputStream(binary.getContent(), range.start(), range.size());
431
432                    builder = status(PARTIAL_CONTENT).entity(rangeInputStream)
433                            .header("Content-Range", contentRangeValue)
434                            .header(CONTENT_LENGTH, range.size());
435                }
436
437            } else {
438                @SuppressWarnings("resource")
439                final InputStream content = binary.getContent();
440                builder = ok(content);
441            }
442
443
444            // we set the content-type explicitly to avoid content-negotiation from getting in the way
445            // getBinaryResourceMediaType will try to use the mime type on the resource, falling back on
446            // 'application/octet-stream' if the mime type is syntactically invalid
447            return builder.type(getBinaryResourceMediaType(resource).toString())
448                    .cacheControl(cc)
449                    .build();
450
451        }
452
453    private RdfStream getTriples(final FedoraResource resource, final Set<? extends TripleCategory> x) {
454        return resource.getTriples(translator(), x);
455    }
456
457    private RdfStream getTriples(final FedoraResource resource, final TripleCategory x) {
458        return resource.getTriples(translator(), x);
459    }
460
461    protected URI getUri(final FedoraResource resource) {
462        try {
463            final String uri = translator().reverse().convert(resource).getURI();
464            return new URI(uri);
465        } catch (final URISyntaxException e) {
466            throw new BadRequestException(e);
467        }
468    }
469
470    protected FedoraResource resource() {
471        if (fedoraResource == null) {
472            fedoraResource = getResourceFromPath(externalPath());
473        }
474        return fedoraResource;
475    }
476
477    /**
478     * Add the standard Accept-Post header, for reuse.
479     */
480    private void addAcceptPostHeader() {
481        final String rdfTypes = TURTLE + "," + N3 + "," + N3_ALT2 + "," + RDF_XML + "," + NTRIPLES + "," + JSON_LD;
482        servletResponse.addHeader("Accept-Post", rdfTypes);
483    }
484
485    /**
486     * Add the standard Accept-External-Content-Handling header, for reuse.
487     */
488    private void addAcceptExternalHeader() {
489        servletResponse.addHeader(ACCEPT_EXTERNAL_CONTENT, COPY + "," + REDIRECT + "," + PROXY);
490    }
491
492    private void addMementoHeaders(final FedoraResource resource) {
493        if (resource.isMemento()) {
494            final Instant mementoInstant = resource.getMementoDatetime();
495            if (mementoInstant != null) {
496                final String mementoDatetime = MEMENTO_RFC_1123_FORMATTER
497                        .format(mementoInstant.atZone(ZoneOffset.UTC));
498                servletResponse.addHeader(MEMENTO_DATETIME_HEADER, mementoDatetime);
499            }
500            servletResponse.addHeader(LINK, buildLink(MEMENTO_TYPE, "type"));
501        }
502    }
503
504    protected void addExternalContentHeaders(final FedoraResource resource) {
505        if (resource instanceof FedoraBinary) {
506            final FedoraBinary binary = (FedoraBinary)resource;
507
508            if (binary.isProxy()) {
509                servletResponse.addHeader(CONTENT_LOCATION, binary.getProxyURL());
510            } else if (binary.isRedirect()) {
511                servletResponse.addHeader(CONTENT_LOCATION, binary.getRedirectURL());
512            }
513        }
514    }
515
516    private void addAclHeader(final FedoraResource resource) {
517        if (!resource.isAcl()) {
518
519            final FedoraResource aclOwner;
520
521            if (resource.isTimeMap() || resource.isMemento()) {
522                aclOwner = resource.getOriginalResource().getDescribedResource();
523            } else {
524                aclOwner = resource.getDescribedResource();
525            }
526
527            final String aclOwnerUri = getUri(aclOwner).toString();
528            final String aclLocation = aclOwnerUri + (aclOwnerUri.endsWith("/") ? "" : "/") + FCR_ACL;
529            servletResponse.addHeader(LINK, buildLink(aclLocation, "acl"));
530        }
531    }
532
533    private void addResourceLinkHeaders(final FedoraResource resource) {
534        addResourceLinkHeaders(resource, false);
535    }
536
537    private void addResourceLinkHeaders(final FedoraResource resource, final boolean includeAnchor) {
538        if (resource instanceof NonRdfSourceDescription) {
539            // Link to the original described resource
540            final FedoraResource described = resource.getOriginalResource().getDescribedResource();
541            final URI uri = getUri(described);
542            final Link link = Link.fromUri(uri).rel("describes").build();
543            servletResponse.addHeader(LINK, link.toString());
544        } else if (resource instanceof FedoraBinary) {
545            // Link to the original description
546            final FedoraResource description = resource.getOriginalResource().getDescription();
547            final URI uri = getUri(description);
548            final Link.Builder builder = Link.fromUri(uri).rel("describedby");
549
550            if (includeAnchor) {
551                builder.param("anchor", getUri(resource).toString());
552            }
553            servletResponse.addHeader(LINK, builder.build().toString());
554        }
555
556        final boolean isOriginal = resource.isOriginalResource();
557        // Add versioning headers for versioned originals and mementos
558        if (isOriginal || resource.isMemento() || resource instanceof FedoraTimeMap) {
559            final URI originalUri = getUri(resource.getOriginalResource());
560            try {
561                final URI timemapUri = getUri(resource.getTimeMap());
562                servletResponse.addHeader(LINK, buildLink(originalUri, "timegate"));
563                servletResponse.addHeader(LINK, buildLink(originalUri, "original"));
564                servletResponse.addHeader(LINK, buildLink(timemapUri, "timemap"));
565
566                if (isOriginal) {
567                    servletResponse.addHeader(LINK, buildLink(VERSIONED_RESOURCE.getURI(), "type"));
568                    servletResponse.addHeader(LINK, buildLink(VERSIONING_TIMEGATE_TYPE, "type"));
569                } else if (resource instanceof FedoraTimeMap) {
570                    servletResponse.addHeader(LINK, buildLink(VERSIONING_TIMEMAP_TYPE, "type"));
571                }
572            } catch (final PathNotFoundRuntimeException e) {
573                LOGGER.debug("TimeMap not found for {}, resource not versioned", getUri(resource));
574            }
575        }
576
577        // Add user-provided types as Link headers... when a description exists
578        final FedoraResource resourceDescription = resource.getDescription();
579        if (resourceDescription != null) {
580            for (final URI typeURI : resourceDescription.getTypes()) {
581
582                // Get namespace of type
583                final String type = typeURI.toString();
584                final String namespace = createURI(type).getNameSpace();
585
586                // Omit server-managed types, as they are added elsewhere
587                if (!isManagedNamespace.test(namespace)) {
588                    servletResponse.addHeader(LINK, buildLink(type, "type"));
589                }
590            }
591        }
592    }
593
594    /**
595     * Add Link and Option headers
596     *
597     * @param resource the resource to generate headers for
598     */
599    protected void addLinkAndOptionsHttpHeaders(final FedoraResource resource) {
600        // Add Link headers
601        addResourceLinkHeaders(resource);
602        addAcceptExternalHeader();
603
604        // Add Options headers
605        final String options;
606        if (resource.isMemento()) {
607            options = "GET,HEAD,OPTIONS,DELETE";
608        } else if (resource instanceof FedoraTimeMap) {
609            options = "POST,HEAD,GET,OPTIONS";
610            servletResponse.addHeader("Vary-Post", MEMENTO_DATETIME_HEADER);
611            addAcceptPostHeader();
612        } else if (resource instanceof FedoraBinary) {
613            options = "DELETE,HEAD,GET,PUT,OPTIONS";
614        } else if (resource instanceof NonRdfSourceDescription) {
615            options = "HEAD,GET,DELETE,PUT,PATCH,OPTIONS";
616            servletResponse.addHeader(HTTP_HEADER_ACCEPT_PATCH, contentTypeSPARQLUpdate);
617        } else if (resource instanceof Container) {
618            options = "MOVE,COPY,DELETE,POST,HEAD,GET,PUT,PATCH,OPTIONS";
619            servletResponse.addHeader(HTTP_HEADER_ACCEPT_PATCH, contentTypeSPARQLUpdate);
620            addAcceptPostHeader();
621        } else {
622            options = "";
623        }
624
625        servletResponse.addHeader("Allow", options);
626    }
627
628    /**
629     * Utility function for building a Link.
630     *
631     * @param linkUri String of URI for the link.
632     * @param relation the relation string.
633     * @return the string version of the link.
634     */
635    protected static String buildLink(final String linkUri, final String relation) {
636        return buildLink(URI.create(linkUri), relation);
637    }
638
639    /**
640     * Utility function for building a Link.
641     *
642     * @param linkUri The URI for the link.
643     * @param relation the relation string.
644     * @return the string version of the link.
645     */
646    private static String buildLink(final URI linkUri, final String relation) {
647        return Link.fromUri(linkUri).rel(relation).build().toString();
648    }
649
650    /**
651     * Multi-value Link header values parsed by the javax.ws.rs.core are not split out by the framework Therefore we
652     * must do this ourselves.
653     *
654     * @param rawLinks the list of unprocessed links
655     * @return List of strings containing one link value per string.
656     */
657    protected List<String> unpackLinks(final List<String> rawLinks) {
658        if (rawLinks == null) {
659            return null;
660        }
661
662        return rawLinks.stream()
663                .flatMap(x -> Arrays.stream(x.split(",")))
664                .collect(Collectors.toList());
665    }
666
667    /**
668     * Add any resource-specific headers to the response
669     * @param resource the resource
670     */
671    protected void addResourceHttpHeaders(final FedoraResource resource) {
672        if (resource instanceof FedoraBinary) {
673            final FedoraBinary binary = (FedoraBinary)resource;
674            final Date createdDate = binary.getCreatedDate() != null ? Date.from(binary.getCreatedDate()) : null;
675            final Date modDate = binary.getLastModifiedDate() != null ? Date.from(binary.getLastModifiedDate()) : null;
676
677            final ContentDisposition contentDisposition = ContentDisposition.type("attachment")
678                    .fileName(binary.getFilename())
679                    .creationDate(createdDate)
680                    .modificationDate(modDate)
681                    .size(binary.getContentSize())
682                    .build();
683
684            servletResponse.addHeader(CONTENT_TYPE, binary.getMimeType());
685            // Returning content-length > 0 causes the client to wait for additional data before following the redirect.
686            if (!binary.isRedirect()) {
687                servletResponse.addHeader(CONTENT_LENGTH, String.valueOf(binary.getContentSize()));
688            }
689            servletResponse.addHeader("Accept-Ranges", "bytes");
690            servletResponse.addHeader(CONTENT_DISPOSITION, contentDisposition.toString());
691        }
692
693        servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "Resource>;rel=\"type\"");
694
695        if (resource instanceof FedoraBinary) {
696            servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "NonRDFSource>;rel=\"type\"");
697        } else if (resource instanceof Container || resource instanceof FedoraTimeMap) {
698            servletResponse.addHeader(LINK, "<" + CONTAINER.getURI() + ">;rel=\"type\"");
699            servletResponse.addHeader(LINK, buildLink(RDF_SOURCE.getURI(), "type"));
700            if (resource.hasType(LDP_BASIC_CONTAINER)) {
701                servletResponse.addHeader(LINK, "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\"");
702            } else if (resource.hasType(LDP_DIRECT_CONTAINER)) {
703                servletResponse.addHeader(LINK, "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\"");
704            } else if (resource.hasType(LDP_INDIRECT_CONTAINER)) {
705                servletResponse.addHeader(LINK, "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\"");
706            } else {
707                servletResponse.addHeader(LINK, "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\"");
708            }
709        } else {
710            servletResponse.addHeader(LINK, buildLink(RDF_SOURCE.getURI(), "type"));
711        }
712        if (httpHeaderInject != null) {
713            httpHeaderInject.addHttpHeaderToResponseStream(servletResponse, uriInfo, resource);
714        }
715
716        addLinkAndOptionsHttpHeaders(resource);
717        addAclHeader(resource);
718        addMementoHeaders(resource);
719    }
720
721    /**
722     * Evaluate the cache control headers for the request to see if it can be served from
723     * the cache.
724     *
725     * @param request the request
726     * @param servletResponse the servlet response
727     * @param resource the fedora resource
728     * @param session the session
729     */
730    protected void checkCacheControlHeaders(final Request request,
731                                                   final HttpServletResponse servletResponse,
732                                                   final FedoraResource resource,
733                                                   final HttpSession session) {
734        evaluateRequestPreconditions(request, servletResponse, resource, session, true);
735        addCacheControlHeaders(servletResponse, resource, session);
736    }
737
738    /**
739     * Add ETag and Last-Modified cache control headers to the response
740     * <p>
741     * Note: In this implementation, the HTTP headers for ETags and Last-Modified dates are swapped
742     * for fedora:Binary resources and their descriptions. Here, we are drawing a distinction between
743     * the HTTP resource and the LDP resource. As an HTTP resource, the last-modified header should
744     * reflect when the resource at the given URL was last changed. With fedora:Binary resources and
745     * their descriptions, this is a little complicated, for the descriptions have, as their subjects,
746     * the binary itself. And the fedora:lastModified property produced by that NonRdfSourceDescription
747     * refers to the last-modified date of the binary -- not the last-modified date of the
748     * NonRdfSourceDescription.
749     * </p>
750     * @param servletResponse the servlet response
751     * @param resource the fedora resource
752     * @param session the session
753     */
754    protected void addCacheControlHeaders(final HttpServletResponse servletResponse,
755                                                 final FedoraResource resource,
756                                                 final HttpSession session) {
757
758        if (session.isBatchSession()) {
759            // Do not add caching headers if in a transaction
760            return;
761        }
762
763        final EntityTag etag;
764        final Instant date;
765
766        // See note about this code in the javadoc above.
767        if (resource instanceof FedoraBinary) {
768            // Use a strong ETag for LDP-NR
769            etag = new EntityTag(resource.getEtagValue());
770            date = resource.getLastModifiedDate();
771        } else {
772            // Use a weak ETag for the LDP-RS
773            etag = new EntityTag(resource.getEtagValue(), true);
774            date = resource.getLastModifiedDate();
775        }
776
777        if (!etag.getValue().isEmpty()) {
778            servletResponse.addHeader("ETag", etag.toString());
779        }
780
781        if (!resource.getStateToken().isEmpty()) {
782            //State Tokens, while not used for caching per se,  nevertheless belong
783            //here since we can conveniently reuse the value of the etag for
784            //our state token
785            servletResponse.addHeader("X-State-Token", etag.getValue());
786        }
787
788        if (date != null) {
789            servletResponse.addDateHeader("Last-Modified", date.toEpochMilli());
790        }
791    }
792
793    /**
794     * Evaluate request preconditions to ensure the resource is the expected state
795     * @param request the request
796     * @param servletResponse the servlet response
797     * @param resource the resource
798     * @param session the session
799     */
800    protected void evaluateRequestPreconditions(final Request request,
801                                                       final HttpServletResponse servletResponse,
802                                                       final FedoraResource resource,
803                                                       final HttpSession session) {
804        evaluateRequestPreconditions(request, servletResponse, resource, session, false);
805    }
806
807    @VisibleForTesting
808    void evaluateRequestPreconditions(final Request request,
809                                                     final HttpServletResponse servletResponse,
810                                                     final FedoraResource resource,
811                                                     final HttpSession session,
812                                                     final boolean cacheControl) {
813
814        if (session.isBatchSession()) {
815            // Force cache revalidation if in a transaction
816            servletResponse.addHeader(CACHE_CONTROL, "must-revalidate");
817            servletResponse.addHeader(CACHE_CONTROL, "max-age=0");
818            return;
819        }
820
821        final EntityTag etag;
822        final Instant date;
823        Instant roundedDate = Instant.now();
824
825        // See the related note about the next block of code in the
826        // ContentExposingResource::addCacheControlHeaders method
827        if (resource instanceof FedoraBinary) {
828            // Use a strong ETag for the LDP-NR
829            etag = new EntityTag(resource.getEtagValue());
830            date = resource.getLastModifiedDate();
831        } else {
832            // Use a strong ETag for the LDP-RS when validating If-(None)-Match headers
833            etag = new EntityTag(resource.getEtagValue());
834            date = resource.getLastModifiedDate();
835        }
836
837        if (date != null) {
838            roundedDate = date.minusMillis(date.toEpochMilli() % 1000);
839        }
840
841        Response.ResponseBuilder builder = request.evaluatePreconditions(etag);
842        if ( builder == null ) {
843            builder = request.evaluatePreconditions(Date.from(roundedDate));
844        }
845
846        if (builder != null && cacheControl ) {
847            final CacheControl cc = new CacheControl();
848            cc.setMaxAge(0);
849            cc.setMustRevalidate(true);
850            // here we are implicitly emitting a 304
851            // the exception is not an error, it's genuinely
852            // an exceptional condition
853            builder = builder.cacheControl(cc).lastModified(Date.from(roundedDate)).tag(etag);
854        }
855
856        if (builder != null) {
857            final Response response = builder.build();
858            final Object message = response.getEntity();
859            throw new PreconditionException(message != null ? message.toString()
860                    : "Request failed due to unspecified failed precondition.", response.getStatus());
861        }
862
863        final String method = request.getMethod();
864        if (method.equals(HttpPut.METHOD_NAME) || method.equals(HttpPatch.METHOD_NAME)) {
865            final String stateToken = resource.getStateToken();
866            final String clientSuppliedStateToken = headers.getHeaderString("X-If-State-Token");
867            if (clientSuppliedStateToken != null && !stateToken.equals(clientSuppliedStateToken)) {
868                throw new PreconditionException(format(
869                    "The client-supplied value ({0}) does not match the current state token ({1}).",
870                    clientSuppliedStateToken, stateToken), 412);
871            }
872        }
873    }
874
875    /**
876     * Returns an acceptable plain text media type if possible, or null if not.
877     * @return an acceptable plain-text media type, or null
878     */
879    private MediaType acceptabePlainTextMediaType() {
880        final List<MediaType> acceptable = headers.getAcceptableMediaTypes();
881        if (acceptable == null || acceptable.size() == 0) {
882            return TEXT_PLAIN_TYPE;
883        }
884        for (final MediaType type : acceptable) {
885            if (type.isWildcardType() || (type.isCompatible(TEXT_PLAIN_TYPE) && type.isWildcardSubtype())) {
886                return TEXT_PLAIN_TYPE;
887            } else if (type.isCompatible(TEXT_PLAIN_TYPE)) {
888                return type;
889            }
890        }
891        return null;
892    }
893
894    /**
895     * Create the appropriate response after a create or update request is processed. When a resource is created,
896     * examine the Prefer and Accept headers to determine whether to include a representation. By default, the URI for
897     * the created resource is return as plain text. If a minimal response is requested, then no body is returned. If a
898     * non-minimal return is requested, return the RDF for the created resource in the appropriate RDF serialization.
899     *
900     * @param resource The created or updated Fedora resource.
901     * @param created True for a newly-created resource, false for an updated resource.
902     * @return 204 No Content (for updated resources), 201 Created (for created resources) including the resource URI or
903     *         content depending on Prefer headers.
904     */
905    @SuppressWarnings("resource")
906    protected Response createUpdateResponse(final FedoraResource resource, final boolean created) {
907        addCacheControlHeaders(servletResponse, resource, session);
908        addResourceLinkHeaders(resource, created);
909        addExternalContentHeaders(resource);
910        addAclHeader(resource);
911        addMementoHeaders(resource);
912
913        if (!created) {
914            return noContent().build();
915        }
916
917        final URI location = getUri(resource);
918        final Response.ResponseBuilder builder = created(location);
919
920        if (prefer == null || !prefer.hasReturn()) {
921            final MediaType acceptablePlainText = acceptabePlainTextMediaType();
922            if (acceptablePlainText != null) {
923                return builder.type(acceptablePlainText).entity(location.toString()).build();
924            }
925            return notAcceptable(mediaTypes(TEXT_PLAIN_TYPE).build()).build();
926        } else if (prefer.getReturn().getValue().equals("minimal")) {
927            return builder.build();
928        } else {
929            if (prefer != null) {
930                prefer.getReturn().addResponseHeaders(servletResponse);
931            }
932            final RdfNamespacedStream rdfStream = new RdfNamespacedStream(
933                new DefaultRdfStream(asNode(resource), getResourceTriples(resource)),
934                namespaceRegistry.getNamespaces());
935            return builder.entity(rdfStream).build();
936        }
937    }
938
939    protected static MediaType getSimpleContentType(final MediaType requestContentType) {
940        return requestContentType != null ?
941                new MediaType(requestContentType.getType(), requestContentType.getSubtype())
942                : APPLICATION_OCTET_STREAM_TYPE;
943    }
944
945    protected static boolean isRdfContentType(final String contentTypeString) {
946        return contentTypeToLang(contentTypeString) != null;
947    }
948
949    protected void replaceResourceBinaryWithStream(final FedoraBinary result,
950                                                   final InputStream requestBodyStream,
951                                                   final ContentDisposition contentDisposition,
952                                                   final MediaType contentType,
953                                                   final Collection<String> checksums,
954                                                   final String externalHandling,
955                                                   final String externalUrl) throws InvalidChecksumException {
956        final Collection<URI> checksumURIs = checksums == null ?
957                new HashSet<>() : checksums.stream().map(checksum -> checksumURI(checksum)).collect(Collectors.toSet());
958        final String originalFileName = contentDisposition != null ? contentDisposition.getFileName() : "";
959        final String originalContentType = contentType != null ? contentType.toString() : "";
960
961        if (externalHandling != null) {
962            result.setExternalContent(originalContentType,
963                    checksumURIs,
964                    originalFileName,
965                    externalHandling,
966                    externalUrl);
967        } else {
968            result.setContent(requestBodyStream,
969                    originalContentType,
970                    checksumURIs,
971                    originalFileName,
972                    storagePolicyDecisionPoint);
973        }
974    }
975
976    protected void replaceResourceWithStream(final FedoraResource resource,
977                                             final InputStream requestBodyStream,
978                                             final MediaType contentType,
979                                             final RdfStream resourceTriples) throws MalformedRdfException {
980        final Model inputModel = parseBodyAsModel(requestBodyStream, contentType, resource);
981
982        ensureValidMemberRelation(inputModel);
983
984        ensureValidACLAuthorization(resource, inputModel);
985
986        resource.replaceProperties(translator(), inputModel, resourceTriples);
987    }
988
989    /**
990     * Parse the request body as a Model.
991     *
992     * @param requestBodyStream rdf request body
993     * @param contentType content type of body
994     * @param resource the fedora resource
995     * @return Model containing triples from request body
996     * @throws MalformedRdfException in case rdf json cannot be parsed
997     */
998    private Model parseBodyAsModel(final InputStream requestBodyStream,
999            final MediaType contentType, final FedoraResource resource) throws MalformedRdfException {
1000        final Lang format = contentTypeToLang(contentType.toString());
1001
1002        final Model inputModel;
1003        try {
1004            inputModel = createDefaultModel();
1005            inputModel.read(requestBodyStream, getUri(resource).toString(), format.getName().toUpperCase());
1006            return inputModel;
1007        } catch (final RiotException e) {
1008            throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e);
1009
1010        } catch (final RuntimeIOException e) {
1011            if (e.getCause() instanceof JsonParseException) {
1012                throw new MalformedRdfException(e.getCause());
1013            }
1014            throw new RepositoryRuntimeException(e);
1015        }
1016    }
1017
1018    /**
1019     * This method throws an exception if the arg model contains a triple with 'ldp:hasMemberRelation' as a predicate
1020     *   and a server-managed property as the object.
1021     *
1022     * @param inputModel to be checked
1023     * @throws ServerManagedPropertyException on error
1024     */
1025    private void ensureValidMemberRelation(final Model inputModel) {
1026        // check that ldp:hasMemberRelation value is not server managed predicate.
1027        inputModel.listStatements().forEachRemaining((final Statement s) -> {
1028            LOGGER.debug("statement: s={}, p={}, o={}", s.getSubject(), s.getPredicate(), s.getObject());
1029
1030            if (s.getPredicate().equals(HAS_MEMBER_RELATION)) {
1031                final RDFNode obj = s.getObject();
1032                if (obj.isURIResource()) {
1033                    final String uri = obj.asResource().getURI();
1034
1035                    // Throw exception if object is a server-managed property
1036                    if (isManagedPredicate.test(createProperty(uri))) {
1037                            throw new ServerManagedPropertyException(
1038                                    format(
1039                                            "{0} cannot take a server managed property " +
1040                                                    "as an object: property value = {1}.",
1041                                            HAS_MEMBER_RELATION, uri));
1042                    }
1043                }
1044            }
1045        });
1046    }
1047
1048    /**
1049     * This method does two things:
1050     * - Throws an exception if an authorization has both accessTo and accessToClass
1051     * - Adds a default accessTo target if an authorization has neither accessTo nor accessToClass
1052     * 
1053     * @param resource the fedora resource
1054     * @param inputModel to be checked and updated
1055     */
1056    private void ensureValidACLAuthorization(final FedoraResource resource, final Model inputModel) {
1057        if (resource.isAcl()) {
1058            final Set<Node> uniqueAuthSubjects = new HashSet<>();
1059            inputModel.listStatements().forEachRemaining((final Statement s) -> {
1060                LOGGER.debug("statement: s={}, p={}, o={}", s.getSubject(), s.getPredicate(), s.getObject());
1061                final Node subject = s.getSubject().asNode();
1062                // If subject is Authorization Hash Resource, add it to the map with its accessTo/accessToClass status.
1063                if (subject.toString().contains("/" + FCR_ACL + "#")) {
1064                    uniqueAuthSubjects.add(subject);
1065                }
1066            });
1067            final Graph graph = inputModel.getGraph();
1068            uniqueAuthSubjects.forEach((final Node subject) -> {
1069                if (graph.contains(subject, WEBAC_ACCESS_TO_URI, Node.ANY) &&
1070                        graph.contains(subject, WEBAC_ACCESS_TO_CLASS_URI, Node.ANY)) {
1071                    throw new ACLAuthorizationConstraintViolationException(
1072                        format(
1073                                "Using both accessTo and accessToClass within " +
1074                                        "a single Authorization is not allowed: {0}.",
1075                                subject.toString().substring(subject.toString().lastIndexOf("#"))));
1076                } else if (!(graph.contains(subject, WEBAC_ACCESS_TO_URI, Node.ANY) ||
1077                        graph.contains(subject, WEBAC_ACCESS_TO_CLASS_URI, Node.ANY))) {
1078                    inputModel.add(createDefaultAccessToStatement(subject.toString()));
1079                }
1080            });
1081        }
1082    }
1083
1084    /**
1085     * Returns a Statement with the resource containing the acl to be the accessTo target for the given auth subject.
1086     * 
1087     * @param authSubject - acl authorization subject uri string
1088     * @return acl statement
1089     */
1090    private Statement createDefaultAccessToStatement(final String authSubject) {
1091        final String currentResourcePath = authSubject.substring(0, authSubject.indexOf("/" + FCR_ACL));
1092        return createStatement(
1093                        createResource(authSubject),
1094                        WEBAC_ACCESS_TO_PROPERTY,
1095                        createResource(currentResourcePath));
1096    }
1097
1098    protected void patchResourcewithSparql(final FedoraResource resource,
1099            final String requestBody,
1100            final RdfStream resourceTriples) {
1101        resource.updateProperties(translator(), requestBody, resourceTriples);
1102    }
1103
1104    /**
1105     * This method returns a MediaType for a binary resource.
1106     * If the resource's media type is syntactically incorrect, it will
1107     * return 'application/octet-stream' as the media type.
1108     * @param  resource the fedora resource
1109     * @return the media type of of a binary resource
1110     */
1111    protected MediaType getBinaryResourceMediaType(final FedoraResource resource) {
1112        try {
1113            return MediaType.valueOf(((FedoraBinary) resource).getMimeType());
1114        } catch (final IllegalArgumentException e) {
1115            LOGGER.warn("Syntactically incorrect MediaType encountered on resource {}: '{}'",
1116                    resource.getPath(), ((FedoraBinary)resource).getMimeType());
1117            return MediaType.APPLICATION_OCTET_STREAM_TYPE;
1118        }
1119    }
1120
1121    /**
1122     * Create a checksum URI object.
1123     **/
1124    protected static URI checksumURI( final String checksum ) {
1125        if (!isBlank(checksum)) {
1126            return URI.create(checksum);
1127        }
1128        return null;
1129    }
1130
1131    /**
1132     * Calculate the max number of children to display at once.
1133     *
1134     * @return the limit of children to display.
1135     */
1136    protected int getChildrenLimit() {
1137        final List<String> acceptHeaders = headers.getRequestHeader(ACCEPT);
1138        if (acceptHeaders != null && acceptHeaders.size() > 0) {
1139            final List<String> accept = Arrays.asList(acceptHeaders.get(0).split(","));
1140            if (accept.contains(TEXT_HTML)) {
1141                // Magic number '100' is tied to common-metadata.vsl display of ellipses
1142                return 100;
1143            }
1144        }
1145
1146        final List<String> limits = headers.getRequestHeader("Limit");
1147        if (null != limits && limits.size() > 0) {
1148            try {
1149                return Integer.parseInt(limits.get(0));
1150
1151            } catch (final NumberFormatException e) {
1152                LOGGER.warn("Invalid 'Limit' header value: {}", limits.get(0));
1153                throw new ClientErrorException("Invalid 'Limit' header value: " + limits.get(0), SC_BAD_REQUEST, e);
1154            }
1155        }
1156        return -1;
1157    }
1158
1159    /**
1160     * Check if a path has a segment prefixed with fedora:
1161     *
1162     * @param externalPath the path.
1163     */
1164    protected static void hasRestrictedPath(final String externalPath) {
1165        final String[] pathSegments = externalPath.split("/");
1166        if (Arrays.stream(pathSegments).anyMatch(p -> p.startsWith("fedora:"))) {
1167            throw new ServerManagedTypeException("Path cannot contain a fedora: prefixed segment.");
1168        }
1169    }
1170
1171    /**
1172     * Parse the RFC-3230 Digest response header value. Look for a sha1 checksum and return it as a urn, if missing or
1173     * malformed an empty string is returned.
1174     *
1175     * @param digest The Digest header value
1176     * @return the sha1 checksum value
1177     * @throws UnsupportedAlgorithmException if an unsupported digest is used
1178     */
1179    protected static Collection<String> parseDigestHeader(final String digest) throws UnsupportedAlgorithmException {
1180        try {
1181            final Map<String, String> digestPairs = RFC3230_SPLITTER.split(nullToEmpty(digest));
1182            final boolean allSupportedAlgorithms = digestPairs.keySet().stream().allMatch(
1183                ContentDigest.DIGEST_ALGORITHM::isSupportedAlgorithm);
1184
1185            // If you have one or more digests that are all valid or no digests.
1186            if (digestPairs.isEmpty() || allSupportedAlgorithms) {
1187                return digestPairs.entrySet().stream()
1188                    .filter(entry -> ContentDigest.DIGEST_ALGORITHM.isSupportedAlgorithm(entry.getKey()))
1189                    .map(entry -> ContentDigest.asURI(entry.getKey(), entry.getValue()).toString())
1190                    .collect(Collectors.toSet());
1191            } else {
1192                throw new UnsupportedAlgorithmException(String.format("Unsupported Digest Algorithim: %1$s", digest));
1193            }
1194        } catch (final RuntimeException e) {
1195            if (e instanceof IllegalArgumentException) {
1196                throw new ClientErrorException("Invalid Digest header: " + digest + "\n", BAD_REQUEST);
1197            }
1198            throw e;
1199        }
1200    }
1201
1202    /**
1203     * @param rootThrowable The original throwable
1204     * @param throwable The throwable under direct scrutiny.
1205     * @throws InvalidChecksumException in case there was a checksum mismatch
1206     */
1207    protected void checkForInsufficientStorageException(final Throwable rootThrowable, final Throwable throwable)
1208        throws InvalidChecksumException {
1209        final String message = throwable.getMessage();
1210        if (throwable instanceof IOException && message != null && message.contains(
1211            INSUFFICIENT_SPACE_IDENTIFYING_MESSAGE)) {
1212            throw new InsufficientStorageException(throwable.getMessage(), rootThrowable);
1213        }
1214
1215        if (throwable.getCause() != null) {
1216            checkForInsufficientStorageException(rootThrowable, throwable.getCause());
1217        }
1218
1219        if (rootThrowable instanceof InvalidChecksumException) {
1220            throw (InvalidChecksumException) rootThrowable;
1221        } else if (rootThrowable instanceof RuntimeException) {
1222            throw (RuntimeException) rootThrowable;
1223        } else {
1224            throw new RepositoryRuntimeException(rootThrowable);
1225        }
1226    }
1227}