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