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 java.util.EnumSet.of; 022import static java.util.stream.Stream.concat; 023import static java.util.stream.Stream.empty; 024import static javax.ws.rs.core.HttpHeaders.CACHE_CONTROL; 025import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH; 026import static javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION; 027import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; 028import static javax.ws.rs.core.HttpHeaders.LINK; 029import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; 030import static javax.ws.rs.core.Response.ok; 031import static javax.ws.rs.core.Response.status; 032import static javax.ws.rs.core.Response.temporaryRedirect; 033import static javax.ws.rs.core.Response.Status.PARTIAL_CONTENT; 034import static javax.ws.rs.core.Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE; 035import static org.apache.commons.lang3.StringUtils.isBlank; 036import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; 037import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 038import static org.apache.jena.riot.RDFLanguages.contentTypeToLang; 039import static org.apache.jena.vocabulary.RDF.type; 040import static org.fcrepo.kernel.api.FedoraTypes.LDP_BASIC_CONTAINER; 041import static org.fcrepo.kernel.api.FedoraTypes.LDP_DIRECT_CONTAINER; 042import static org.fcrepo.kernel.api.FedoraTypes.LDP_INDIRECT_CONTAINER; 043import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER; 044import static org.fcrepo.kernel.api.RdfLexicon.CONTAINER; 045import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER; 046import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER; 047import static org.fcrepo.kernel.api.RdfLexicon.LDP_NAMESPACE; 048import static org.fcrepo.kernel.api.RdfLexicon.HAS_MEMBER_RELATION; 049import static org.fcrepo.kernel.api.RdfLexicon.isManagedNamespace; 050import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate; 051import static org.fcrepo.kernel.api.RequiredRdfContext.EMBED_RESOURCES; 052import static org.fcrepo.kernel.api.RequiredRdfContext.INBOUND_REFERENCES; 053import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT; 054import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP; 055import static org.fcrepo.kernel.api.RequiredRdfContext.MINIMAL; 056import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES; 057import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED; 058import static org.slf4j.LoggerFactory.getLogger; 059 060import java.io.IOException; 061import java.io.InputStream; 062import java.net.URI; 063import java.net.URISyntaxException; 064import java.text.MessageFormat; 065import java.time.Instant; 066import java.util.ArrayList; 067import java.util.Collection; 068import java.util.Date; 069import java.util.HashSet; 070import java.util.List; 071import java.util.Set; 072import java.util.function.Predicate; 073import java.util.stream.Collectors; 074import java.util.stream.Stream; 075 076import javax.inject.Inject; 077import javax.servlet.http.HttpServletResponse; 078import javax.ws.rs.BadRequestException; 079import javax.ws.rs.BeanParam; 080import javax.ws.rs.core.CacheControl; 081import javax.ws.rs.core.Context; 082import javax.ws.rs.core.EntityTag; 083import javax.ws.rs.core.Link; 084import javax.ws.rs.core.MediaType; 085import javax.ws.rs.core.Request; 086import javax.ws.rs.core.Response; 087 088import org.fcrepo.http.commons.api.HttpHeaderInjector; 089import org.fcrepo.http.commons.api.rdf.HttpTripleUtil; 090import org.fcrepo.http.commons.domain.MultiPrefer; 091import org.fcrepo.http.commons.domain.PreferTag; 092import org.fcrepo.http.commons.domain.Range; 093import org.fcrepo.http.commons.domain.ldp.LdpPreferTag; 094import org.fcrepo.http.commons.responses.RangeRequestInputStream; 095import org.fcrepo.http.commons.responses.RdfNamespacedStream; 096import org.fcrepo.http.commons.session.HttpSession; 097import org.fcrepo.kernel.api.RdfStream; 098import org.fcrepo.kernel.api.TripleCategory; 099import org.fcrepo.kernel.api.exception.InvalidChecksumException; 100import org.fcrepo.kernel.api.exception.MalformedRdfException; 101import org.fcrepo.kernel.api.exception.PreconditionException; 102import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 103import org.fcrepo.kernel.api.exception.ServerManagedPropertyException; 104import org.fcrepo.kernel.api.models.Container; 105import org.fcrepo.kernel.api.models.FedoraBinary; 106import org.fcrepo.kernel.api.models.FedoraResource; 107import org.fcrepo.kernel.api.models.NonRdfSourceDescription; 108import org.fcrepo.kernel.api.rdf.DefaultRdfStream; 109import org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint; 110 111import org.apache.jena.atlas.RuntimeIOException; 112import org.apache.jena.graph.Triple; 113import org.apache.jena.rdf.model.Model; 114import org.apache.jena.rdf.model.RDFNode; 115import org.apache.jena.rdf.model.Statement; 116import org.apache.jena.riot.Lang; 117import org.apache.jena.riot.RiotException; 118import org.glassfish.jersey.media.multipart.ContentDisposition; 119import org.jvnet.hk2.annotations.Optional; 120import org.slf4j.Logger; 121 122import com.fasterxml.jackson.core.JsonParseException; 123import com.google.common.annotations.VisibleForTesting; 124 125/** 126 * An abstract class that sits between AbstractResource and any resource that 127 * wishes to share the routines for building responses containing binary 128 * content. 129 * 130 * @author Mike Durbin 131 * @author ajs6f 132 */ 133public abstract class ContentExposingResource extends FedoraBaseResource { 134 135 private static final Logger LOGGER = getLogger(ContentExposingResource.class); 136 public static final MediaType MESSAGE_EXTERNAL_BODY = MediaType.valueOf("message/external-body"); 137 138 @Context protected Request request; 139 @Context protected HttpServletResponse servletResponse; 140 141 @Inject 142 @Optional 143 private HttpTripleUtil httpTripleUtil; 144 145 @Inject 146 @Optional 147 private HttpHeaderInjector httpHeaderInject; 148 149 @BeanParam 150 protected MultiPrefer prefer; 151 152 @Inject 153 @Optional 154 StoragePolicyDecisionPoint storagePolicyDecisionPoint; 155 156 protected FedoraResource resource; 157 158 @Inject 159 protected PathLockManager lockManager; 160 161 private static final Predicate<Triple> IS_MANAGED_TYPE = t -> t.getPredicate().equals(type.asNode()) && 162 isManagedNamespace.test(t.getObject().getNameSpace()); 163 private static final Predicate<Triple> IS_MANAGED_TRIPLE = IS_MANAGED_TYPE 164 .or(t -> isManagedPredicate.test(createProperty(t.getPredicate().getURI()))); 165 166 protected abstract String externalPath(); 167 168 protected Response getContent(final String rangeValue, 169 final RdfStream rdfStream) throws IOException { 170 return getContent(rangeValue, -1, rdfStream); 171 } 172 173 /** 174 * This method returns an HTTP response with content body appropriate to the following arguments. 175 * 176 * @param rangeValue starting and ending byte offsets, see {@link Range} 177 * @param limit is the number of child resources returned in the response, -1 for all 178 * @param rdfStream to which response RDF will be concatenated 179 * @return HTTP response 180 * @throws IOException in case of error extracting content 181 */ 182 protected Response getContent(final String rangeValue, 183 final int limit, 184 final RdfStream rdfStream) throws IOException { 185 186 final RdfNamespacedStream outputStream; 187 188 if (resource() instanceof FedoraBinary) { 189 190 final MediaType mediaType = MediaType.valueOf(((FedoraBinary) resource()).getMimeType()); 191 192 if (isExternalBody(mediaType)) { 193 return temporaryRedirect(URI.create(mediaType.getParameters().get("URL"))).build(); 194 } 195 196 return getBinaryContent(rangeValue); 197 } else { 198 outputStream = new RdfNamespacedStream( 199 new DefaultRdfStream(rdfStream.topic(), concat(rdfStream, 200 getResourceTriples(limit))), 201 session.getFedoraSession().getNamespaces()); 202 if (prefer != null) { 203 prefer.getReturn().addResponseHeaders(servletResponse); 204 } 205 } 206 servletResponse.addHeader("Vary", "Accept, Range, Accept-Encoding, Accept-Language"); 207 208 return ok(outputStream).build(); 209 } 210 211 protected boolean isExternalBody(final MediaType mediaType) { 212 return MESSAGE_EXTERNAL_BODY.isCompatible(mediaType) && 213 mediaType.getParameters().containsKey("access-type") && 214 mediaType.getParameters().get("access-type").equals("URL") && 215 mediaType.getParameters().containsKey("URL"); 216 } 217 218 protected RdfStream getResourceTriples() { 219 return getResourceTriples(-1); 220 } 221 222 /** 223 * This method returns a stream of RDF triples associated with this target resource 224 * 225 * @param limit is the number of child resources returned in the response, -1 for all 226 * @return {@link RdfStream} 227 */ 228 protected RdfStream getResourceTriples(final int limit) { 229 // use the thing described, not the description, for the subject of descriptive triples 230 if (resource() instanceof NonRdfSourceDescription) { 231 resource = resource().getDescribedResource(); 232 } 233 final PreferTag returnPreference; 234 235 if (prefer != null && prefer.hasReturn()) { 236 returnPreference = prefer.getReturn(); 237 } else if (prefer != null && prefer.hasHandling()) { 238 returnPreference = prefer.getHandling(); 239 } else { 240 returnPreference = PreferTag.emptyTag(); 241 } 242 243 final LdpPreferTag ldpPreferences = new LdpPreferTag(returnPreference); 244 245 final Predicate<Triple> tripleFilter = ldpPreferences.prefersServerManaged() ? x -> true : 246 IS_MANAGED_TRIPLE.negate(); 247 248 final List<Stream<Triple>> streams = new ArrayList<>(); 249 250 251 if (returnPreference.getValue().equals("minimal")) { 252 streams.add(getTriples(of(PROPERTIES, MINIMAL)).filter(tripleFilter)); 253 254 if (ldpPreferences.prefersServerManaged()) { 255 streams.add(getTriples(of(SERVER_MANAGED, MINIMAL))); 256 } 257 } else { 258 streams.add(getTriples(PROPERTIES).filter(tripleFilter)); 259 260 // Additional server-managed triples about this resource 261 if (ldpPreferences.prefersServerManaged()) { 262 streams.add(getTriples(SERVER_MANAGED)); 263 } 264 265 // containment triples about this resource 266 if (ldpPreferences.prefersContainment()) { 267 if (limit == -1) { 268 streams.add(getTriples(LDP_CONTAINMENT)); 269 } else { 270 streams.add(getTriples(LDP_CONTAINMENT).limit(limit)); 271 } 272 } 273 274 // LDP container membership triples for this resource 275 if (ldpPreferences.prefersMembership()) { 276 streams.add(getTriples(LDP_MEMBERSHIP)); 277 } 278 279 // Include inbound references to this object 280 if (ldpPreferences.prefersReferences()) { 281 streams.add(getTriples(INBOUND_REFERENCES)); 282 } 283 284 // Embed the children of this object 285 if (ldpPreferences.prefersEmbed()) { 286 streams.add(getTriples(EMBED_RESOURCES)); 287 } 288 } 289 290 final RdfStream rdfStream = new DefaultRdfStream( 291 asNode(resource()), streams.stream().reduce(empty(), Stream::concat)); 292 293 if (httpTripleUtil != null && ldpPreferences.prefersServerManaged()) { 294 return httpTripleUtil.addHttpComponentModelsForResourceToStream(rdfStream, resource(), uriInfo, 295 translator()); 296 } 297 298 return rdfStream; 299 } 300 301 /** 302 * Get the binary content of a datastream 303 * 304 * @param rangeValue the range value 305 * @return Binary blob 306 * @throws IOException if io exception occurred 307 */ 308 protected Response getBinaryContent(final String rangeValue) 309 throws IOException { 310 final FedoraBinary binary = (FedoraBinary)resource(); 311 312 // we include an explicit etag, because the default behavior is to use the JCR node's etag, not 313 // the jcr:content node digest. The etag is only included if we are not within a transaction. 314 if (!session().isBatchSession()) { 315 checkCacheControlHeaders(request, servletResponse, binary, session()); 316 } 317 final CacheControl cc = new CacheControl(); 318 cc.setMaxAge(0); 319 cc.setMustRevalidate(true); 320 Response.ResponseBuilder builder; 321 322 if (rangeValue != null && rangeValue.startsWith("bytes")) { 323 324 final Range range = Range.convert(rangeValue); 325 326 final long contentSize = binary.getContentSize(); 327 328 final String endAsString; 329 330 if (range.end() == -1) { 331 endAsString = Long.toString(contentSize - 1); 332 } else { 333 endAsString = Long.toString(range.end()); 334 } 335 336 final String contentRangeValue = 337 String.format("bytes %s-%s/%s", range.start(), 338 endAsString, contentSize); 339 340 if (range.end() > contentSize || 341 (range.end() == -1 && range.start() > contentSize)) { 342 343 builder = status(REQUESTED_RANGE_NOT_SATISFIABLE) 344 .header("Content-Range", contentRangeValue); 345 } else { 346 @SuppressWarnings("resource") 347 final RangeRequestInputStream rangeInputStream = 348 new RangeRequestInputStream(binary.getContent(), range.start(), range.size()); 349 350 builder = status(PARTIAL_CONTENT).entity(rangeInputStream) 351 .header("Content-Range", contentRangeValue) 352 .header(CONTENT_LENGTH, range.size()); 353 } 354 355 } else { 356 @SuppressWarnings("resource") 357 final InputStream content = binary.getContent(); 358 builder = ok(content); 359 } 360 361 362 // we set the content-type explicitly to avoid content-negotiation from getting in the way 363 return builder.type(binary.getMimeType()) 364 .cacheControl(cc) 365 .build(); 366 367 } 368 369 protected RdfStream getTriples(final Set<? extends TripleCategory> x) { 370 return getTriples(resource(), x); 371 } 372 373 protected RdfStream getTriples(final FedoraResource resource, final Set<? extends TripleCategory> x) { 374 return resource.getTriples(translator(), x); 375 } 376 377 protected RdfStream getTriples(final TripleCategory x) { 378 return getTriples(resource(), x); 379 } 380 381 protected RdfStream getTriples(final FedoraResource resource, final TripleCategory x) { 382 return resource.getTriples(translator(), x); 383 } 384 385 protected URI getUri(final FedoraResource resource) { 386 try { 387 final String uri = translator().reverse().convert(resource).getURI(); 388 return new URI(uri); 389 } catch (final URISyntaxException e) { 390 throw new BadRequestException(e); 391 } 392 } 393 394 protected FedoraResource resource() { 395 if (resource == null) { 396 resource = getResourceFromPath(externalPath()); 397 } 398 return resource; 399 } 400 401 protected void addResourceLinkHeaders(final FedoraResource resource) { 402 addResourceLinkHeaders(resource, false); 403 } 404 405 protected void addResourceLinkHeaders(final FedoraResource resource, final boolean includeAnchor) { 406 if (resource instanceof NonRdfSourceDescription) { 407 final URI uri = getUri(resource.getDescribedResource()); 408 final Link link = Link.fromUri(uri).rel("describes").build(); 409 servletResponse.addHeader(LINK, link.toString()); 410 } else if (resource instanceof FedoraBinary) { 411 final URI uri = getUri(resource.getDescription()); 412 final Link.Builder builder = Link.fromUri(uri).rel("describedby"); 413 414 if (includeAnchor) { 415 builder.param("anchor", getUri(resource).toString()); 416 } 417 servletResponse.addHeader(LINK, builder.build().toString()); 418 } 419 } 420 421 /** 422 * Add any resource-specific headers to the response 423 * @param resource the resource 424 */ 425 protected void addResourceHttpHeaders(final FedoraResource resource) { 426 if (resource instanceof FedoraBinary) { 427 final FedoraBinary binary = (FedoraBinary)resource; 428 final Date createdDate = binary.getCreatedDate() != null ? Date.from(binary.getCreatedDate()) : null; 429 final Date modDate = binary.getLastModifiedDate() != null ? Date.from(binary.getLastModifiedDate()) : null; 430 431 final ContentDisposition contentDisposition = ContentDisposition.type("attachment") 432 .fileName(binary.getFilename()) 433 .creationDate(createdDate) 434 .modificationDate(modDate) 435 .size(binary.getContentSize()) 436 .build(); 437 438 servletResponse.addHeader(CONTENT_TYPE, binary.getMimeType()); 439 servletResponse.addHeader(CONTENT_LENGTH, String.valueOf(binary.getContentSize())); 440 servletResponse.addHeader("Accept-Ranges", "bytes"); 441 servletResponse.addHeader(CONTENT_DISPOSITION, contentDisposition.toString()); 442 } 443 444 servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "Resource>;rel=\"type\""); 445 446 if (resource instanceof FedoraBinary) { 447 servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "NonRDFSource>;rel=\"type\""); 448 } else if (resource instanceof Container) { 449 servletResponse.addHeader(LINK, "<" + CONTAINER.getURI() + ">;rel=\"type\""); 450 if (resource.hasType(LDP_BASIC_CONTAINER)) { 451 servletResponse.addHeader(LINK, "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 452 } else if (resource.hasType(LDP_DIRECT_CONTAINER)) { 453 servletResponse.addHeader(LINK, "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 454 } else if (resource.hasType(LDP_INDIRECT_CONTAINER)) { 455 servletResponse.addHeader(LINK, "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 456 } else { 457 servletResponse.addHeader(LINK, "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 458 } 459 } else { 460 servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "RDFSource>;rel=\"type\""); 461 } 462 if (httpHeaderInject != null) { 463 httpHeaderInject.addHttpHeaderToResponseStream(servletResponse, uriInfo, resource()); 464 } 465 466 } 467 468 /** 469 * Evaluate the cache control headers for the request to see if it can be served from 470 * the cache. 471 * 472 * @param request the request 473 * @param servletResponse the servlet response 474 * @param resource the fedora resource 475 * @param session the session 476 */ 477 protected void checkCacheControlHeaders(final Request request, 478 final HttpServletResponse servletResponse, 479 final FedoraResource resource, 480 final HttpSession session) { 481 evaluateRequestPreconditions(request, servletResponse, resource, session, true); 482 addCacheControlHeaders(servletResponse, resource, session); 483 } 484 485 /** 486 * Add ETag and Last-Modified cache control headers to the response 487 * <p> 488 * Note: In this implementation, the HTTP headers for ETags and Last-Modified dates are swapped 489 * for fedora:Binary resources and their descriptions. Here, we are drawing a distinction between 490 * the HTTP resource and the LDP resource. As an HTTP resource, the last-modified header should 491 * reflect when the resource at the given URL was last changed. With fedora:Binary resources and 492 * their descriptions, this is a little complicated, for the descriptions have, as their subjects, 493 * the binary itself. And the fedora:lastModified property produced by that NonRdfSourceDescription 494 * refers to the last-modified date of the binary -- not the last-modified date of the 495 * NonRdfSourceDescription. 496 * </p> 497 * @param servletResponse the servlet response 498 * @param resource the fedora resource 499 * @param session the session 500 */ 501 protected void addCacheControlHeaders(final HttpServletResponse servletResponse, 502 final FedoraResource resource, 503 final HttpSession session) { 504 505 if (session.isBatchSession()) { 506 // Do not add caching headers if in a transaction 507 return; 508 } 509 510 final EntityTag etag; 511 final Instant date; 512 513 // See note about this code in the javadoc above. 514 if (resource instanceof FedoraBinary) { 515 // Use a strong ETag for LDP-NR 516 etag = new EntityTag(resource.getDescription().getEtagValue()); 517 date = resource.getDescription().getLastModifiedDate(); 518 } else { 519 // Use a weak ETag for the LDP-RS 520 etag = new EntityTag(resource.getDescribedResource().getEtagValue(), true); 521 date = resource.getDescribedResource().getLastModifiedDate(); 522 } 523 524 if (!etag.getValue().isEmpty()) { 525 servletResponse.addHeader("ETag", etag.toString()); 526 } 527 528 if (date != null) { 529 servletResponse.addDateHeader("Last-Modified", date.toEpochMilli()); 530 } 531 } 532 533 /** 534 * Evaluate request preconditions to ensure the resource is the expected state 535 * @param request the request 536 * @param servletResponse the servlet response 537 * @param resource the resource 538 * @param session the session 539 */ 540 protected void evaluateRequestPreconditions(final Request request, 541 final HttpServletResponse servletResponse, 542 final FedoraResource resource, 543 final HttpSession session) { 544 evaluateRequestPreconditions(request, servletResponse, resource, session, false); 545 } 546 547 @VisibleForTesting 548 void evaluateRequestPreconditions(final Request request, 549 final HttpServletResponse servletResponse, 550 final FedoraResource resource, 551 final HttpSession session, 552 final boolean cacheControl) { 553 554 if (session.isBatchSession()) { 555 // Force cache revalidation if in a transaction 556 servletResponse.addHeader(CACHE_CONTROL, "must-revalidate"); 557 servletResponse.addHeader(CACHE_CONTROL, "max-age=0"); 558 return; 559 } 560 561 final EntityTag etag; 562 final Instant date; 563 Instant roundedDate = Instant.now(); 564 565 // See the related note about the next block of code in the 566 // ContentExposingResource::addCacheControlHeaders method 567 if (resource instanceof FedoraBinary) { 568 // Use a strong ETag for the LDP-NR 569 etag = new EntityTag(resource.getDescription().getEtagValue()); 570 date = resource.getDescription().getLastModifiedDate(); 571 } else { 572 // Use a strong ETag for the LDP-RS when validating If-(None)-Match headers 573 etag = new EntityTag(resource.getDescribedResource().getEtagValue()); 574 date = resource.getDescribedResource().getLastModifiedDate(); 575 } 576 577 if (date != null) { 578 roundedDate = date.minusMillis(date.toEpochMilli() % 1000); 579 } 580 581 Response.ResponseBuilder builder = request.evaluatePreconditions(etag); 582 if ( builder == null ) { 583 builder = request.evaluatePreconditions(Date.from(roundedDate)); 584 } 585 586 if (builder != null && cacheControl ) { 587 final CacheControl cc = new CacheControl(); 588 cc.setMaxAge(0); 589 cc.setMustRevalidate(true); 590 // here we are implicitly emitting a 304 591 // the exception is not an error, it's genuinely 592 // an exceptional condition 593 builder = builder.cacheControl(cc).lastModified(Date.from(roundedDate)).tag(etag); 594 } 595 596 if (builder != null) { 597 final Response response = builder.build(); 598 final Object message = response.getEntity(); 599 throw new PreconditionException(message != null ? message.toString() 600 : "Request failed due to unspecified failed precondition.", response.getStatus()); 601 } 602 } 603 604 protected static MediaType getSimpleContentType(final MediaType requestContentType) { 605 return requestContentType != null ? new MediaType(requestContentType.getType(), requestContentType.getSubtype()) 606 : APPLICATION_OCTET_STREAM_TYPE; 607 } 608 609 protected static boolean isRdfContentType(final String contentTypeString) { 610 return contentTypeToLang(contentTypeString) != null; 611 } 612 613 protected void replaceResourceBinaryWithStream(final FedoraBinary result, 614 final InputStream requestBodyStream, 615 final ContentDisposition contentDisposition, 616 final MediaType contentType, 617 final Collection<String> checksums) throws InvalidChecksumException { 618 final Collection<URI> checksumURIs = checksums == null ? 619 new HashSet<>() : checksums.stream().map(checksum -> checksumURI(checksum)).collect(Collectors.toSet()); 620 final String originalFileName = contentDisposition != null ? contentDisposition.getFileName() : ""; 621 final String originalContentType = contentType != null ? contentType.toString() : ""; 622 623 result.setContent(requestBodyStream, 624 originalContentType, 625 checksumURIs, 626 originalFileName, 627 storagePolicyDecisionPoint); 628 } 629 630 protected void replaceResourceWithStream(final FedoraResource resource, 631 final InputStream requestBodyStream, 632 final MediaType contentType, 633 final RdfStream resourceTriples) throws MalformedRdfException { 634 final Lang format = contentTypeToLang(contentType.toString()); 635 636 final Model inputModel = createDefaultModel(); 637 try { 638 inputModel.read(requestBodyStream, getUri(resource).toString(), format.getName().toUpperCase()); 639 } catch (final RiotException e) { 640 throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e); 641 642 } catch (final RuntimeIOException e) { 643 if (e.getCause() instanceof JsonParseException) { 644 throw new MalformedRdfException(e.getCause()); 645 } 646 throw new RepositoryRuntimeException(e); 647 } 648 649 ensureValidMemberRelation(inputModel); 650 651 resource.replaceProperties(translator(), inputModel, resourceTriples); 652 } 653 654 /** 655 * This method throws an exception if the arg model contains a triple with 'ldp:hasMemberRelation' as a predicate 656 * and a server-managed property as the object. 657 * 658 * @param inputModel to be checked 659 * @throws ServerManagedPropertyException 660 */ 661 private void ensureValidMemberRelation(final Model inputModel) throws BadRequestException { 662 // check that ldp:hasMemberRelation value is not server managed predicate. 663 inputModel.listStatements().forEachRemaining((Statement s) -> { 664 LOGGER.debug("statement: s={}, p={}, o={}", s.getSubject(), s.getPredicate(), s.getObject()); 665 666 if (s.getPredicate().equals(HAS_MEMBER_RELATION)) { 667 final RDFNode obj = s.getObject(); 668 if (obj.isURIResource()) { 669 final String uri = obj.asResource().getURI(); 670 671 // Throw exception if object is a server-managed property 672 if (isManagedPredicate.test(createProperty(uri))) { 673 throw new ServerManagedPropertyException( 674 MessageFormat.format( 675 "{0} cannot take a server managed property " + 676 "as an object: property value = {1}.", 677 HAS_MEMBER_RELATION, uri)); 678 } 679 } 680 } 681 }); 682 } 683 684 protected void patchResourcewithSparql(final FedoraResource resource, 685 final String requestBody, 686 final RdfStream resourceTriples) { 687 resource.getDescribedResource().updateProperties(translator(), requestBody, resourceTriples); 688 } 689 690 /** 691 * Create a checksum URI object. 692 **/ 693 private static URI checksumURI( final String checksum ) { 694 if (!isBlank(checksum)) { 695 return URI.create(checksum); 696 } 697 return null; 698 } 699}