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