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