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