001/** 002 * Copyright 2015 DuraSpace, Inc. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.fcrepo.http.api; 017 018 019import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource; 020import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML; 021import static javax.ws.rs.core.MediaType.APPLICATION_XML; 022import static javax.ws.rs.core.MediaType.TEXT_HTML; 023import static javax.ws.rs.core.MediaType.TEXT_PLAIN; 024import static javax.ws.rs.core.Response.Status.BAD_REQUEST; 025import static javax.ws.rs.core.Response.Status.NOT_IMPLEMENTED; 026import static javax.ws.rs.core.Response.created; 027import static javax.ws.rs.core.Response.noContent; 028import static javax.ws.rs.core.Response.ok; 029import static javax.ws.rs.core.Response.Status.CONFLICT; 030import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE; 031import static javax.ws.rs.core.Response.Status.FORBIDDEN; 032import static org.apache.commons.lang3.StringUtils.isBlank; 033import static org.apache.http.HttpStatus.SC_BAD_REQUEST; 034import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate; 035import static org.fcrepo.http.commons.domain.RDFMediaType.JSON_LD; 036import static org.fcrepo.http.commons.domain.RDFMediaType.N3; 037import static org.fcrepo.http.commons.domain.RDFMediaType.N3_ALT2; 038import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES; 039import static org.fcrepo.http.commons.domain.RDFMediaType.RDF_XML; 040import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE; 041import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE_X; 042import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_BINARY; 043import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_CONTAINER; 044import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_PAIRTREE; 045import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE; 046import static org.fcrepo.kernel.modeshape.services.TransactionServiceImpl.getCurrentTransactionId; 047import static org.slf4j.LoggerFactory.getLogger; 048 049import java.io.IOException; 050import java.io.InputStream; 051import java.io.UnsupportedEncodingException; 052import java.net.URI; 053import java.net.URLDecoder; 054import java.util.Arrays; 055import java.util.List; 056import java.util.Map; 057 058import javax.annotation.PostConstruct; 059import javax.inject.Inject; 060import javax.jcr.AccessDeniedException; 061import javax.jcr.PathNotFoundException; 062import javax.jcr.RepositoryException; 063import javax.jcr.Session; 064import javax.ws.rs.BadRequestException; 065import javax.ws.rs.ClientErrorException; 066import javax.ws.rs.Consumes; 067import javax.ws.rs.DELETE; 068import javax.ws.rs.GET; 069import javax.ws.rs.HEAD; 070import javax.ws.rs.HeaderParam; 071import javax.ws.rs.OPTIONS; 072import javax.ws.rs.POST; 073import javax.ws.rs.PUT; 074import javax.ws.rs.Path; 075import javax.ws.rs.PathParam; 076import javax.ws.rs.Produces; 077import javax.ws.rs.QueryParam; 078import javax.ws.rs.ServerErrorException; 079import javax.ws.rs.core.HttpHeaders; 080import javax.ws.rs.core.Link; 081import javax.ws.rs.core.MediaType; 082import javax.ws.rs.core.Response; 083import javax.ws.rs.core.UriBuilderException; 084 085import org.fcrepo.http.commons.domain.ContentLocation; 086import org.fcrepo.http.commons.domain.PATCH; 087import org.fcrepo.kernel.api.exception.InvalidChecksumException; 088import org.fcrepo.kernel.api.exception.MalformedRdfException; 089import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 090import org.fcrepo.kernel.api.models.Container; 091import org.fcrepo.kernel.api.models.FedoraBinary; 092import org.fcrepo.kernel.api.models.FedoraResource; 093import org.fcrepo.kernel.api.models.NonRdfSourceDescription; 094import org.fcrepo.kernel.api.utils.iterators.RdfStream; 095 096import org.apache.commons.io.IOUtils; 097import org.apache.commons.lang3.StringUtils; 098import org.glassfish.jersey.media.multipart.ContentDisposition; 099import org.slf4j.Logger; 100import org.springframework.context.annotation.Scope; 101 102import com.codahale.metrics.annotation.Timed; 103import com.google.common.annotations.VisibleForTesting; 104import com.google.common.base.Splitter; 105 106import static com.google.common.base.Strings.nullToEmpty; 107 108/** 109 * @author cabeer 110 * @author ajs6f 111 * @since 9/25/14 112 */ 113 114@Scope("request") 115@Path("/{path: .*}") 116public class FedoraLdp extends ContentExposingResource { 117 118 119 @Inject 120 protected Session session; 121 122 private static final Logger LOGGER = getLogger(FedoraLdp.class); 123 124 private static final Splitter.MapSplitter RFC3230_SPLITTER = 125 Splitter.on(',').omitEmptyStrings().trimResults(). 126 withKeyValueSeparator(Splitter.on('=').limit(2)); 127 128 @PathParam("path") protected String externalPath; 129 130 @Inject private FedoraHttpConfiguration httpConfiguration; 131 132 /** 133 * Default JAX-RS entry point 134 */ 135 public FedoraLdp() { 136 super(); 137 } 138 139 /** 140 * Create a new FedoraNodes instance for a given path 141 * @param externalPath the external path 142 */ 143 @VisibleForTesting 144 public FedoraLdp(final String externalPath) { 145 this.externalPath = externalPath; 146 } 147 148 /** 149 * Run these actions after initializing this resource 150 */ 151 @PostConstruct 152 public void postConstruct() { 153 setUpJMSInfo(uriInfo, headers); 154 } 155 156 /** 157 * Retrieve the node headers 158 * @return response 159 */ 160 @HEAD 161 @Timed 162 public Response head() { 163 LOGGER.info("HEAD for: {}", externalPath); 164 165 checkCacheControlHeaders(request, servletResponse, resource(), session); 166 167 addResourceHttpHeaders(resource()); 168 169 final Response.ResponseBuilder builder = ok(); 170 171 if (resource() instanceof FedoraBinary) { 172 builder.type(((FedoraBinary) resource()).getMimeType()); 173 } 174 175 return builder.build(); 176 } 177 178 /** 179 * Outputs information about the supported HTTP methods, etc. 180 * @return the outputs information about the supported HTTP methods, etc. 181 */ 182 @OPTIONS 183 @Timed 184 public Response options() { 185 LOGGER.info("OPTIONS for '{}'", externalPath); 186 addOptionsHttpHeaders(); 187 return ok().build(); 188 } 189 190 191 /** 192 * Retrieve the node profile 193 * 194 * @param rangeValue the range value 195 * @return triples for the specified node 196 * @throws IOException if IO exception occurred 197 */ 198 @GET 199 @Produces({TURTLE + ";qs=10", JSON_LD + ";qs=8", 200 N3, N3_ALT2, RDF_XML, NTRIPLES, APPLICATION_XML, TEXT_PLAIN, TURTLE_X, 201 TEXT_HTML, APPLICATION_XHTML_XML, "*/*"}) 202 public Response describe(@HeaderParam("Range") final String rangeValue) throws IOException { 203 checkCacheControlHeaders(request, servletResponse, resource(), session); 204 205 LOGGER.info("GET resource '{}'", externalPath); 206 addResourceHttpHeaders(resource()); 207 208 final RdfStream rdfStream = new RdfStream().session(session) 209 .topic(translator().reverse().convert(resource()).asNode()); 210 211 return getContent(rangeValue, getChildrenLimit(), rdfStream); 212 213 } 214 215 private int getChildrenLimit() { 216 final List<String> acceptHeaders = headers.getRequestHeader(HttpHeaders.ACCEPT); 217 if (acceptHeaders != null && acceptHeaders.size() > 0) { 218 final List<String> accept = Arrays.asList(acceptHeaders.get(0).split(",")); 219 if (accept.contains(TEXT_HTML) || accept.contains(APPLICATION_XHTML_XML)) { 220 // Magic number '100' is tied to common-metadata.vsl display of ellipses 221 return 100; 222 } 223 } 224 225 final List<String> limits = headers.getRequestHeader("Limit"); 226 if (null != limits && limits.size() > 0) { 227 try { 228 return Integer.parseInt(limits.get(0)); 229 230 } catch (NumberFormatException e) { 231 LOGGER.warn("Invalid 'Limit' header value: {}", limits.get(0)); 232 throw new ClientErrorException("Invalid 'Limit' header value: " + limits.get(0), SC_BAD_REQUEST, e); 233 } 234 } 235 return -1; 236 } 237 238 /** 239 * Deletes an object. 240 * 241 * @return response 242 */ 243 @DELETE 244 @Timed 245 public Response deleteObject() { 246 evaluateRequestPreconditions(request, servletResponse, resource(), session); 247 248 LOGGER.info("Delete resource '{}'", externalPath); 249 resource().delete(); 250 251 try { 252 session.save(); 253 } catch (final RepositoryException e) { 254 throw new RepositoryRuntimeException(e); 255 } 256 257 return noContent().build(); 258 } 259 260 /** 261 * Create a resource at a specified path, or replace triples with provided RDF. 262 * @param requestContentType the request content type 263 * @param requestBodyStream the request body stream 264 * @param checksum the checksum value 265 * @param contentDisposition the content disposition value 266 * @param ifMatch the if-match value 267 * @param link the link value 268 * @return 204 269 * @throws InvalidChecksumException if invalid checksum exception occurred 270 * @throws MalformedRdfException if malformed rdf exception occurred 271 */ 272 public Response createOrReplaceObjectRdf( 273 @HeaderParam("Content-Type") final MediaType requestContentType, 274 @ContentLocation final InputStream requestBodyStream, 275 @QueryParam("checksum") final String checksum, 276 @HeaderParam("Content-Disposition") final ContentDisposition contentDisposition, 277 @HeaderParam("If-Match") final String ifMatch, 278 @HeaderParam("Link") final String link) 279 throws InvalidChecksumException, MalformedRdfException { 280 return createOrReplaceObjectRdf(requestContentType, requestBodyStream, 281 checksum, contentDisposition, ifMatch, link, null); 282 } 283 284 /** 285 * Create a resource at a specified path, or replace triples with provided RDF. 286 * 287 * Temporary 6 parameter version of this function to allow for backwards 288 * compatability during a period of transition from a digest hash being 289 * provided via non-standard 'checksum' query parameter to RFC-3230 compliant 290 * 'Digest' header. 291 * 292 * TODO: Remove this function in favour of the 5 parameter version that takes 293 * the Digest header in lieu of the checksum parameter 294 * https://jira.duraspace.org/browse/FCREPO-1851 295 * 296 * @param requestContentType the request content type 297 * @param requestBodyStream the request body stream 298 * @param checksumDeprecated the deprecated digest hash 299 * @param contentDisposition the content disposition value 300 * @param ifMatch the if-match value 301 * @param link the link value 302 * @param digest the digest header 303 * @return 204 304 * @throws InvalidChecksumException if invalid checksum exception occurred 305 * @throws MalformedRdfException if malformed rdf exception occurred 306 */ 307 @PUT 308 @Consumes 309 @Timed 310 public Response createOrReplaceObjectRdf( 311 @HeaderParam("Content-Type") final MediaType requestContentType, 312 @ContentLocation final InputStream requestBodyStream, 313 @QueryParam("checksum") final String checksumDeprecated, 314 @HeaderParam("Content-Disposition") final ContentDisposition contentDisposition, 315 @HeaderParam("If-Match") final String ifMatch, 316 @HeaderParam("Link") final String link, 317 @HeaderParam("Digest") final String digest) 318 throws InvalidChecksumException, MalformedRdfException { 319 320 checkLinkForLdpResourceCreation(link); 321 322 final FedoraResource resource; 323 final Response.ResponseBuilder response; 324 325 final String path = toPath(translator(), externalPath); 326 327 // TODO: Add final when deprecated checksum Query paramater is removed 328 // https://jira.duraspace.org/browse/FCREPO-1851 329 String checksum = parseDigestHeader(digest); 330 331 final MediaType contentType = getSimpleContentType(requestContentType); 332 333 if (nodeService.exists(session, path)) { 334 resource = resource(); 335 response = noContent(); 336 } else { 337 final MediaType effectiveContentType 338 = requestBodyStream == null || requestContentType == null ? null : contentType; 339 resource = createFedoraResource(path, effectiveContentType, contentDisposition); 340 341 final URI location = getUri(resource); 342 343 response = created(location).entity(location.toString()); 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 352 final RdfStream resourceTriples; 353 354 if (resource.isNew()) { 355 resourceTriples = new RdfStream(); 356 } else { 357 resourceTriples = getResourceTriples(); 358 } 359 360 LOGGER.info("PUT resource '{}'", externalPath); 361 if (resource instanceof FedoraBinary) { 362 if (!StringUtils.isBlank(checksumDeprecated) && StringUtils.isBlank(digest)) { 363 addChecksumDeprecationHeader(resource); 364 checksum = checksumDeprecated; 365 } 366 replaceResourceBinaryWithStream((FedoraBinary) resource, 367 requestBodyStream, contentDisposition, requestContentType, checksum); 368 } else if (isRdfContentType(contentType.toString())) { 369 replaceResourceWithStream(resource, requestBodyStream, contentType, resourceTriples); 370 } else if (!resource.isNew()) { 371 boolean emptyRequest = true; 372 try { 373 emptyRequest = requestBodyStream.read() == -1; 374 } catch (final IOException ex) { 375 LOGGER.debug("Error checking for request body content", ex); 376 } 377 378 if (requestContentType == null && emptyRequest) { 379 throw new ClientErrorException("Resource Already Exists", CONFLICT); 380 } 381 throw new ClientErrorException("Invalid Content Type " + requestContentType, UNSUPPORTED_MEDIA_TYPE); 382 } 383 384 try { 385 session.save(); 386 } catch (final RepositoryException e) { 387 throw new RepositoryRuntimeException(e); 388 } 389 390 addCacheControlHeaders(servletResponse, resource, session); 391 392 addResourceLinkHeaders(resource); 393 394 return response.build(); 395 396 } 397 398 /** 399 * Update an object using SPARQL-UPDATE 400 * 401 * @param requestBodyStream the request body stream 402 * @return 201 403 * @throws MalformedRdfException if malformed rdf exception occurred 404 * @throws AccessDeniedException if exception updating property occurred 405 * @throws IOException if IO exception occurred 406 */ 407 @PATCH 408 @Consumes({contentTypeSPARQLUpdate}) 409 @Timed 410 public Response updateSparql(@ContentLocation final InputStream requestBodyStream) 411 throws IOException, MalformedRdfException, AccessDeniedException { 412 413 if (null == requestBodyStream) { 414 throw new BadRequestException("SPARQL-UPDATE requests must have content!"); 415 } 416 417 if (resource() instanceof FedoraBinary) { 418 throw new BadRequestException(resource() + " is not a valid object to receive a PATCH"); 419 } 420 421 try { 422 final String requestBody = IOUtils.toString(requestBodyStream); 423 if (isBlank(requestBody)) { 424 throw new BadRequestException("SPARQL-UPDATE requests must have content!"); 425 } 426 427 evaluateRequestPreconditions(request, servletResponse, resource(), session); 428 429 final RdfStream resourceTriples; 430 431 if (resource().isNew()) { 432 resourceTriples = new RdfStream(); 433 } else { 434 resourceTriples = getResourceTriples(); 435 } 436 437 LOGGER.info("PATCH for '{}'", externalPath); 438 patchResourcewithSparql(resource(), requestBody, resourceTriples); 439 440 session.save(); 441 442 addCacheControlHeaders(servletResponse, resource(), session); 443 444 return noContent().build(); 445 } catch (final IllegalArgumentException iae) { 446 throw new BadRequestException(iae.getMessage()); 447 } catch ( final RuntimeException ex ) { 448 final Throwable cause = ex.getCause(); 449 if (cause instanceof PathNotFoundException) { 450 // the sparql update referred to a repository resource that doesn't exist 451 throw new BadRequestException(cause.getMessage()); 452 } 453 throw ex; 454 } catch (final RepositoryException e) { 455 if (e instanceof AccessDeniedException) { 456 throw new AccessDeniedException(e.getMessage()); 457 } 458 throw new RepositoryRuntimeException(e); 459 } 460 } 461 462 /** 463 * Creates a new object. 464 * 465 * application/octet-stream;qs=1001 is a workaround for JERSEY-2636, to ensure 466 * requests without a Content-Type get routed here. 467 * 468 * @param checksum the checksum value 469 * @param contentDisposition the content Disposition value 470 * @param requestContentType the request content type 471 * @param slug the slug value 472 * @param requestBodyStream the request body stream 473 * @param link the link value 474 * @return 201 475 * @throws InvalidChecksumException if invalid checksum exception occurred 476 * @throws IOException if IO exception occurred 477 * @throws MalformedRdfException if malformed rdf exception occurred 478 * @throws AccessDeniedException if access denied in creating resource 479 */ 480 public Response createObject(@QueryParam("checksum") final String checksum, 481 @HeaderParam("Content-Disposition") final ContentDisposition contentDisposition, 482 @HeaderParam("Content-Type") final MediaType requestContentType, 483 @HeaderParam("Slug") final String slug, 484 @ContentLocation final InputStream requestBodyStream, 485 @HeaderParam("Link") final String link) 486 throws InvalidChecksumException, IOException, MalformedRdfException, AccessDeniedException { 487 return createObject(checksum, contentDisposition, requestContentType, slug, requestBodyStream, link, null); 488 } 489 /** 490 * Creates a new object. 491 * 492 * application/octet-stream;qs=1001 is a workaround for JERSEY-2636, to ensure 493 * requests without a Content-Type get routed here. 494 * 495 * @param checksumDeprecated the checksum value 496 * @param contentDisposition the content Disposition value 497 * @param requestContentType the request content type 498 * @param slug the slug value 499 * @param requestBodyStream the request body stream 500 * @param link the link value 501 * @param digest the digest header 502 * @return 201 503 * @throws InvalidChecksumException if invalid checksum exception occurred 504 * @throws IOException if IO exception occurred 505 * @throws MalformedRdfException if malformed rdf exception occurred 506 * @throws AccessDeniedException if access denied in creating resource 507 */ 508 @POST 509 @Consumes({MediaType.APPLICATION_OCTET_STREAM + ";qs=1001", MediaType.WILDCARD}) 510 @Timed 511 public Response createObject(@QueryParam("checksum") final String checksumDeprecated, 512 @HeaderParam("Content-Disposition") final ContentDisposition contentDisposition, 513 @HeaderParam("Content-Type") final MediaType requestContentType, 514 @HeaderParam("Slug") final String slug, 515 @ContentLocation final InputStream requestBodyStream, 516 @HeaderParam("Link") final String link, 517 @HeaderParam("Digest") final String digest) 518 throws InvalidChecksumException, IOException, MalformedRdfException, AccessDeniedException { 519 520 checkLinkForLdpResourceCreation(link); 521 522 if (!(resource() instanceof Container)) { 523 throw new ClientErrorException("Object cannot have child nodes", CONFLICT); 524 } else if (resource().hasType(FEDORA_PAIRTREE)) { 525 throw new ClientErrorException("Objects cannot be created under pairtree nodes", FORBIDDEN); 526 } 527 528 final MediaType contentType = getSimpleContentType(requestContentType); 529 530 final String contentTypeString = contentType.toString(); 531 532 final String newObjectPath = mintNewPid(slug); 533 534 // TODO: Add final when deprecated checksum Query paramater is removed 535 // https://jira.duraspace.org/browse/FCREPO-1851 536 String checksum = parseDigestHeader(digest); 537 538 LOGGER.info("Ingest with path: {}", newObjectPath); 539 540 final MediaType effectiveContentType 541 = requestBodyStream == null || requestContentType == null ? null : contentType; 542 final FedoraResource result = createFedoraResource( 543 newObjectPath, 544 effectiveContentType, 545 contentDisposition); 546 547 final RdfStream resourceTriples; 548 549 if (result.isNew()) { 550 resourceTriples = new RdfStream(); 551 } else { 552 resourceTriples = getResourceTriples(); 553 } 554 555 if (requestBodyStream == null) { 556 LOGGER.trace("No request body detected"); 557 } else { 558 LOGGER.trace("Received createObject with a request body and content type \"{}\"", contentTypeString); 559 560 if ((result instanceof Container) 561 && isRdfContentType(contentTypeString)) { 562 replaceResourceWithStream(result, requestBodyStream, contentType, resourceTriples); 563 } else if (result instanceof FedoraBinary) { 564 LOGGER.trace("Created a datastream and have a binary payload."); 565 if (!StringUtils.isBlank(checksumDeprecated) && StringUtils.isBlank(digest)) { 566 addChecksumDeprecationHeader(resource); 567 checksum = checksumDeprecated; 568 } 569 replaceResourceBinaryWithStream((FedoraBinary) result, 570 requestBodyStream, contentDisposition, requestContentType, checksum); 571 572 } else if (contentTypeString.equals(contentTypeSPARQLUpdate)) { 573 LOGGER.trace("Found SPARQL-Update content, applying.."); 574 patchResourcewithSparql(result, IOUtils.toString(requestBodyStream), resourceTriples); 575 } else { 576 if (requestBodyStream.read() != -1) { 577 throw new ClientErrorException("Invalid Content Type " + contentTypeString, UNSUPPORTED_MEDIA_TYPE); 578 } 579 } 580 } 581 582 try { 583 session.save(); 584 } catch (final RepositoryException e) { 585 throw new RepositoryRuntimeException(e); 586 } 587 588 LOGGER.debug("Finished creating resource with path: {}", newObjectPath); 589 590 addCacheControlHeaders(servletResponse, result, session); 591 592 final URI location = getUri(result); 593 594 addResourceLinkHeaders(result, true); 595 596 return created(location).entity(location.toString()).build(); 597 598 } 599 600 @Override 601 protected void addResourceHttpHeaders(final FedoraResource resource) { 602 super.addResourceHttpHeaders(resource); 603 604 if (getCurrentTransactionId(session) != null) { 605 final String canonical = translator().reverse() 606 .convert(resource) 607 .toString() 608 .replaceFirst("/tx:[^/]+", ""); 609 610 611 servletResponse.addHeader("Link", "<" + canonical + ">;rel=\"canonical\""); 612 613 } 614 615 addOptionsHttpHeaders(); 616 } 617 618 @Override 619 protected String externalPath() { 620 return externalPath; 621 } 622 623 private void addOptionsHttpHeaders() { 624 final String options; 625 626 if (resource() instanceof FedoraBinary) { 627 options = "DELETE,HEAD,GET,PUT,OPTIONS"; 628 629 } else if (resource() instanceof NonRdfSourceDescription) { 630 options = "MOVE,COPY,DELETE,POST,HEAD,GET,PUT,PATCH,OPTIONS"; 631 servletResponse.addHeader("Accept-Patch", contentTypeSPARQLUpdate); 632 633 } else if (resource() instanceof Container) { 634 options = "MOVE,COPY,DELETE,POST,HEAD,GET,PUT,PATCH,OPTIONS"; 635 servletResponse.addHeader("Accept-Patch", contentTypeSPARQLUpdate); 636 637 final String rdfTypes = TURTLE + "," + N3 + "," 638 + N3_ALT2 + "," + RDF_XML + "," + NTRIPLES; 639 servletResponse.addHeader("Accept-Post", rdfTypes + "," + MediaType.MULTIPART_FORM_DATA 640 + "," + contentTypeSPARQLUpdate); 641 } else { 642 options = ""; 643 } 644 645 addResourceLinkHeaders(resource()); 646 647 servletResponse.addHeader("Allow", options); 648 } 649 650 private void addResourceLinkHeaders(final FedoraResource resource) { 651 addResourceLinkHeaders(resource, false); 652 } 653 654 private void addResourceLinkHeaders(final FedoraResource resource, final boolean includeAnchor) { 655 if (resource instanceof NonRdfSourceDescription) { 656 final URI uri = getUri(((NonRdfSourceDescription) resource).getDescribedResource()); 657 final Link link = Link.fromUri(uri).rel("describes").build(); 658 servletResponse.addHeader("Link", link.toString()); 659 } else if (resource instanceof FedoraBinary) { 660 final URI uri = getUri(((FedoraBinary) resource).getDescription()); 661 final Link.Builder builder = Link.fromUri(uri).rel("describedby"); 662 663 if (includeAnchor) { 664 builder.param("anchor", getUri(resource).toString()); 665 } 666 servletResponse.addHeader("Link", builder.build().toString()); 667 } 668 669 670 } 671 /** 672 * Add a deprecation notice via the Warning header as per 673 * RFC-7234 https://tools.ietf.org/html/rfc7234#section-5.5 674 */ 675 private void addChecksumDeprecationHeader(final FedoraResource resource) { 676 servletResponse.addHeader("Warning", "Specifying a SHA-1 Checksum via query parameter is deprecated."); 677 } 678 679 private static String getRequestedObjectType(final MediaType requestContentType, 680 final ContentDisposition contentDisposition) { 681 682 if (requestContentType != null) { 683 final String s = requestContentType.toString(); 684 if (!s.equals(contentTypeSPARQLUpdate) && !isRdfContentType(s) || s.equals(TEXT_PLAIN)) { 685 return FEDORA_BINARY; 686 } 687 } 688 689 if (contentDisposition != null && contentDisposition.getType().equals("attachment")) { 690 return FEDORA_BINARY; 691 } 692 693 return FEDORA_CONTAINER; 694 } 695 696 private FedoraResource createFedoraResource(final String path, 697 final MediaType requestContentType, 698 final ContentDisposition contentDisposition) { 699 final String objectType = getRequestedObjectType(requestContentType, contentDisposition); 700 701 final FedoraResource result; 702 703 if (objectType.equals(FEDORA_BINARY)) { 704 result = binaryService.findOrCreate(session, path); 705 } else { 706 result = containerService.findOrCreate(session, path); 707 } 708 709 return result; 710 } 711 712 @Override 713 protected Session session() { 714 return session; 715 } 716 717 private String mintNewPid(final String slug) { 718 String pid; 719 720 if (slug != null && !slug.isEmpty()) { 721 pid = slug; 722 } else if (pidMinter != null) { 723 pid = pidMinter.get(); 724 } else { 725 pid = defaultPidMinter.get(); 726 } 727 // reverse translate the proffered or created identifier 728 LOGGER.trace("Using external identifier {} to create new resource.", pid); 729 LOGGER.trace("Using prefixed external identifier {} to create new resource.", uriInfo.getBaseUri() + "/" 730 + pid); 731 732 final URI newResourceUri = uriInfo.getAbsolutePathBuilder().clone().path(FedoraLdp.class) 733 .resolveTemplate("path", pid, false).build(); 734 735 pid = translator().asString(createResource(newResourceUri.toString())); 736 try { 737 pid = URLDecoder.decode(pid, "UTF-8"); 738 } catch (final UnsupportedEncodingException e) { 739 // noop 740 } 741 // remove leading slash left over from translation 742 LOGGER.trace("Using internal identifier {} to create new resource.", pid); 743 744 if (nodeService.exists(session, pid)) { 745 LOGGER.trace("Resource with path {} already exists; minting new path instead", pid); 746 return mintNewPid(null); 747 } 748 749 return pid; 750 } 751 752 private void checkLinkForLdpResourceCreation(final String link) { 753 if (link != null) { 754 try { 755 final Link linq = Link.valueOf(link); 756 if ("type".equals(linq.getRel()) && (LDP_NAMESPACE + "Resource").equals(linq.getUri().toString())) { 757 LOGGER.info("Unimplemented LDPR creation requested with header link: {}", link); 758 throw new ServerErrorException("LDPR creation not implemented", NOT_IMPLEMENTED); 759 } 760 } catch (RuntimeException e) { 761 if (e instanceof IllegalArgumentException | e instanceof UriBuilderException) { 762 throw new ClientErrorException("Invalid link specified: " + link, BAD_REQUEST); 763 } 764 throw e; 765 } 766 } 767 } 768 769 /** 770 * Parse the RFC-3230 Digest response header value. Look for a 771 * sha1 checksum and return it as a urn, if missing or malformed 772 * an empty string is returned. 773 * @param digest The Digest header value 774 * @return the sha1 checksum value 775 */ 776 private String parseDigestHeader(final String digest) { 777 try { 778 final Map<String,String> digestPairs = RFC3230_SPLITTER.split(nullToEmpty(digest)); 779 return digestPairs.entrySet().stream() 780 .filter(s -> s.getKey().toLowerCase().equals("sha1")) 781 .map(Map.Entry::getValue) 782 .findFirst() 783 .map("urn:sha1:"::concat) 784 .orElse(""); 785 } catch (RuntimeException e) { 786 if (e instanceof IllegalArgumentException) { 787 throw new ClientErrorException("Invalid Digest header: " + digest + "\n", BAD_REQUEST); 788 } 789 throw e; 790 } 791 } 792}