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}