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 020 021import static com.google.common.base.Strings.nullToEmpty; 022import static java.nio.charset.StandardCharsets.UTF_8; 023import static javax.ws.rs.core.MediaType.TEXT_HTML; 024import static javax.ws.rs.core.MediaType.TEXT_PLAIN; 025import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE; 026import static javax.ws.rs.core.MediaType.WILDCARD; 027import static javax.ws.rs.core.HttpHeaders.ACCEPT; 028import static javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION; 029import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; 030import static javax.ws.rs.core.HttpHeaders.LINK; 031import static javax.ws.rs.core.Response.created; 032import static javax.ws.rs.core.Response.noContent; 033import static javax.ws.rs.core.Response.notAcceptable; 034import static javax.ws.rs.core.Response.ok; 035import static javax.ws.rs.core.Response.temporaryRedirect; 036import static javax.ws.rs.core.Response.Status.BAD_REQUEST; 037import static javax.ws.rs.core.Response.Status.CONFLICT; 038import static javax.ws.rs.core.Response.Status.FORBIDDEN; 039import static javax.ws.rs.core.Response.Status.NOT_IMPLEMENTED; 040import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE; 041import static javax.ws.rs.core.Variant.mediaTypes; 042import static org.apache.commons.lang3.StringUtils.isBlank; 043import static org.apache.http.HttpStatus.SC_BAD_REQUEST; 044import static org.apache.jena.rdf.model.ResourceFactory.createResource; 045import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate; 046import static org.fcrepo.http.commons.domain.RDFMediaType.JSON_LD; 047import static org.fcrepo.http.commons.domain.RDFMediaType.N3; 048import static org.fcrepo.http.commons.domain.RDFMediaType.N3_WITH_CHARSET; 049import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2; 050import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2_WITH_CHARSET; 051import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES; 052import static org.fcrepo.http.commons.domain.RDFMediaType.RDF_XML; 053import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_PLAIN_WITH_CHARSET; 054import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_HTML_WITH_CHARSET; 055import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE; 056import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_WITH_CHARSET; 057import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_X; 058import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_BINARY; 059import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_CONTAINER; 060import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_PAIRTREE; 061import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE; 062import static org.slf4j.LoggerFactory.getLogger; 063 064import java.io.IOException; 065import java.io.InputStream; 066import java.io.UnsupportedEncodingException; 067import java.net.URI; 068import java.net.URLDecoder; 069import java.util.Arrays; 070import java.util.Collection; 071import java.util.List; 072import java.util.Map; 073import java.util.stream.Collectors; 074import javax.annotation.PostConstruct; 075import javax.inject.Inject; 076import javax.ws.rs.BadRequestException; 077import javax.ws.rs.ClientErrorException; 078import javax.ws.rs.Consumes; 079import javax.ws.rs.DELETE; 080import javax.ws.rs.GET; 081import javax.ws.rs.HEAD; 082import javax.ws.rs.HeaderParam; 083import javax.ws.rs.OPTIONS; 084import javax.ws.rs.POST; 085import javax.ws.rs.PUT; 086import javax.ws.rs.Path; 087import javax.ws.rs.PathParam; 088import javax.ws.rs.Produces; 089import javax.ws.rs.ServerErrorException; 090import javax.ws.rs.core.Link; 091import javax.ws.rs.core.MediaType; 092import javax.ws.rs.core.Response; 093import javax.ws.rs.core.UriBuilderException; 094import javax.ws.rs.core.Variant.VariantListBuilder; 095 096import org.apache.commons.io.IOUtils; 097import org.apache.commons.lang3.StringUtils; 098 099import org.fcrepo.http.api.PathLockManager.AcquiredLock; 100import org.fcrepo.http.commons.domain.ContentLocation; 101import org.fcrepo.http.commons.domain.PATCH; 102import org.fcrepo.http.commons.responses.RdfNamespacedStream; 103import org.fcrepo.kernel.api.RdfStream; 104import org.fcrepo.kernel.api.exception.AccessDeniedException; 105import org.fcrepo.kernel.api.exception.InsufficientStorageException; 106import org.fcrepo.kernel.api.exception.InvalidChecksumException; 107import org.fcrepo.kernel.api.exception.MalformedRdfException; 108import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException; 109import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 110import org.fcrepo.kernel.api.models.Container; 111import org.fcrepo.kernel.api.models.FedoraBinary; 112import org.fcrepo.kernel.api.models.FedoraResource; 113import org.fcrepo.kernel.api.models.NonRdfSourceDescription; 114import org.fcrepo.kernel.api.rdf.DefaultRdfStream; 115import org.fcrepo.kernel.api.utils.ContentDigest; 116import org.glassfish.jersey.media.multipart.ContentDisposition; 117import org.slf4j.Logger; 118import org.springframework.context.annotation.Scope; 119 120import com.codahale.metrics.annotation.Timed; 121import com.google.common.annotations.VisibleForTesting; 122import com.google.common.base.Splitter; 123import com.google.common.collect.ImmutableList; 124 125/** 126 * @author cabeer 127 * @author ajs6f 128 * @since 9/25/14 129 */ 130 131@Scope("request") 132@Path("/{path: .*}") 133public class FedoraLdp extends ContentExposingResource { 134 135 private static final Logger LOGGER = getLogger(FedoraLdp.class); 136 137 private static final Splitter.MapSplitter RFC3230_SPLITTER = 138 Splitter.on(',').omitEmptyStrings().trimResults(). 139 withKeyValueSeparator(Splitter.on('=').limit(2)); 140 141 static final String INSUFFICIENT_SPACE_IDENTIFYING_MESSAGE = "No space left on device"; 142 143 static final String HTTP_HEADER_ACCEPT_PATCH = "Accept-Patch"; 144 145 @PathParam("path") protected String externalPath; 146 147 @Inject private FedoraHttpConfiguration httpConfiguration; 148 149 /** 150 * Default JAX-RS entry point 151 */ 152 public FedoraLdp() { 153 super(); 154 } 155 156 /** 157 * Create a new FedoraNodes instance for a given path 158 * @param externalPath the external path 159 */ 160 @VisibleForTesting 161 public FedoraLdp(final String externalPath) { 162 this.externalPath = externalPath; 163 } 164 165 /** 166 * Run these actions after initializing this resource 167 */ 168 @PostConstruct 169 public void postConstruct() { 170 setUpJMSInfo(uriInfo, headers); 171 } 172 173 /** 174 * Retrieve the node headers 175 * @return response 176 */ 177 @HEAD 178 @Timed 179 public Response head() { 180 LOGGER.info("HEAD for: {}", externalPath); 181 182 checkCacheControlHeaders(request, servletResponse, resource(), session); 183 184 addResourceHttpHeaders(resource()); 185 186 Response.ResponseBuilder builder = ok(); 187 188 if (resource() instanceof FedoraBinary) { 189 final MediaType mediaType = MediaType.valueOf(((FedoraBinary) resource()).getMimeType()); 190 191 if (isExternalBody(mediaType)) { 192 builder = temporaryRedirect(URI.create(mediaType.getParameters().get("URL"))); 193 } 194 195 // we set the content-type explicitly to avoid content-negotiation from getting in the way 196 builder.type(mediaType.toString()); 197 } 198 199 return builder.build(); 200 } 201 202 /** 203 * Outputs information about the supported HTTP methods, etc. 204 * @return the outputs information about the supported HTTP methods, etc. 205 */ 206 @OPTIONS 207 @Timed 208 public Response options() { 209 LOGGER.info("OPTIONS for '{}'", externalPath); 210 addLinkAndOptionsHttpHeaders(); 211 return ok().build(); 212 } 213 214 215 /** 216 * Retrieve the node profile 217 * 218 * @param rangeValue the range value 219 * @return a binary or the triples for the specified node 220 * @throws IOException if IO exception occurred 221 */ 222 @GET 223 @Produces({TURTLE_WITH_CHARSET + ";qs=1.0", JSON_LD + ";qs=0.8", 224 N3_WITH_CHARSET, N3_ALT2_WITH_CHARSET, RDF_XML, NTRIPLES, TEXT_PLAIN_WITH_CHARSET, 225 TURTLE_X, TEXT_HTML_WITH_CHARSET}) 226 public Response getResource(@HeaderParam("Range") final String rangeValue) throws IOException { 227 checkCacheControlHeaders(request, servletResponse, resource(), session); 228 229 LOGGER.info("GET resource '{}'", externalPath); 230 final AcquiredLock readLock = lockManager.lockForRead(resource().getPath()); 231 try (final RdfStream rdfStream = new DefaultRdfStream(asNode(resource()))) { 232 233 // If requesting a binary, check the mime-type if "Accept:" header is present. 234 // (This needs to be done before setting up response headers, as getContent 235 // returns a response - so changing headers after that won't work so nicely.) 236 final ImmutableList<MediaType> acceptableMediaTypes = ImmutableList.copyOf(headers 237 .getAcceptableMediaTypes()); 238 239 if (resource() instanceof FedoraBinary && acceptableMediaTypes.size() > 0) { 240 final MediaType mediaType = MediaType.valueOf(((FedoraBinary) resource()).getMimeType()); 241 242 if (!acceptableMediaTypes.stream().anyMatch(t -> t.isCompatible(mediaType))) { 243 return notAcceptable(VariantListBuilder.newInstance().mediaTypes(mediaType).build()).build(); 244 } 245 } 246 247 addResourceHttpHeaders(resource()); 248 return getContent(rangeValue, getChildrenLimit(), rdfStream); 249 } finally { 250 readLock.release(); 251 } 252 } 253 254 private int getChildrenLimit() { 255 final List<String> acceptHeaders = headers.getRequestHeader(ACCEPT); 256 if (acceptHeaders != null && acceptHeaders.size() > 0) { 257 final List<String> accept = Arrays.asList(acceptHeaders.get(0).split(",")); 258 if (accept.contains(TEXT_HTML)) { 259 // Magic number '100' is tied to common-metadata.vsl display of ellipses 260 return 100; 261 } 262 } 263 264 final List<String> limits = headers.getRequestHeader("Limit"); 265 if (null != limits && limits.size() > 0) { 266 try { 267 return Integer.parseInt(limits.get(0)); 268 269 } catch (final NumberFormatException e) { 270 LOGGER.warn("Invalid 'Limit' header value: {}", limits.get(0)); 271 throw new ClientErrorException("Invalid 'Limit' header value: " + limits.get(0), SC_BAD_REQUEST, e); 272 } 273 } 274 return -1; 275 } 276 277 /** 278 * Deletes an object. 279 * 280 * @return response 281 */ 282 @DELETE 283 @Timed 284 public Response deleteObject() { 285 evaluateRequestPreconditions(request, servletResponse, resource(), session); 286 287 LOGGER.info("Delete resource '{}'", externalPath); 288 289 final AcquiredLock lock = lockManager.lockForDelete(resource().getPath()); 290 291 try { 292 resource().delete(); 293 session.commit(); 294 return noContent().build(); 295 } finally { 296 lock.release(); 297 } 298 } 299 300 /** 301 * Create a resource at a specified path, or replace triples with provided RDF. 302 * 303 * @param requestContentType the request content type 304 * @param requestBodyStream the request body stream 305 * @param contentDisposition the content disposition value 306 * @param ifMatch the if-match value 307 * @param link the link value 308 * @param digest the digest header 309 * @return 204 310 * @throws InvalidChecksumException if invalid checksum exception occurred 311 * @throws MalformedRdfException if malformed rdf exception occurred 312 */ 313 @PUT 314 @Consumes 315 @Timed 316 public Response createOrReplaceObjectRdf( 317 @HeaderParam(CONTENT_TYPE) final MediaType requestContentType, 318 @ContentLocation final InputStream requestBodyStream, 319 @HeaderParam(CONTENT_DISPOSITION) final ContentDisposition contentDisposition, 320 @HeaderParam("If-Match") final String ifMatch, 321 @HeaderParam(LINK) final String link, 322 @HeaderParam("Digest") final String digest) 323 throws InvalidChecksumException, MalformedRdfException { 324 325 checkLinkForLdpResourceCreation(link); 326 327 final FedoraResource resource; 328 329 final String path = toPath(translator(), externalPath); 330 331 final AcquiredLock lock = lockManager.lockForWrite(path, session.getFedoraSession(), nodeService); 332 333 try { 334 335 final Collection<String> checksums = parseDigestHeader(digest); 336 337 final MediaType contentType = getSimpleContentType(requestContentType); 338 339 340 if (nodeService.exists(session.getFedoraSession(), path)) { 341 resource = resource(); 342 } else { 343 final MediaType effectiveContentType 344 = requestBodyStream == null || requestContentType == null ? null : contentType; 345 resource = createFedoraResource(path, effectiveContentType, contentDisposition); 346 } 347 348 349 if (httpConfiguration.putRequiresIfMatch() && StringUtils.isBlank(ifMatch) && !resource.isNew()) { 350 throw new ClientErrorException("An If-Match header is required", 428); 351 } 352 353 evaluateRequestPreconditions(request, servletResponse, resource, session); 354 final boolean created = resource.isNew(); 355 356 try (final RdfStream resourceTriples = 357 created ? new DefaultRdfStream(asNode(resource())) : getResourceTriples()) { 358 359 LOGGER.info("PUT resource '{}'", externalPath); 360 if (resource instanceof FedoraBinary) { 361 replaceResourceBinaryWithStream((FedoraBinary) resource, 362 requestBodyStream, contentDisposition, requestContentType, checksums); 363 } else if (isRdfContentType(contentType.toString())) { 364 replaceResourceWithStream(resource, requestBodyStream, contentType, resourceTriples); 365 } else if (!created) { 366 boolean emptyRequest = true; 367 try { 368 emptyRequest = requestBodyStream.read() == -1; 369 } catch (final IOException ex) { 370 LOGGER.debug("Error checking for request body content", ex); 371 } 372 373 if (requestContentType == null && emptyRequest) { 374 throw new ClientErrorException("Resource Already Exists", CONFLICT); 375 } 376 throw new ClientErrorException("Invalid Content Type " + requestContentType, 377 UNSUPPORTED_MEDIA_TYPE); 378 } 379 } catch (final Exception e) { 380 checkForInsufficientStorageException(e, e); 381 } 382 383 session.commit(); 384 return createUpdateResponse(resource, created); 385 386 } finally { 387 lock.release(); 388 } 389 } 390 391 /** 392 * Update an object using SPARQL-UPDATE 393 * 394 * @param requestBodyStream the request body stream 395 * @return 201 396 * @throws IOException if IO exception occurred 397 */ 398 @PATCH 399 @Consumes({contentTypeSPARQLUpdate}) 400 @Timed 401 public Response updateSparql(@ContentLocation final InputStream requestBodyStream) 402 throws IOException { 403 404 if (null == requestBodyStream) { 405 throw new BadRequestException("SPARQL-UPDATE requests must have content!"); 406 } 407 408 if (resource() instanceof FedoraBinary) { 409 throw new BadRequestException(resource().getPath() + " is not a valid object to receive a PATCH"); 410 } 411 412 final AcquiredLock lock = lockManager.lockForWrite(resource().getPath(), session.getFedoraSession(), 413 nodeService); 414 415 try { 416 final String requestBody = IOUtils.toString(requestBodyStream, UTF_8); 417 if (isBlank(requestBody)) { 418 throw new BadRequestException("SPARQL-UPDATE requests must have content!"); 419 } 420 421 evaluateRequestPreconditions(request, servletResponse, resource(), session); 422 423 try (final RdfStream resourceTriples = 424 resource().isNew() ? new DefaultRdfStream(asNode(resource())) : getResourceTriples()) { 425 LOGGER.info("PATCH for '{}'", externalPath); 426 patchResourcewithSparql(resource(), requestBody, resourceTriples); 427 } 428 session.commit(); 429 430 addCacheControlHeaders(servletResponse, resource().getDescription(), session); 431 432 return noContent().build(); 433 } catch (final IllegalArgumentException iae) { 434 throw new BadRequestException(iae.getMessage()); 435 } catch (final AccessDeniedException e) { 436 throw e; 437 } catch ( final RuntimeException ex ) { 438 final Throwable cause = ex.getCause(); 439 if (cause instanceof PathNotFoundRuntimeException) { 440 // the sparql update referred to a repository resource that doesn't exist 441 throw new BadRequestException(cause.getMessage()); 442 } 443 throw ex; 444 } finally { 445 lock.release(); 446 } 447 } 448 449 /** 450 * Creates a new object. 451 * 452 * This originally used application/octet-stream;qs=1001 as a workaround 453 * for JERSEY-2636, to ensure requests without a Content-Type get routed here. 454 * This qs value does not parse with newer versions of Jersey, as qs values 455 * must be between 0 and 1. We use qs=1.000 to mark where this historical 456 * anomaly had been. 457 * 458 * @param contentDisposition the content Disposition value 459 * @param requestContentType the request content type 460 * @param slug the slug value 461 * @param requestBodyStream the request body stream 462 * @param link the link value 463 * @param digest the digest header 464 * @return 201 465 * @throws InvalidChecksumException if invalid checksum exception occurred 466 * @throws IOException if IO exception occurred 467 * @throws MalformedRdfException if malformed rdf exception occurred 468 */ 469 @POST 470 @Consumes({MediaType.APPLICATION_OCTET_STREAM + ";qs=1.000", WILDCARD}) 471 @Timed 472 @Produces({TURTLE_WITH_CHARSET + ";qs=1.0", JSON_LD + ";qs=0.8", 473 N3_WITH_CHARSET, N3_ALT2_WITH_CHARSET, RDF_XML, NTRIPLES, TEXT_PLAIN_WITH_CHARSET, 474 TURTLE_X, TEXT_HTML_WITH_CHARSET, "*/*"}) 475 public Response createObject(@HeaderParam(CONTENT_DISPOSITION) final ContentDisposition contentDisposition, 476 @HeaderParam(CONTENT_TYPE) final MediaType requestContentType, 477 @HeaderParam("Slug") final String slug, 478 @ContentLocation final InputStream requestBodyStream, 479 @HeaderParam(LINK) final String link, 480 @HeaderParam("Digest") final String digest) 481 throws InvalidChecksumException, IOException, MalformedRdfException { 482 483 checkLinkForLdpResourceCreation(link); 484 485 if (!(resource() instanceof Container)) { 486 throw new ClientErrorException("Object cannot have child nodes", CONFLICT); 487 } else if (resource().hasType(FEDORA_PAIRTREE)) { 488 throw new ClientErrorException("Objects cannot be created under pairtree nodes", FORBIDDEN); 489 } 490 491 final MediaType contentType = getSimpleContentType(requestContentType); 492 493 final String contentTypeString = contentType.toString(); 494 495 final String newObjectPath = mintNewPid(slug); 496 497 final AcquiredLock lock = lockManager.lockForWrite(newObjectPath, session.getFedoraSession(), nodeService); 498 499 try { 500 501 final Collection<String> checksum = parseDigestHeader(digest); 502 503 LOGGER.info("Ingest with path: {}", newObjectPath); 504 505 final MediaType effectiveContentType 506 = requestBodyStream == null || requestContentType == null ? null : contentType; 507 resource = createFedoraResource(newObjectPath, effectiveContentType, contentDisposition); 508 509 try (final RdfStream resourceTriples = 510 resource.isNew() ? new DefaultRdfStream(asNode(resource())) : getResourceTriples()) { 511 512 if (requestBodyStream == null) { 513 LOGGER.trace("No request body detected"); 514 } else { 515 LOGGER.trace("Received createObject with a request body and content type \"{}\"", 516 contentTypeString); 517 518 if ((resource instanceof Container) && isRdfContentType(contentTypeString)) { 519 replaceResourceWithStream(resource, requestBodyStream, contentType, resourceTriples); 520 } else if (resource instanceof FedoraBinary) { 521 LOGGER.trace("Created a datastream and have a binary payload."); 522 replaceResourceBinaryWithStream((FedoraBinary) resource, 523 requestBodyStream, contentDisposition, requestContentType, checksum); 524 525 } else if (contentTypeString.equals(contentTypeSPARQLUpdate)) { 526 LOGGER.trace("Found SPARQL-Update content, applying.."); 527 patchResourcewithSparql(resource, IOUtils.toString(requestBodyStream, UTF_8), resourceTriples); 528 } else { 529 if (requestBodyStream.read() != -1) { 530 throw new ClientErrorException("Invalid Content Type " + contentTypeString, 531 UNSUPPORTED_MEDIA_TYPE); 532 } 533 } 534 } 535 session.commit(); 536 } catch (final Exception e) { 537 checkForInsufficientStorageException(e, e); 538 } 539 540 LOGGER.debug("Finished creating resource with path: {}", newObjectPath); 541 return createUpdateResponse(resource, true); 542 } finally { 543 lock.release(); 544 } 545 } 546 547 /** 548 * @param rootThrowable The original throwable 549 * @param throwable The throwable under direct scrutiny. 550 */ 551 private void checkForInsufficientStorageException(final Throwable rootThrowable, final Throwable throwable) 552 throws InvalidChecksumException { 553 final String message = throwable.getMessage(); 554 if (throwable instanceof IOException && message != null && message.contains( 555 INSUFFICIENT_SPACE_IDENTIFYING_MESSAGE)) { 556 throw new InsufficientStorageException(throwable.getMessage(), rootThrowable); 557 } 558 559 if (throwable.getCause() != null) { 560 checkForInsufficientStorageException(rootThrowable, throwable.getCause()); 561 } 562 563 if (rootThrowable instanceof InvalidChecksumException) { 564 throw (InvalidChecksumException) rootThrowable; 565 } else if (rootThrowable instanceof RuntimeException) { 566 throw (RuntimeException) rootThrowable; 567 } else { 568 throw new RepositoryRuntimeException(rootThrowable); 569 } 570 } 571 572 /** 573 * Create the appropriate response after a create or update request is processed. When a resource is created, 574 * examine the Prefer and Accept headers to determine whether to include a representation. By default, the 575 * URI for the created resource is return as plain text. If a minimal response is requested, then no body is 576 * returned. If a non-minimal return is requested, return the RDF for the created resource in the appropriate 577 * RDF serialization. 578 * 579 * @param resource The created or updated Fedora resource. 580 * @param created True for a newly-created resource, false for an updated resource. 581 * @return 204 No Content (for updated resources), 201 Created (for created resources) including the resource 582 * URI or content depending on Prefer headers. 583 */ 584 @SuppressWarnings("resource") 585 private Response createUpdateResponse(final FedoraResource resource, final boolean created) { 586 addCacheControlHeaders(servletResponse, resource, session); 587 addResourceLinkHeaders(resource, created); 588 if (!created) { 589 return noContent().build(); 590 } 591 592 final URI location = getUri(resource); 593 final Response.ResponseBuilder builder = created(location); 594 595 if (prefer == null || !prefer.hasReturn()) { 596 final MediaType acceptablePlainText = acceptabePlainTextMediaType(); 597 if (acceptablePlainText != null) { 598 return builder.type(acceptablePlainText).entity(location.toString()).build(); 599 } 600 return notAcceptable(mediaTypes(TEXT_PLAIN_TYPE).build()).build(); 601 } else if (prefer.getReturn().getValue().equals("minimal")) { 602 return builder.build(); 603 } else { 604 servletResponse.addHeader("Vary", "Accept, Range, Accept-Encoding, Accept-Language"); 605 if (prefer != null) { 606 prefer.getReturn().addResponseHeaders(servletResponse); 607 } 608 final RdfNamespacedStream rdfStream = new RdfNamespacedStream( 609 new DefaultRdfStream(asNode(resource()), getResourceTriples()), 610 session().getFedoraSession().getNamespaces()); 611 return builder.entity(rdfStream).build(); 612 } 613 } 614 615 /** 616 * Returns an acceptable plain text media type if possible, or null if not. 617 */ 618 private MediaType acceptabePlainTextMediaType() { 619 final List<MediaType> acceptable = headers.getAcceptableMediaTypes(); 620 if (acceptable == null || acceptable.size() == 0) { 621 return TEXT_PLAIN_TYPE; 622 } 623 for (final MediaType type : acceptable ) { 624 if (type.isWildcardType() || (type.isCompatible(TEXT_PLAIN_TYPE) && type.isWildcardSubtype())) { 625 return TEXT_PLAIN_TYPE; 626 } else if (type.isCompatible(TEXT_PLAIN_TYPE)) { 627 return type; 628 } 629 } 630 return null; 631 } 632 633 @Override 634 protected void addResourceHttpHeaders(final FedoraResource resource) { 635 super.addResourceHttpHeaders(resource); 636 637 if (session.isBatchSession()) { 638 final String canonical = translator().reverse() 639 .convert(resource) 640 .toString() 641 .replaceFirst("/tx:[^/]+", ""); 642 643 644 servletResponse.addHeader(LINK, "<" + canonical + ">;rel=\"canonical\""); 645 646 } 647 648 addLinkAndOptionsHttpHeaders(); 649 } 650 651 @Override 652 protected String externalPath() { 653 return externalPath; 654 } 655 656 private void addLinkAndOptionsHttpHeaders() { 657 // Add Link headers 658 addResourceLinkHeaders(resource()); 659 660 // Add Options headers 661 final String options; 662 663 if (resource() instanceof FedoraBinary) { 664 options = "DELETE,HEAD,GET,PUT,OPTIONS"; 665 666 } else if (resource() instanceof NonRdfSourceDescription) { 667 options = "MOVE,COPY,DELETE,POST,HEAD,GET,PUT,PATCH,OPTIONS"; 668 servletResponse.addHeader(HTTP_HEADER_ACCEPT_PATCH, contentTypeSPARQLUpdate); 669 670 } else if (resource() instanceof Container) { 671 options = "MOVE,COPY,DELETE,POST,HEAD,GET,PUT,PATCH,OPTIONS"; 672 servletResponse.addHeader(HTTP_HEADER_ACCEPT_PATCH, contentTypeSPARQLUpdate); 673 674 final String rdfTypes = TURTLE + "," + N3 + "," + N3_ALT2 + "," 675 + RDF_XML + "," + NTRIPLES + "," + JSON_LD; 676 servletResponse.addHeader("Accept-Post", rdfTypes + "," + MediaType.MULTIPART_FORM_DATA 677 + "," + contentTypeSPARQLUpdate); 678 } else { 679 options = ""; 680 } 681 682 servletResponse.addHeader("Allow", options); 683 } 684 685 private static String getRequestedObjectType(final MediaType requestContentType, 686 final ContentDisposition contentDisposition) { 687 688 if (requestContentType != null) { 689 final String s = requestContentType.toString(); 690 if (!s.equals(contentTypeSPARQLUpdate) && !isRdfContentType(s) || s.equals(TEXT_PLAIN)) { 691 return FEDORA_BINARY; 692 } 693 } 694 695 if (contentDisposition != null && contentDisposition.getType().equals("attachment")) { 696 return FEDORA_BINARY; 697 } 698 699 return FEDORA_CONTAINER; 700 } 701 702 private FedoraResource createFedoraResource(final String path, 703 final MediaType requestContentType, 704 final ContentDisposition contentDisposition) { 705 final String objectType = getRequestedObjectType(requestContentType, contentDisposition); 706 707 final FedoraResource result; 708 709 if (objectType.equals(FEDORA_BINARY)) { 710 result = binaryService.findOrCreate(session.getFedoraSession(), path); 711 } else { 712 result = containerService.findOrCreate(session.getFedoraSession(), path); 713 } 714 715 return result; 716 } 717 718 private String mintNewPid(final String slug) { 719 String pid; 720 721 if (slug != null && !slug.isEmpty()) { 722 pid = slug; 723 } else if (pidMinter != null) { 724 pid = pidMinter.get(); 725 } else { 726 pid = defaultPidMinter.get(); 727 } 728 // reverse translate the proffered or created identifier 729 LOGGER.trace("Using external identifier {} to create new resource.", pid); 730 LOGGER.trace("Using prefixed external identifier {} to create new resource.", uriInfo.getBaseUri() + "/" 731 + pid); 732 733 final URI newResourceUri = uriInfo.getAbsolutePathBuilder().clone().path(FedoraLdp.class) 734 .resolveTemplate("path", pid, false).build(); 735 736 pid = translator().asString(createResource(newResourceUri.toString())); 737 try { 738 pid = URLDecoder.decode(pid, "UTF-8"); 739 } catch (final UnsupportedEncodingException e) { 740 // noop 741 } 742 // remove leading slash left over from translation 743 LOGGER.trace("Using internal identifier {} to create new resource.", pid); 744 745 if (nodeService.exists(session.getFedoraSession(), pid)) { 746 LOGGER.trace("Resource with path {} already exists; minting new path instead", pid); 747 return mintNewPid(null); 748 } 749 750 return pid; 751 } 752 753 private static void checkLinkForLdpResourceCreation(final String link) { 754 if (link != null) { 755 try { 756 final Link linq = Link.valueOf(link); 757 if ("type".equals(linq.getRel()) && (LDP_NAMESPACE + "Resource").equals(linq.getUri().toString())) { 758 LOGGER.info("Unimplemented LDPR creation requested with header link: {}", link); 759 throw new ServerErrorException("LDPR creation not implemented", NOT_IMPLEMENTED); 760 } 761 } catch (final RuntimeException e) { 762 if (e instanceof IllegalArgumentException | e instanceof UriBuilderException) { 763 throw new ClientErrorException("Invalid link specified: " + link, BAD_REQUEST); 764 } 765 throw e; 766 } 767 } 768 } 769 770 /** 771 * Parse the RFC-3230 Digest response header value. Look for a 772 * sha1 checksum and return it as a urn, if missing or malformed 773 * an empty string is returned. 774 * @param digest The Digest header value 775 * @return the sha1 checksum value 776 * @throws InvalidChecksumException if an unsupported digest is used 777 */ 778 private static Collection<String> parseDigestHeader(final String digest) throws InvalidChecksumException { 779 try { 780 final Map<String,String> digestPairs = RFC3230_SPLITTER.split(nullToEmpty(digest)); 781 final boolean allSupportedAlgorithms = digestPairs.keySet().stream().allMatch( 782 ContentDigest.DIGEST_ALGORITHM::isSupportedAlgorithm); 783 784 // If you have one or more digests that are all valid or no digests. 785 if (digestPairs.isEmpty() || allSupportedAlgorithms) { 786 return digestPairs.entrySet().stream() 787 .filter(entry -> ContentDigest.DIGEST_ALGORITHM.isSupportedAlgorithm(entry.getKey())) 788 .map(entry -> ContentDigest.asURI(entry.getKey(), entry.getValue()).toString()) 789 .collect(Collectors.toSet()); 790 } else { 791 throw new InvalidChecksumException(String.format("Unsupported Digest Algorithim: %1$s", digest)); 792 } 793 } catch (final RuntimeException e) { 794 if (e instanceof IllegalArgumentException) { 795 throw new ClientErrorException("Invalid Digest header: " + digest + "\n", BAD_REQUEST); 796 } 797 throw e; 798 } 799 } 800}