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 if (ldpPreferences.prefersServerManaged()) { 263 rdfStream.concat(getTriples(described,LdpRdfContext.class)); 264 } 265 } 266 267 // Embed all hash and blank nodes 268 rdfStream.concat(filter(getTriples(HashRdfContext.class), tripleFilter)); 269 rdfStream.concat(filter(getTriples(BlankNodeRdfContext.class), tripleFilter)); 270 271 // Include inbound references to this object 272 if (ldpPreferences.prefersReferences()) { 273 rdfStream.concat(getTriples(ReferencesRdfContext.class)); 274 } 275 276 // Embed the children of this object 277 if (ldpPreferences.prefersEmbed()) { 278 279 final Iterator<FedoraResource> children = resource().getChildren(); 280 281 rdfStream.concat(filter(concat(transform(children, 282 new Function<FedoraResource, RdfStream>() { 283 284 @Override 285 public RdfStream apply(final FedoraResource child) { 286 return child.getTriples(translator(), ImmutableList.of( 287 TypeRdfContext.class, 288 PropertiesRdfContext.class, 289 BlankNodeRdfContext.class)); 290 } 291 })), tripleFilter)); 292 293 } 294 } 295 296 if (httpTripleUtil != null && ldpPreferences.prefersServerManaged()) { 297 httpTripleUtil.addHttpComponentModelsForResourceToStream(rdfStream, resource(), uriInfo, translator()); 298 } 299 300 301 return rdfStream; 302 } 303 304 /** 305 * Get the binary content of a datastream 306 * 307 * @param rangeValue the range value 308 * @return Binary blob 309 * @throws IOException if io exception occurred 310 */ 311 protected Response getBinaryContent(final String rangeValue) 312 throws IOException { 313 final FedoraBinary binary = (FedoraBinary)resource(); 314 315 // we include an explicit etag, because the default behavior is to use the JCR node's etag, not 316 // the jcr:content node digest. The etag is only included if we are not within a transaction. 317 final String txId = TransactionServiceImpl.getCurrentTransactionId(session()); 318 if (txId == null) { 319 checkCacheControlHeaders(request, servletResponse, binary, session()); 320 } 321 final CacheControl cc = new CacheControl(); 322 cc.setMaxAge(0); 323 cc.setMustRevalidate(true); 324 Response.ResponseBuilder builder; 325 326 if (rangeValue != null && rangeValue.startsWith("bytes")) { 327 328 final Range range = Range.convert(rangeValue); 329 330 final long contentSize = binary.getContentSize(); 331 332 final String endAsString; 333 334 if (range.end() == -1) { 335 endAsString = Long.toString(contentSize - 1); 336 } else { 337 endAsString = Long.toString(range.end()); 338 } 339 340 final String contentRangeValue = 341 String.format("bytes %s-%s/%s", range.start(), 342 endAsString, contentSize); 343 344 if (range.end() > contentSize || 345 (range.end() == -1 && range.start() > contentSize)) { 346 347 builder = status(REQUESTED_RANGE_NOT_SATISFIABLE) 348 .header("Content-Range", contentRangeValue); 349 } else { 350 final long rangeStart = range.start(); 351 final long rangeSize = range.size() == -1 ? contentSize - rangeStart : range.size(); 352 final long remainingBytes = contentSize - rangeStart; 353 final long bufSize = rangeSize < remainingBytes ? rangeSize : remainingBytes; 354 355 if (bufSize < MAX_BUFFER_SIZE) { 356 // Small size range content retrieval use javax.jcr.Binary to improve performance 357 final byte[] buf = new byte[(int) bufSize]; 358 359 final Binary binaryContent = binary.getBinaryContent(); 360 try { 361 binaryContent.read(buf, rangeStart); 362 } catch (final RepositoryException e1) { 363 throw new RepositoryRuntimeException(e1); 364 } 365 binaryContent.dispose(); 366 367 builder = status(PARTIAL_CONTENT).entity(buf) 368 .header("Content-Range", contentRangeValue); 369 } else { 370 // For large range content retrieval, go with the InputStream class to balance 371 // the memory usage, though this is a rare case in range content retrieval. 372 final InputStream content = binary.getContent(); 373 final RangeRequestInputStream rangeInputStream = 374 new RangeRequestInputStream(content, range.start(), range.size()); 375 376 builder = status(PARTIAL_CONTENT).entity(rangeInputStream) 377 .header("Content-Range", contentRangeValue); 378 } 379 } 380 381 } else { 382 final InputStream content = binary.getContent(); 383 builder = ok(content); 384 } 385 386 387 // we set the content-type explicitly to avoid content-negotiation from getting in the way 388 return builder.type(binary.getMimeType()) 389 .cacheControl(cc) 390 .build(); 391 392 } 393 394 protected RdfStream getTriples(final Class<? extends RdfStream> x) { 395 return getTriples(resource(), x); 396 } 397 398 protected RdfStream getTriples(final FedoraResource resource, final Class<? extends RdfStream> x) { 399 return resource.getTriples(translator(), x); 400 } 401 402 protected URI getUri(final FedoraResource resource) { 403 try { 404 final String uri = translator().reverse().convert(resource).getURI(); 405 return new URI(uri); 406 } catch (final URISyntaxException e) { 407 throw new BadRequestException(e); 408 } 409 } 410 411 protected FedoraResource resource() { 412 if (resource == null) { 413 resource = getResourceFromPath(externalPath()); 414 } 415 416 return resource; 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 427 final FedoraBinary binary = (FedoraBinary)resource; 428 final ContentDisposition contentDisposition = ContentDisposition.type("attachment") 429 .fileName(binary.getFilename()) 430 .creationDate(binary.getCreatedDate()) 431 .modificationDate(binary.getLastModifiedDate()) 432 .size(binary.getContentSize()) 433 .build(); 434 435 servletResponse.addHeader("Content-Type", binary.getMimeType()); 436 servletResponse.addHeader("Content-Length", String.valueOf(binary.getContentSize())); 437 servletResponse.addHeader("Accept-Ranges", "bytes"); 438 servletResponse.addHeader("Content-Disposition", contentDisposition.toString()); 439 } 440 441 servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "Resource>;rel=\"type\""); 442 443 if (resource instanceof NonRdfSource) { 444 servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "NonRDFSource>;rel=\"type\""); 445 } else if (resource instanceof Container) { 446 servletResponse.addHeader("Link", "<" + CONTAINER.getURI() + ">;rel=\"type\""); 447 if (resource.hasType(LDP_BASIC_CONTAINER)) { 448 servletResponse.addHeader("Link", "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 449 } else if (resource.hasType(LDP_DIRECT_CONTAINER)) { 450 servletResponse.addHeader("Link", "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 451 } else if (resource.hasType(LDP_INDIRECT_CONTAINER)) { 452 servletResponse.addHeader("Link", "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 453 } else { 454 servletResponse.addHeader("Link", "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 455 } 456 } else { 457 servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "RDFSource>;rel=\"type\""); 458 } 459 460 } 461 462 /** 463 * Evaluate the cache control headers for the request to see if it can be served from 464 * the cache. 465 * 466 * @param request the request 467 * @param servletResponse the servlet response 468 * @param resource the fedora resource 469 * @param session the session 470 */ 471 protected static void checkCacheControlHeaders(final Request request, 472 final HttpServletResponse servletResponse, 473 final FedoraResource resource, 474 final Session session) { 475 evaluateRequestPreconditions(request, servletResponse, resource, session, true); 476 addCacheControlHeaders(servletResponse, resource, session); 477 } 478 479 /** 480 * Add ETag and Last-Modified cache control headers to the response 481 * @param servletResponse the servlet response 482 * @param resource the fedora resource 483 * @param session the session 484 */ 485 protected static void addCacheControlHeaders(final HttpServletResponse servletResponse, 486 final FedoraResource resource, 487 final Session session) { 488 489 final String txId = TransactionServiceImpl.getCurrentTransactionId(session); 490 if (txId != null) { 491 // Do not add caching headers if in a transaction 492 return; 493 } 494 495 final EntityTag etag = new EntityTag(resource.getEtagValue()); 496 final Date date = resource.getLastModifiedDate(); 497 498 if (!etag.getValue().isEmpty()) { 499 servletResponse.addHeader("ETag", etag.toString()); 500 } 501 502 if (date != null) { 503 servletResponse.addDateHeader("Last-Modified", date.getTime()); 504 } 505 } 506 507 /** 508 * Evaluate request preconditions to ensure the resource is the expected state 509 * @param request the request 510 * @param servletResponse the servlet response 511 * @param resource the resource 512 * @param session the session 513 */ 514 protected static void evaluateRequestPreconditions(final Request request, 515 final HttpServletResponse servletResponse, 516 final FedoraResource resource, 517 final Session session) { 518 evaluateRequestPreconditions(request, servletResponse, resource, session, false); 519 } 520 521 private static void evaluateRequestPreconditions(final Request request, 522 final HttpServletResponse servletResponse, 523 final FedoraResource resource, 524 final Session session, 525 final boolean cacheControl) { 526 527 final String txId = TransactionServiceImpl.getCurrentTransactionId(session); 528 if (txId != null) { 529 // Force cache revalidation if in a transaction 530 servletResponse.addHeader(CACHE_CONTROL, "must-revalidate"); 531 servletResponse.addHeader(CACHE_CONTROL, "max-age=0"); 532 return; 533 } 534 535 final EntityTag etag = new EntityTag(resource.getEtagValue()); 536 final Date date = resource.getLastModifiedDate(); 537 final Date roundedDate = new Date(); 538 539 if (date != null) { 540 roundedDate.setTime(date.getTime() - date.getTime() % 1000); 541 } 542 543 Response.ResponseBuilder builder = request.evaluatePreconditions(etag); 544 if ( builder != null ) { 545 builder = builder.entity("ETag mismatch"); 546 } else { 547 builder = request.evaluatePreconditions(roundedDate); 548 if ( builder != null ) { 549 builder = builder.entity("Date mismatch"); 550 } 551 } 552 553 if (builder != null && cacheControl ) { 554 final CacheControl cc = new CacheControl(); 555 cc.setMaxAge(0); 556 cc.setMustRevalidate(true); 557 // here we are implicitly emitting a 304 558 // the exception is not an error, it's genuinely 559 // an exceptional condition 560 builder = builder.cacheControl(cc).lastModified(date).tag(etag); 561 } 562 if (builder != null) { 563 throw new WebApplicationException(builder.build()); 564 } 565 } 566 567 protected static MediaType getSimpleContentType(final MediaType requestContentType) { 568 return requestContentType != null ? new MediaType(requestContentType.getType(), requestContentType.getSubtype()) 569 : APPLICATION_OCTET_STREAM_TYPE; 570 } 571 572 protected static boolean isRdfContentType(final String contentTypeString) { 573 return contentTypeToLang(contentTypeString) != null; 574 } 575 576 protected void replaceResourceBinaryWithStream(final FedoraBinary result, 577 final InputStream requestBodyStream, 578 final ContentDisposition contentDisposition, 579 final MediaType contentType, 580 final String checksum) throws InvalidChecksumException { 581 final URI checksumURI = checksumURI(checksum); 582 final String originalFileName = contentDisposition != null ? contentDisposition.getFileName() : ""; 583 final String originalContentType = contentType != null ? contentType.toString() : ""; 584 585 result.setContent(requestBodyStream, 586 originalContentType, 587 checksumURI, 588 originalFileName, 589 storagePolicyDecisionPoint); 590 } 591 592 protected void replaceResourceWithStream(final FedoraResource resource, 593 final InputStream requestBodyStream, 594 final MediaType contentType, 595 final RdfStream resourceTriples) throws MalformedRdfException { 596 final Lang format = contentTypeToLang(contentType.toString()); 597 598 final Model inputModel = createDefaultModel() 599 .read(requestBodyStream, getUri(resource).toString(), format.getName().toUpperCase()); 600 601 resource.replaceProperties(translator(), inputModel, resourceTriples); 602 } 603 604 protected void patchResourcewithSparql(final FedoraResource resource, 605 final String requestBody, 606 final RdfStream resourceTriples) 607 throws MalformedRdfException, AccessDeniedException { 608 resource.updateProperties(translator(), requestBody, resourceTriples); 609 } 610 611 /** 612 * Create a checksum URI object. 613 **/ 614 private static URI checksumURI( final String checksum ) { 615 if (!isBlank(checksum)) { 616 return URI.create(checksum); 617 } 618 return null; 619 } 620}