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}