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