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 } 353 354 } else { 355 @SuppressWarnings("resource") 356 final InputStream content = binary.getContent(); 357 builder = ok(content); 358 } 359 360 361 // we set the content-type explicitly to avoid content-negotiation from getting in the way 362 return builder.type(binary.getMimeType()) 363 .cacheControl(cc) 364 .build(); 365 366 } 367 368 protected RdfStream getTriples(final Set<? extends TripleCategory> x) { 369 return getTriples(resource(), x); 370 } 371 372 protected RdfStream getTriples(final FedoraResource resource, final Set<? extends TripleCategory> x) { 373 return resource.getTriples(translator(), x); 374 } 375 376 protected RdfStream getTriples(final TripleCategory x) { 377 return getTriples(resource(), x); 378 } 379 380 protected RdfStream getTriples(final FedoraResource resource, final TripleCategory x) { 381 return resource.getTriples(translator(), x); 382 } 383 384 protected URI getUri(final FedoraResource resource) { 385 try { 386 final String uri = translator().reverse().convert(resource).getURI(); 387 return new URI(uri); 388 } catch (final URISyntaxException e) { 389 throw new BadRequestException(e); 390 } 391 } 392 393 protected FedoraResource resource() { 394 if (resource == null) { 395 resource = getResourceFromPath(externalPath()); 396 } 397 return resource; 398 } 399 400 protected void addResourceLinkHeaders(final FedoraResource resource) { 401 addResourceLinkHeaders(resource, false); 402 } 403 404 protected void addResourceLinkHeaders(final FedoraResource resource, final boolean includeAnchor) { 405 if (resource instanceof NonRdfSourceDescription) { 406 final URI uri = getUri(resource.getDescribedResource()); 407 final Link link = Link.fromUri(uri).rel("describes").build(); 408 servletResponse.addHeader(LINK, link.toString()); 409 } else if (resource instanceof FedoraBinary) { 410 final URI uri = getUri(resource.getDescription()); 411 final Link.Builder builder = Link.fromUri(uri).rel("describedby"); 412 413 if (includeAnchor) { 414 builder.param("anchor", getUri(resource).toString()); 415 } 416 servletResponse.addHeader(LINK, builder.build().toString()); 417 } 418 } 419 420 /** 421 * Add any resource-specific headers to the response 422 * @param resource the resource 423 */ 424 protected void addResourceHttpHeaders(final FedoraResource resource) { 425 if (resource instanceof FedoraBinary) { 426 final FedoraBinary binary = (FedoraBinary)resource; 427 final Date createdDate = binary.getCreatedDate() != null ? Date.from(binary.getCreatedDate()) : null; 428 final Date modDate = binary.getLastModifiedDate() != null ? Date.from(binary.getLastModifiedDate()) : null; 429 430 final ContentDisposition contentDisposition = ContentDisposition.type("attachment") 431 .fileName(binary.getFilename()) 432 .creationDate(createdDate) 433 .modificationDate(modDate) 434 .size(binary.getContentSize()) 435 .build(); 436 437 servletResponse.addHeader(CONTENT_TYPE, binary.getMimeType()); 438 servletResponse.addHeader(CONTENT_LENGTH, String.valueOf(binary.getContentSize())); 439 servletResponse.addHeader("Accept-Ranges", "bytes"); 440 servletResponse.addHeader(CONTENT_DISPOSITION, contentDisposition.toString()); 441 } 442 443 servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "Resource>;rel=\"type\""); 444 445 if (resource instanceof FedoraBinary) { 446 servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "NonRDFSource>;rel=\"type\""); 447 } else if (resource instanceof Container) { 448 servletResponse.addHeader(LINK, "<" + CONTAINER.getURI() + ">;rel=\"type\""); 449 if (resource.hasType(LDP_BASIC_CONTAINER)) { 450 servletResponse.addHeader(LINK, "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 451 } else if (resource.hasType(LDP_DIRECT_CONTAINER)) { 452 servletResponse.addHeader(LINK, "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 453 } else if (resource.hasType(LDP_INDIRECT_CONTAINER)) { 454 servletResponse.addHeader(LINK, "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 455 } else { 456 servletResponse.addHeader(LINK, "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 457 } 458 } else { 459 servletResponse.addHeader(LINK, "<" + LDP_NAMESPACE + "RDFSource>;rel=\"type\""); 460 } 461 if (httpHeaderInject != null) { 462 httpHeaderInject.addHttpHeaderToResponseStream(servletResponse, uriInfo, resource()); 463 } 464 465 } 466 467 /** 468 * Evaluate the cache control headers for the request to see if it can be served from 469 * the cache. 470 * 471 * @param request the request 472 * @param servletResponse the servlet response 473 * @param resource the fedora resource 474 * @param session the session 475 */ 476 protected void checkCacheControlHeaders(final Request request, 477 final HttpServletResponse servletResponse, 478 final FedoraResource resource, 479 final HttpSession session) { 480 evaluateRequestPreconditions(request, servletResponse, resource, session, true); 481 addCacheControlHeaders(servletResponse, resource, session); 482 } 483 484 /** 485 * Add ETag and Last-Modified cache control headers to the response 486 * <p> 487 * Note: In this implementation, the HTTP headers for ETags and Last-Modified dates are swapped 488 * for fedora:Binary resources and their descriptions. Here, we are drawing a distinction between 489 * the HTTP resource and the LDP resource. As an HTTP resource, the last-modified header should 490 * reflect when the resource at the given URL was last changed. With fedora:Binary resources and 491 * their descriptions, this is a little complicated, for the descriptions have, as their subjects, 492 * the binary itself. And the fedora:lastModified property produced by that NonRdfSourceDescription 493 * refers to the last-modified date of the binary -- not the last-modified date of the 494 * NonRdfSourceDescription. 495 * </p> 496 * @param servletResponse the servlet response 497 * @param resource the fedora resource 498 * @param session the session 499 */ 500 protected void addCacheControlHeaders(final HttpServletResponse servletResponse, 501 final FedoraResource resource, 502 final HttpSession session) { 503 504 if (session.isBatchSession()) { 505 // Do not add caching headers if in a transaction 506 return; 507 } 508 509 final EntityTag etag; 510 final Instant date; 511 512 // See note about this code in the javadoc above. 513 if (resource instanceof FedoraBinary) { 514 // Use a strong ETag for LDP-NR 515 etag = new EntityTag(resource.getDescription().getEtagValue()); 516 date = resource.getDescription().getLastModifiedDate(); 517 } else { 518 // Use a weak ETag for the LDP-RS 519 etag = new EntityTag(resource.getDescribedResource().getEtagValue(), true); 520 date = resource.getDescribedResource().getLastModifiedDate(); 521 } 522 523 if (!etag.getValue().isEmpty()) { 524 servletResponse.addHeader("ETag", etag.toString()); 525 } 526 527 if (date != null) { 528 servletResponse.addDateHeader("Last-Modified", date.toEpochMilli()); 529 } 530 } 531 532 /** 533 * Evaluate request preconditions to ensure the resource is the expected state 534 * @param request the request 535 * @param servletResponse the servlet response 536 * @param resource the resource 537 * @param session the session 538 */ 539 protected void evaluateRequestPreconditions(final Request request, 540 final HttpServletResponse servletResponse, 541 final FedoraResource resource, 542 final HttpSession session) { 543 evaluateRequestPreconditions(request, servletResponse, resource, session, false); 544 } 545 546 @VisibleForTesting 547 void evaluateRequestPreconditions(final Request request, 548 final HttpServletResponse servletResponse, 549 final FedoraResource resource, 550 final HttpSession session, 551 final boolean cacheControl) { 552 553 if (session.isBatchSession()) { 554 // Force cache revalidation if in a transaction 555 servletResponse.addHeader(CACHE_CONTROL, "must-revalidate"); 556 servletResponse.addHeader(CACHE_CONTROL, "max-age=0"); 557 return; 558 } 559 560 final EntityTag etag; 561 final Instant date; 562 Instant roundedDate = Instant.now(); 563 564 // See the related note about the next block of code in the 565 // ContentExposingResource::addCacheControlHeaders method 566 if (resource instanceof FedoraBinary) { 567 // Use a strong ETag for the LDP-NR 568 etag = new EntityTag(resource.getDescription().getEtagValue()); 569 date = resource.getDescription().getLastModifiedDate(); 570 } else { 571 // Use a strong ETag for the LDP-RS when validating If-(None)-Match headers 572 etag = new EntityTag(resource.getDescribedResource().getEtagValue()); 573 date = resource.getDescribedResource().getLastModifiedDate(); 574 } 575 576 if (date != null) { 577 roundedDate = date.minusMillis(date.toEpochMilli() % 1000); 578 } 579 580 Response.ResponseBuilder builder = request.evaluatePreconditions(etag); 581 if ( builder == null ) { 582 builder = request.evaluatePreconditions(Date.from(roundedDate)); 583 } 584 585 if (builder != null && cacheControl ) { 586 final CacheControl cc = new CacheControl(); 587 cc.setMaxAge(0); 588 cc.setMustRevalidate(true); 589 // here we are implicitly emitting a 304 590 // the exception is not an error, it's genuinely 591 // an exceptional condition 592 builder = builder.cacheControl(cc).lastModified(Date.from(roundedDate)).tag(etag); 593 } 594 595 if (builder != null) { 596 final Response response = builder.build(); 597 final Object message = response.getEntity(); 598 throw new PreconditionException(message != null ? message.toString() 599 : "Request failed due to unspecified failed precondition.", response.getStatus()); 600 } 601 } 602 603 protected static MediaType getSimpleContentType(final MediaType requestContentType) { 604 return requestContentType != null ? new MediaType(requestContentType.getType(), requestContentType.getSubtype()) 605 : APPLICATION_OCTET_STREAM_TYPE; 606 } 607 608 protected static boolean isRdfContentType(final String contentTypeString) { 609 return contentTypeToLang(contentTypeString) != null; 610 } 611 612 protected void replaceResourceBinaryWithStream(final FedoraBinary result, 613 final InputStream requestBodyStream, 614 final ContentDisposition contentDisposition, 615 final MediaType contentType, 616 final Collection<String> checksums) throws InvalidChecksumException { 617 final Collection<URI> checksumURIs = checksums == null ? 618 new HashSet<>() : checksums.stream().map(checksum -> checksumURI(checksum)).collect(Collectors.toSet()); 619 final String originalFileName = contentDisposition != null ? contentDisposition.getFileName() : ""; 620 final String originalContentType = contentType != null ? contentType.toString() : ""; 621 622 result.setContent(requestBodyStream, 623 originalContentType, 624 checksumURIs, 625 originalFileName, 626 storagePolicyDecisionPoint); 627 } 628 629 protected void replaceResourceWithStream(final FedoraResource resource, 630 final InputStream requestBodyStream, 631 final MediaType contentType, 632 final RdfStream resourceTriples) throws MalformedRdfException { 633 final Lang format = contentTypeToLang(contentType.toString()); 634 635 final Model inputModel = createDefaultModel(); 636 try { 637 inputModel.read(requestBodyStream, getUri(resource).toString(), format.getName().toUpperCase()); 638 } catch (final RiotException e) { 639 throw new BadRequestException("RDF was not parsable: " + e.getMessage(), e); 640 641 } catch (final RuntimeIOException e) { 642 if (e.getCause() instanceof JsonParseException) { 643 throw new MalformedRdfException(e.getCause()); 644 } 645 throw new RepositoryRuntimeException(e); 646 } 647 648 ensureValidMemberRelation(inputModel); 649 650 resource.replaceProperties(translator(), inputModel, resourceTriples); 651 } 652 653 /** 654 * This method throws an exception if the arg model contains a triple with 'ldp:hasMemberRelation' as a predicate 655 * and a server-managed property as the object. 656 * 657 * @param inputModel to be checked 658 * @throws ServerManagedPropertyException 659 */ 660 private void ensureValidMemberRelation(final Model inputModel) throws BadRequestException { 661 // check that ldp:hasMemberRelation value is not server managed predicate. 662 inputModel.listStatements().forEachRemaining((Statement s) -> { 663 LOGGER.debug("statement: s={}, p={}, o={}", s.getSubject(), s.getPredicate(), s.getObject()); 664 665 if (s.getPredicate().equals(HAS_MEMBER_RELATION)) { 666 final RDFNode obj = s.getObject(); 667 if (obj.isURIResource()) { 668 final String uri = obj.asResource().getURI(); 669 670 // Throw exception if object is a server-managed property 671 if (isManagedPredicate.test(createProperty(uri))) { 672 throw new ServerManagedPropertyException( 673 MessageFormat.format( 674 "{0} cannot take a server managed property " + 675 "as an object: property value = {1}.", 676 HAS_MEMBER_RELATION, uri)); 677 } 678 } 679 } 680 }); 681 } 682 683 protected void patchResourcewithSparql(final FedoraResource resource, 684 final String requestBody, 685 final RdfStream resourceTriples) { 686 resource.getDescribedResource().updateProperties(translator(), requestBody, resourceTriples); 687 } 688 689 /** 690 * Create a checksum URI object. 691 **/ 692 private static URI checksumURI( final String checksum ) { 693 if (!isBlank(checksum)) { 694 return URI.create(checksum); 695 } 696 return null; 697 } 698}