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.google.common.base.Predicates.alwaysTrue; 020import static com.google.common.base.Predicates.and; 021import static com.google.common.base.Predicates.not; 022import static com.google.common.collect.Iterators.concat; 023import static com.google.common.collect.Iterators.filter; 024import static com.google.common.collect.Iterators.transform; 025import static com.hp.hpl.jena.rdf.model.ModelFactory.createDefaultModel; 026import static javax.ws.rs.core.HttpHeaders.CACHE_CONTROL; 027import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE; 028import static javax.ws.rs.core.Response.ok; 029import static javax.ws.rs.core.Response.status; 030import static javax.ws.rs.core.Response.temporaryRedirect; 031import static javax.ws.rs.core.Response.Status.PARTIAL_CONTENT; 032import static javax.ws.rs.core.Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE; 033import static org.apache.commons.lang.StringUtils.isBlank; 034import static org.apache.jena.riot.RDFLanguages.contentTypeToLang; 035import static org.fcrepo.kernel.FedoraJcrTypes.LDP_BASIC_CONTAINER; 036import static org.fcrepo.kernel.FedoraJcrTypes.LDP_DIRECT_CONTAINER; 037import static org.fcrepo.kernel.FedoraJcrTypes.LDP_INDIRECT_CONTAINER; 038import static org.fcrepo.kernel.RdfLexicon.BASIC_CONTAINER; 039import static org.fcrepo.kernel.RdfLexicon.CONTAINER; 040import static org.fcrepo.kernel.RdfLexicon.DIRECT_CONTAINER; 041import static org.fcrepo.kernel.RdfLexicon.INDIRECT_CONTAINER; 042import static org.fcrepo.kernel.RdfLexicon.LDP_NAMESPACE; 043import static org.fcrepo.kernel.RdfLexicon.isManagedNamespace; 044 045import java.io.IOException; 046import java.io.InputStream; 047import java.net.URI; 048import java.net.URISyntaxException; 049import java.util.Date; 050import java.util.Iterator; 051 052import javax.inject.Inject; 053import javax.jcr.AccessDeniedException; 054import javax.jcr.Binary; 055import javax.jcr.RepositoryException; 056import javax.jcr.Session; 057import javax.servlet.http.HttpServletResponse; 058import javax.ws.rs.BadRequestException; 059import javax.ws.rs.BeanParam; 060import javax.ws.rs.WebApplicationException; 061import javax.ws.rs.core.CacheControl; 062import javax.ws.rs.core.Context; 063import javax.ws.rs.core.EntityTag; 064import javax.ws.rs.core.MediaType; 065import javax.ws.rs.core.Request; 066import javax.ws.rs.core.Response; 067 068import org.fcrepo.http.commons.api.rdf.HttpTripleUtil; 069import org.fcrepo.http.commons.domain.MultiPrefer; 070import org.fcrepo.http.commons.domain.PreferTag; 071import org.fcrepo.http.commons.domain.Range; 072import org.fcrepo.http.commons.domain.ldp.LdpPreferTag; 073import org.fcrepo.http.commons.responses.RangeRequestInputStream; 074import org.fcrepo.kernel.exception.InvalidChecksumException; 075import org.fcrepo.kernel.exception.MalformedRdfException; 076import org.fcrepo.kernel.exception.RepositoryRuntimeException; 077import org.fcrepo.kernel.impl.rdf.ManagedRdf; 078import org.fcrepo.kernel.impl.rdf.impl.AclRdfContext; 079import org.fcrepo.kernel.impl.rdf.impl.BlankNodeRdfContext; 080import org.fcrepo.kernel.impl.rdf.impl.ChildrenRdfContext; 081import org.fcrepo.kernel.impl.rdf.impl.ContentRdfContext; 082import org.fcrepo.kernel.impl.rdf.impl.HashRdfContext; 083import org.fcrepo.kernel.impl.rdf.impl.LdpContainerRdfContext; 084import org.fcrepo.kernel.impl.rdf.impl.LdpIsMemberOfRdfContext; 085import org.fcrepo.kernel.impl.rdf.impl.LdpRdfContext; 086import org.fcrepo.kernel.impl.rdf.impl.ParentRdfContext; 087import org.fcrepo.kernel.impl.rdf.impl.PropertiesRdfContext; 088import org.fcrepo.kernel.impl.rdf.impl.ReferencesRdfContext; 089import org.fcrepo.kernel.impl.rdf.impl.RootRdfContext; 090import org.fcrepo.kernel.impl.rdf.impl.TypeRdfContext; 091import org.fcrepo.kernel.impl.services.TransactionServiceImpl; 092import org.fcrepo.kernel.models.Container; 093import org.fcrepo.kernel.models.FedoraBinary; 094import org.fcrepo.kernel.models.FedoraResource; 095import org.fcrepo.kernel.models.NonRdfSource; 096import org.fcrepo.kernel.models.NonRdfSourceDescription; 097import org.fcrepo.kernel.services.policy.StoragePolicyDecisionPoint; 098import org.fcrepo.kernel.utils.iterators.RdfStream; 099 100import org.apache.jena.riot.Lang; 101import org.glassfish.jersey.media.multipart.ContentDisposition; 102import org.jvnet.hk2.annotations.Optional; 103 104import com.google.common.base.Function; 105import com.google.common.base.Predicate; 106import com.google.common.collect.ImmutableList; 107import com.google.common.collect.Iterators; 108import com.hp.hpl.jena.graph.Triple; 109import com.hp.hpl.jena.rdf.model.Model; 110import com.hp.hpl.jena.rdf.model.Statement; 111import com.hp.hpl.jena.vocabulary.RDF; 112 113/** 114 * An abstract class that sits between AbstractResource and any resource that 115 * wishes to share the routines for building responses containing binary 116 * content. 117 * 118 * @author Mike Durbin 119 */ 120public abstract class ContentExposingResource extends FedoraBaseResource { 121 122 public static final MediaType MESSAGE_EXTERNAL_BODY = MediaType.valueOf("message/external-body"); 123 124 @Context protected Request request; 125 @Context protected HttpServletResponse servletResponse; 126 127 @Inject 128 @Optional 129 private HttpTripleUtil httpTripleUtil; 130 131 @BeanParam 132 protected MultiPrefer prefer; 133 134 @Inject 135 @Optional 136 StoragePolicyDecisionPoint storagePolicyDecisionPoint; 137 138 protected FedoraResource resource; 139 140 private static final long MAX_BUFFER_SIZE = 10240000; 141 142 protected abstract String externalPath(); 143 144 protected Response getContent(final String rangeValue, 145 final RdfStream rdfStream) throws IOException { 146 if (resource() instanceof FedoraBinary) { 147 148 final String contentTypeString = ((FedoraBinary) resource()).getMimeType(); 149 150 final Lang lang = contentTypeToLang(contentTypeString); 151 152 if (!contentTypeString.equals("text/plain") && lang != null) { 153 154 final String format = lang.getName().toUpperCase(); 155 156 final InputStream content = ((FedoraBinary) resource()).getContent(); 157 158 final Model inputModel = createDefaultModel() 159 .read(content, (resource()).toString(), format); 160 161 rdfStream.concat(Iterators.transform(inputModel.listStatements(), 162 new Function<Statement, Triple>() { 163 164 @Override 165 public Triple apply(final Statement input) { 166 return input.asTriple(); 167 } 168 })); 169 } else { 170 171 final MediaType mediaType = MediaType.valueOf(contentTypeString); 172 if (MESSAGE_EXTERNAL_BODY.isCompatible(mediaType) 173 && mediaType.getParameters().containsKey("access-type") 174 && mediaType.getParameters().get("access-type").equals("URL") 175 && mediaType.getParameters().containsKey("URL") ) { 176 try { 177 return temporaryRedirect(new URI(mediaType.getParameters().get("URL"))).build(); 178 } catch (final URISyntaxException e) { 179 throw new RepositoryRuntimeException(e); 180 } 181 } 182 return getBinaryContent(rangeValue); 183 } 184 185 } else { 186 rdfStream.concat(getResourceTriples()); 187 188 if (prefer != null) { 189 prefer.getReturn().addResponseHeaders(servletResponse); 190 } 191 192 } 193 servletResponse.addHeader("Vary", "Accept, Range, Accept-Encoding, Accept-Language"); 194 195 return Response.ok(rdfStream).build(); 196 } 197 198 protected RdfStream getResourceTriples() { 199 200 final PreferTag returnPreference; 201 202 if (prefer != null && prefer.hasReturn()) { 203 returnPreference = prefer.getReturn(); 204 } else if (prefer != null && prefer.hasHandling()) { 205 returnPreference = prefer.getHandling(); 206 } else { 207 returnPreference = PreferTag.emptyTag(); 208 } 209 210 final LdpPreferTag ldpPreferences = new LdpPreferTag(returnPreference); 211 212 final RdfStream rdfStream = new RdfStream(); 213 214 final Predicate<Triple> tripleFilter; 215 if (ldpPreferences.prefersServerManaged()) { 216 tripleFilter = alwaysTrue(); 217 } else { 218 tripleFilter = and(not(ManagedRdf.isManagedTriple), not(new Predicate<Triple>() { 219 @Override 220 public boolean apply(final Triple input) { 221 return input.getPredicate().equals(RDF.type.asNode()) 222 && isManagedNamespace.apply(input.getObject().getNameSpace()); 223 } 224 })); 225 } 226 227 if (ldpPreferences.prefersServerManaged()) { 228 rdfStream.concat(getTriples(LdpRdfContext.class)); 229 } 230 231 rdfStream.concat(filter(getTriples(TypeRdfContext.class), tripleFilter)); 232 233 rdfStream.concat(filter(getTriples(PropertiesRdfContext.class), tripleFilter)); 234 235 if (!returnPreference.getValue().equals("minimal")) { 236 237 // Additional server-managed triples about this resource 238 if (ldpPreferences.prefersServerManaged()) { 239 rdfStream.concat(getTriples(AclRdfContext.class)); 240 rdfStream.concat(getTriples(RootRdfContext.class)); 241 rdfStream.concat(getTriples(ContentRdfContext.class)); 242 rdfStream.concat(getTriples(ParentRdfContext.class)); 243 } 244 245 // containment triples about this resource 246 if (ldpPreferences.prefersContainment()) { 247 rdfStream.concat(getTriples(ChildrenRdfContext.class)); 248 } 249 250 // LDP container membership triples for this resource 251 if (ldpPreferences.prefersMembership()) { 252 rdfStream.concat(getTriples(LdpContainerRdfContext.class)); 253 rdfStream.concat(getTriples(LdpIsMemberOfRdfContext.class)); 254 } 255 256 // Include binary properties if this is a binary description 257 if (resource() instanceof NonRdfSourceDescription) { 258 final FedoraResource described = ((NonRdfSourceDescription) resource()).getDescribedResource(); 259 rdfStream.concat(filter(described.getTriples(translator(), ImmutableList.of(TypeRdfContext.class, 260 PropertiesRdfContext.class, 261 ContentRdfContext.class)), tripleFilter)); 262 } 263 264 // Embed all hash and blank nodes 265 rdfStream.concat(filter(getTriples(HashRdfContext.class), tripleFilter)); 266 rdfStream.concat(filter(getTriples(BlankNodeRdfContext.class), tripleFilter)); 267 268 // Include inbound references to this object 269 if (ldpPreferences.prefersReferences()) { 270 rdfStream.concat(getTriples(ReferencesRdfContext.class)); 271 } 272 273 // Embed the children of this object 274 if (ldpPreferences.prefersEmbed()) { 275 276 final Iterator<FedoraResource> children = resource().getChildren(); 277 278 rdfStream.concat(filter(concat(transform(children, 279 new Function<FedoraResource, RdfStream>() { 280 281 @Override 282 public RdfStream apply(final FedoraResource child) { 283 return child.getTriples(translator(), ImmutableList.of( 284 TypeRdfContext.class, 285 PropertiesRdfContext.class, 286 BlankNodeRdfContext.class)); 287 } 288 })), tripleFilter)); 289 290 } 291 } 292 293 if (httpTripleUtil != null && ldpPreferences.prefersServerManaged()) { 294 httpTripleUtil.addHttpComponentModelsForResourceToStream(rdfStream, resource(), uriInfo, translator()); 295 } 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 final String txId = TransactionServiceImpl.getCurrentTransactionId(session()); 315 if (txId == null) { 316 checkCacheControlHeaders(request, servletResponse, binary, session()); 317 } 318 final CacheControl cc = new CacheControl(); 319 cc.setMaxAge(0); 320 cc.setMustRevalidate(true); 321 Response.ResponseBuilder builder; 322 323 if (rangeValue != null && rangeValue.startsWith("bytes")) { 324 325 final Range range = Range.convert(rangeValue); 326 327 final long contentSize = binary.getContentSize(); 328 329 final String endAsString; 330 331 if (range.end() == -1) { 332 endAsString = Long.toString(contentSize - 1); 333 } else { 334 endAsString = Long.toString(range.end()); 335 } 336 337 final String contentRangeValue = 338 String.format("bytes %s-%s/%s", range.start(), 339 endAsString, contentSize); 340 341 if (range.end() > contentSize || 342 (range.end() == -1 && range.start() > contentSize)) { 343 344 builder = status(REQUESTED_RANGE_NOT_SATISFIABLE) 345 .header("Content-Range", contentRangeValue); 346 } else { 347 final long rangeStart = range.start(); 348 final long rangeSize = range.size() == -1 ? contentSize - rangeStart : range.size(); 349 final long remainingBytes = contentSize - rangeStart; 350 final long bufSize = rangeSize < remainingBytes ? rangeSize : remainingBytes; 351 352 if (bufSize < MAX_BUFFER_SIZE) { 353 // Small size range content retrieval use javax.jcr.Binary to improve performance 354 final byte[] buf = new byte[(int) bufSize]; 355 356 final Binary binaryContent = binary.getBinaryContent(); 357 try { 358 binaryContent.read(buf, rangeStart); 359 } catch (final RepositoryException e1) { 360 throw new RepositoryRuntimeException(e1); 361 } 362 binaryContent.dispose(); 363 364 builder = status(PARTIAL_CONTENT).entity(buf) 365 .header("Content-Range", contentRangeValue); 366 } else { 367 // For large range content retrieval, go with the InputStream class to balance 368 // the memory usage, though this is a rare case in range content retrieval. 369 final InputStream content = binary.getContent(); 370 final RangeRequestInputStream rangeInputStream = 371 new RangeRequestInputStream(content, range.start(), range.size()); 372 373 builder = status(PARTIAL_CONTENT).entity(rangeInputStream) 374 .header("Content-Range", contentRangeValue); 375 } 376 } 377 378 } else { 379 final InputStream content = binary.getContent(); 380 builder = ok(content); 381 } 382 383 384 // we set the content-type explicitly to avoid content-negotiation from getting in the way 385 return builder.type(binary.getMimeType()) 386 .cacheControl(cc) 387 .build(); 388 389 } 390 391 protected RdfStream getTriples(final Class<? extends RdfStream> x) { 392 return getTriples(resource(), x); 393 } 394 395 protected RdfStream getTriples(final FedoraResource resource, final Class<? extends RdfStream> x) { 396 return resource.getTriples(translator(), x); 397 } 398 399 protected URI getUri(final FedoraResource resource) { 400 try { 401 final String uri = translator().reverse().convert(resource).getURI(); 402 return new URI(uri); 403 } catch (final URISyntaxException e) { 404 throw new BadRequestException(e); 405 } 406 } 407 408 protected FedoraResource resource() { 409 if (resource == null) { 410 resource = getResourceFromPath(externalPath()); 411 } 412 413 return resource; 414 } 415 416 417 /** 418 * Add any resource-specific headers to the response 419 * @param resource the resource 420 */ 421 protected void addResourceHttpHeaders(final FedoraResource resource) { 422 if (resource instanceof FedoraBinary) { 423 424 final FedoraBinary binary = (FedoraBinary)resource; 425 final ContentDisposition contentDisposition = ContentDisposition.type("attachment") 426 .fileName(binary.getFilename()) 427 .creationDate(binary.getCreatedDate()) 428 .modificationDate(binary.getLastModifiedDate()) 429 .size(binary.getContentSize()) 430 .build(); 431 432 servletResponse.addHeader("Content-Type", binary.getMimeType()); 433 servletResponse.addHeader("Content-Length", String.valueOf(binary.getContentSize())); 434 servletResponse.addHeader("Accept-Ranges", "bytes"); 435 servletResponse.addHeader("Content-Disposition", contentDisposition.toString()); 436 } 437 438 servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "Resource>;rel=\"type\""); 439 440 if (resource instanceof NonRdfSource) { 441 servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "NonRDFSource>;rel=\"type\""); 442 } else if (resource instanceof Container) { 443 servletResponse.addHeader("Link", "<" + CONTAINER.getURI() + ">;rel=\"type\""); 444 if (resource.hasType(LDP_BASIC_CONTAINER)) { 445 servletResponse.addHeader("Link", "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 446 } else if (resource.hasType(LDP_DIRECT_CONTAINER)) { 447 servletResponse.addHeader("Link", "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 448 } else if (resource.hasType(LDP_INDIRECT_CONTAINER)) { 449 servletResponse.addHeader("Link", "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 450 } else { 451 servletResponse.addHeader("Link", "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 452 } 453 } else { 454 servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "RDFSource>;rel=\"type\""); 455 } 456 457 } 458 459 /** 460 * Evaluate the cache control headers for the request to see if it can be served from 461 * the cache. 462 * 463 * @param request the request 464 * @param servletResponse the servlet response 465 * @param resource the fedora resource 466 * @param session the session 467 */ 468 protected static void checkCacheControlHeaders(final Request request, 469 final HttpServletResponse servletResponse, 470 final FedoraResource resource, 471 final Session session) { 472 evaluateRequestPreconditions(request, servletResponse, resource, session, true); 473 addCacheControlHeaders(servletResponse, resource, session); 474 } 475 476 /** 477 * Add ETag and Last-Modified cache control headers to the response 478 * @param servletResponse the servlet response 479 * @param resource the fedora resource 480 * @param session the session 481 */ 482 protected static void addCacheControlHeaders(final HttpServletResponse servletResponse, 483 final FedoraResource resource, 484 final Session session) { 485 486 final String txId = TransactionServiceImpl.getCurrentTransactionId(session); 487 if (txId != null) { 488 // Do not add caching headers if in a transaction 489 return; 490 } 491 492 final EntityTag etag = new EntityTag(resource.getEtagValue()); 493 final Date date = resource.getLastModifiedDate(); 494 495 if (!etag.getValue().isEmpty()) { 496 servletResponse.addHeader("ETag", etag.toString()); 497 } 498 499 if (date != null) { 500 servletResponse.addDateHeader("Last-Modified", date.getTime()); 501 } 502 } 503 504 /** 505 * Evaluate request preconditions to ensure the resource is the expected state 506 * @param request the request 507 * @param servletResponse the servlet response 508 * @param resource the resource 509 * @param session the session 510 */ 511 protected static void evaluateRequestPreconditions(final Request request, 512 final HttpServletResponse servletResponse, 513 final FedoraResource resource, 514 final Session session) { 515 evaluateRequestPreconditions(request, servletResponse, resource, session, false); 516 } 517 518 private static void evaluateRequestPreconditions(final Request request, 519 final HttpServletResponse servletResponse, 520 final FedoraResource resource, 521 final Session session, 522 final boolean cacheControl) { 523 524 final String txId = TransactionServiceImpl.getCurrentTransactionId(session); 525 if (txId != null) { 526 // Force cache revalidation if in a transaction 527 servletResponse.addHeader(CACHE_CONTROL, "must-revalidate"); 528 servletResponse.addHeader(CACHE_CONTROL, "max-age=0"); 529 return; 530 } 531 532 final EntityTag etag = new EntityTag(resource.getEtagValue()); 533 final Date date = resource.getLastModifiedDate(); 534 final Date roundedDate = new Date(); 535 536 if (date != null) { 537 roundedDate.setTime(date.getTime() - date.getTime() % 1000); 538 } 539 540 Response.ResponseBuilder builder = request.evaluatePreconditions(etag); 541 if ( builder != null ) { 542 builder = builder.entity("ETag mismatch"); 543 } else { 544 builder = request.evaluatePreconditions(roundedDate); 545 if ( builder != null ) { 546 builder = builder.entity("Date mismatch"); 547 } 548 } 549 550 if (builder != null && cacheControl ) { 551 final CacheControl cc = new CacheControl(); 552 cc.setMaxAge(0); 553 cc.setMustRevalidate(true); 554 // here we are implicitly emitting a 304 555 // the exception is not an error, it's genuinely 556 // an exceptional condition 557 builder = builder.cacheControl(cc).lastModified(date).tag(etag); 558 } 559 if (builder != null) { 560 throw new WebApplicationException(builder.build()); 561 } 562 } 563 564 protected static MediaType getSimpleContentType(final MediaType requestContentType) { 565 return requestContentType != null ? new MediaType(requestContentType.getType(), requestContentType.getSubtype()) 566 : APPLICATION_OCTET_STREAM_TYPE; 567 } 568 569 protected static boolean isRdfContentType(final String contentTypeString) { 570 return contentTypeToLang(contentTypeString) != null; 571 } 572 573 protected void replaceResourceBinaryWithStream(final FedoraBinary result, 574 final InputStream requestBodyStream, 575 final ContentDisposition contentDisposition, 576 final MediaType contentType, 577 final String checksum) throws InvalidChecksumException { 578 final URI checksumURI = checksumURI(checksum); 579 final String originalFileName = contentDisposition != null ? contentDisposition.getFileName() : ""; 580 final String originalContentType = contentType != null ? contentType.toString() : ""; 581 582 result.setContent(requestBodyStream, 583 originalContentType, 584 checksumURI, 585 originalFileName, 586 storagePolicyDecisionPoint); 587 } 588 589 protected void replaceResourceWithStream(final FedoraResource resource, 590 final InputStream requestBodyStream, 591 final MediaType contentType, 592 final RdfStream resourceTriples) throws MalformedRdfException { 593 final Lang format = contentTypeToLang(contentType.toString()); 594 595 final Model inputModel = createDefaultModel() 596 .read(requestBodyStream, getUri(resource).toString(), format.getName().toUpperCase()); 597 598 resource.replaceProperties(translator(), inputModel, resourceTriples); 599 } 600 601 protected void patchResourcewithSparql(final FedoraResource resource, 602 final String requestBody, 603 final RdfStream resourceTriples) 604 throws MalformedRdfException, AccessDeniedException { 605 resource.updateProperties(translator(), requestBody, resourceTriples); 606 } 607 608 /** 609 * Create a checksum URI object. 610 **/ 611 private static URI checksumURI( final String checksum ) { 612 if (!isBlank(checksum)) { 613 return URI.create(checksum); 614 } 615 return null; 616 } 617}