001/** 002 * Copyright 2014 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.Status.PARTIAL_CONTENT; 031import static javax.ws.rs.core.Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE; 032import static javax.ws.rs.core.Response.temporaryRedirect; 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; 044import static org.slf4j.LoggerFactory.getLogger; 045 046import java.io.IOException; 047import java.io.InputStream; 048import java.net.URI; 049import java.net.URISyntaxException; 050import java.util.Date; 051import java.util.Iterator; 052 053import javax.inject.Inject; 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.apache.jena.riot.Lang; 069import org.fcrepo.http.commons.api.rdf.HttpTripleUtil; 070import org.fcrepo.http.commons.domain.MultiPrefer; 071import org.fcrepo.http.commons.domain.PreferTag; 072import org.fcrepo.http.commons.domain.Range; 073import org.fcrepo.http.commons.domain.ldp.LdpPreferTag; 074import org.fcrepo.http.commons.responses.RangeRequestInputStream; 075import org.fcrepo.kernel.exception.InvalidChecksumException; 076import org.fcrepo.kernel.exception.MalformedRdfException; 077import org.fcrepo.kernel.exception.RepositoryRuntimeException; 078import org.fcrepo.kernel.impl.rdf.ManagedRdf; 079import org.fcrepo.kernel.impl.rdf.impl.AclRdfContext; 080import org.fcrepo.kernel.impl.rdf.impl.BlankNodeRdfContext; 081import org.fcrepo.kernel.impl.rdf.impl.ChildrenRdfContext; 082import org.fcrepo.kernel.impl.rdf.impl.ContentRdfContext; 083import org.fcrepo.kernel.impl.rdf.impl.HashRdfContext; 084import org.fcrepo.kernel.impl.rdf.impl.LdpContainerRdfContext; 085import org.fcrepo.kernel.impl.rdf.impl.LdpIsMemberOfRdfContext; 086import org.fcrepo.kernel.impl.rdf.impl.LdpRdfContext; 087import org.fcrepo.kernel.impl.rdf.impl.ParentRdfContext; 088import org.fcrepo.kernel.impl.rdf.impl.PropertiesRdfContext; 089import org.fcrepo.kernel.impl.rdf.impl.ReferencesRdfContext; 090import org.fcrepo.kernel.impl.rdf.impl.RootRdfContext; 091import org.fcrepo.kernel.impl.rdf.impl.TypeRdfContext; 092import org.fcrepo.kernel.impl.services.TransactionServiceImpl; 093import org.fcrepo.kernel.models.Container; 094import org.fcrepo.kernel.models.FedoraBinary; 095import org.fcrepo.kernel.models.FedoraResource; 096import org.fcrepo.kernel.models.NonRdfSource; 097import org.fcrepo.kernel.models.NonRdfSourceDescription; 098import org.fcrepo.kernel.services.policy.StoragePolicyDecisionPoint; 099import org.fcrepo.kernel.utils.iterators.RdfStream; 100import org.glassfish.jersey.media.multipart.ContentDisposition; 101import org.jvnet.hk2.annotations.Optional; 102import org.slf4j.Logger; 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 private static final Logger LOGGER = getLogger(ContentExposingResource.class); 123 public static final MediaType MESSAGE_EXTERNAL_BODY = MediaType.valueOf("message/external-body"); 124 125 @Context protected Request request; 126 @Context protected HttpServletResponse servletResponse; 127 128 @Inject 129 @Optional 130 private HttpTripleUtil httpTripleUtil; 131 132 @BeanParam 133 protected MultiPrefer prefer; 134 135 @Inject 136 @Optional 137 StoragePolicyDecisionPoint storagePolicyDecisionPoint; 138 139 protected FedoraResource resource; 140 141 private static long MAX_BUFFER_SIZE = 10240000; 142 143 protected abstract String externalPath(); 144 145 protected Response getContent(final String rangeValue, 146 final RdfStream rdfStream) throws IOException { 147 if (resource() instanceof FedoraBinary) { 148 149 final String contentTypeString = ((FedoraBinary) resource()).getMimeType(); 150 151 final Lang lang = contentTypeToLang(contentTypeString); 152 153 if (!contentTypeString.equals("text/plain") && lang != null) { 154 155 final String format = lang.getName().toUpperCase(); 156 157 final InputStream content = ((FedoraBinary) resource()).getContent(); 158 159 final Model inputModel = createDefaultModel() 160 .read(content, (resource()).toString(), format); 161 162 rdfStream.concat(Iterators.transform(inputModel.listStatements(), 163 new Function<Statement, Triple>() { 164 165 @Override 166 public Triple apply(final Statement input) { 167 return input.asTriple(); 168 } 169 })); 170 } else { 171 172 final MediaType mediaType = MediaType.valueOf(contentTypeString); 173 if (MESSAGE_EXTERNAL_BODY.isCompatible(mediaType) 174 && mediaType.getParameters().containsKey("access-type") 175 && mediaType.getParameters().get("access-type").equals("URL") 176 && mediaType.getParameters().containsKey("URL") ) { 177 try { 178 return temporaryRedirect(new URI(mediaType.getParameters().get("URL"))).build(); 179 } catch (final URISyntaxException e) { 180 throw new RepositoryRuntimeException(e); 181 } 182 } else { 183 return getBinaryContent(rangeValue); 184 } 185 } 186 187 } else { 188 rdfStream.concat(getResourceTriples()); 189 190 if (prefer != null) { 191 prefer.getReturn().addResponseHeaders(servletResponse); 192 } 193 194 } 195 servletResponse.addHeader("Vary", "Accept, Range, Accept-Encoding, Accept-Language"); 196 197 return Response.ok(rdfStream).build(); 198 } 199 200 protected RdfStream getResourceTriples() { 201 202 final PreferTag returnPreference; 203 204 if (prefer != null && prefer.hasReturn()) { 205 returnPreference = prefer.getReturn(); 206 } else if (prefer != null && prefer.hasHandling()) { 207 returnPreference = prefer.getHandling(); 208 } else { 209 returnPreference = PreferTag.emptyTag(); 210 } 211 212 final LdpPreferTag ldpPreferences = new LdpPreferTag(returnPreference); 213 214 final RdfStream rdfStream = new RdfStream(); 215 216 final Predicate<Triple> tripleFilter; 217 if (ldpPreferences.prefersServerManaged()) { 218 tripleFilter = alwaysTrue(); 219 } else { 220 tripleFilter = and(not(ManagedRdf.isManagedTriple), not(new Predicate<Triple>() { 221 @Override 222 public boolean apply(final Triple input) { 223 return input.getPredicate().equals(RDF.type.asNode()) 224 && isManagedNamespace.apply(input.getObject().getNameSpace()); 225 } 226 })); 227 } 228 229 if (ldpPreferences.prefersServerManaged()) { 230 rdfStream.concat(getTriples(LdpRdfContext.class)); 231 } 232 233 rdfStream.concat(filter(getTriples(TypeRdfContext.class), tripleFilter)); 234 235 rdfStream.concat(filter(getTriples(PropertiesRdfContext.class), tripleFilter)); 236 237 if (!returnPreference.getValue().equals("minimal")) { 238 239 // Additional server-managed triples about this resource 240 if (ldpPreferences.prefersServerManaged()) { 241 rdfStream.concat(getTriples(AclRdfContext.class)); 242 rdfStream.concat(getTriples(RootRdfContext.class)); 243 rdfStream.concat(getTriples(ContentRdfContext.class)); 244 rdfStream.concat(getTriples(ParentRdfContext.class)); 245 } 246 247 // containment triples about this resource 248 if (ldpPreferences.prefersContainment()) { 249 rdfStream.concat(getTriples(ChildrenRdfContext.class)); 250 } 251 252 // LDP container membership triples for this resource 253 if (ldpPreferences.prefersMembership()) { 254 rdfStream.concat(getTriples(LdpContainerRdfContext.class)); 255 rdfStream.concat(getTriples(LdpIsMemberOfRdfContext.class)); 256 } 257 258 // Include binary properties if this is a binary description 259 if (resource() instanceof NonRdfSourceDescription) { 260 final FedoraResource described = ((NonRdfSourceDescription) resource()).getDescribedResource(); 261 rdfStream.concat(filter(described.getTriples(translator(), ImmutableList.of(TypeRdfContext.class, 262 PropertiesRdfContext.class, 263 ContentRdfContext.class)), tripleFilter)); 264 } 265 266 // Embed all hash and blank nodes 267 rdfStream.concat(filter(getTriples(HashRdfContext.class), tripleFilter)); 268 rdfStream.concat(filter(getTriples(BlankNodeRdfContext.class), tripleFilter)); 269 270 // Include inbound references to this object 271 if (ldpPreferences.prefersReferences()) { 272 rdfStream.concat(getTriples(ReferencesRdfContext.class)); 273 } 274 275 // Embed the children of this object 276 if (ldpPreferences.prefersEmbed()) { 277 278 final Iterator<FedoraResource> children = resource().getChildren(); 279 280 rdfStream.concat(filter(concat(transform(children, 281 new Function<FedoraResource, RdfStream>() { 282 283 @Override 284 public RdfStream apply(final FedoraResource child) { 285 return child.getTriples(translator(), ImmutableList.of( 286 TypeRdfContext.class, 287 PropertiesRdfContext.class, 288 BlankNodeRdfContext.class)); 289 } 290 })), tripleFilter)); 291 292 } 293 } 294 295 if (httpTripleUtil != null && ldpPreferences.prefersServerManaged()) { 296 httpTripleUtil.addHttpComponentModelsForResourceToStream(rdfStream, resource(), uriInfo, translator()); 297 } 298 299 300 return rdfStream; 301 } 302 303 /** 304 * Get the binary content of a datastream 305 * 306 * @return Binary blob 307 * @throws RepositoryException 308 */ 309 protected Response getBinaryContent(final String rangeValue) 310 throws IOException { 311 final FedoraBinary binary = (FedoraBinary)resource(); 312 313 // we include an explicit etag, because the default behavior is to use the JCR node's etag, not 314 // the jcr:content node digest. The etag is only included if we are not within a transaction. 315 final String txId = TransactionServiceImpl.getCurrentTransactionId(session()); 316 if (txId == null) { 317 checkCacheControlHeaders(request, servletResponse, binary, session()); 318 } 319 final CacheControl cc = new CacheControl(); 320 cc.setMaxAge(0); 321 cc.setMustRevalidate(true); 322 Response.ResponseBuilder builder; 323 324 if (rangeValue != null && rangeValue.startsWith("bytes")) { 325 326 final Range range = Range.convert(rangeValue); 327 328 final long contentSize = binary.getContentSize(); 329 330 final String endAsString; 331 332 if (range.end() == -1) { 333 endAsString = Long.toString(contentSize - 1); 334 } else { 335 endAsString = Long.toString(range.end()); 336 } 337 338 final String contentRangeValue = 339 String.format("bytes %s-%s/%s", range.start(), 340 endAsString, contentSize); 341 342 if (range.end() > contentSize || 343 (range.end() == -1 && range.start() > contentSize)) { 344 345 builder = status(REQUESTED_RANGE_NOT_SATISFIABLE) 346 .header("Content-Range", contentRangeValue); 347 } else { 348 final long maxBufferSize = MAX_BUFFER_SIZE; // 10MB max buffer size? 349 final long rangeStart = range.start(); 350 final long rangeSize = range.size() == -1 ? contentSize - rangeStart : range.size(); 351 final long remainingBytes = contentSize - rangeStart; 352 final long bufSize = rangeSize < remainingBytes ? rangeSize : remainingBytes; 353 354 if (bufSize < maxBufferSize) { 355 // Small size range content retrieval use javax.jcr.Binary to improve performance 356 final byte[] buf = new byte[(int) bufSize]; 357 358 final Binary binaryContent = binary.getBinaryContent(); 359 try { 360 binaryContent.read(buf, rangeStart); 361 } catch (RepositoryException e1) { 362 throw new RepositoryRuntimeException(e1); 363 } 364 binaryContent.dispose(); 365 366 builder = status(PARTIAL_CONTENT).entity(buf) 367 .header("Content-Range", contentRangeValue); 368 } else { 369 // For large range content retrieval, go with the InputStream class to balance 370 // the memory usage, though this is a rare case in range content retrieval. 371 final InputStream content = binary.getContent(); 372 final RangeRequestInputStream rangeInputStream = 373 new RangeRequestInputStream(content, range.start(), range.size()); 374 375 builder = status(PARTIAL_CONTENT).entity(rangeInputStream) 376 .header("Content-Range", contentRangeValue); 377 } 378 } 379 380 } else { 381 final InputStream content = binary.getContent(); 382 builder = ok(content); 383 } 384 385 386 // we set the content-type explicitly to avoid content-negotiation from getting in the way 387 return builder.type(binary.getMimeType()) 388 .cacheControl(cc) 389 .build(); 390 391 } 392 393 protected RdfStream getTriples(final Class<? extends RdfStream> x) { 394 return getTriples(resource(), x); 395 } 396 397 protected RdfStream getTriples(final FedoraResource resource, final Class<? extends RdfStream> x) { 398 return resource.getTriples(translator(), x); 399 } 400 401 protected URI getUri(final FedoraResource resource) { 402 try { 403 final String uri = translator().reverse().convert(resource).getURI(); 404 return new URI(uri); 405 } catch (final URISyntaxException e) { 406 throw new BadRequestException(e); 407 } 408 } 409 410 protected FedoraResource resource() { 411 if (resource == null) { 412 resource = getResourceFromPath(externalPath()); 413 } 414 415 return resource; 416 } 417 418 419 /** 420 * Add any resource-specific headers to the response 421 * @param resource 422 */ 423 protected void addResourceHttpHeaders(final FedoraResource resource) { 424 if (resource instanceof FedoraBinary) { 425 426 final FedoraBinary binary = (FedoraBinary)resource; 427 final ContentDisposition contentDisposition = ContentDisposition.type("attachment") 428 .fileName(binary.getFilename()) 429 .creationDate(binary.getCreatedDate()) 430 .modificationDate(binary.getLastModifiedDate()) 431 .size(binary.getContentSize()) 432 .build(); 433 434 servletResponse.addHeader("Content-Type", binary.getMimeType()); 435 servletResponse.addHeader("Content-Length", String.valueOf(binary.getContentSize())); 436 servletResponse.addHeader("Accept-Ranges", "bytes"); 437 servletResponse.addHeader("Content-Disposition", contentDisposition.toString()); 438 } 439 440 servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "Resource>;rel=\"type\""); 441 442 if (resource instanceof NonRdfSource) { 443 servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "NonRDFSource>;rel=\"type\""); 444 } else if (resource instanceof Container) { 445 servletResponse.addHeader("Link", "<" + CONTAINER.getURI() + ">;rel=\"type\""); 446 if (resource.hasType(LDP_BASIC_CONTAINER)) { 447 servletResponse.addHeader("Link", "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 448 } else if (resource.hasType(LDP_DIRECT_CONTAINER)) { 449 servletResponse.addHeader("Link", "<" + DIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 450 } else if (resource.hasType(LDP_INDIRECT_CONTAINER)) { 451 servletResponse.addHeader("Link", "<" + INDIRECT_CONTAINER.getURI() + ">;rel=\"type\""); 452 } else { 453 servletResponse.addHeader("Link", "<" + BASIC_CONTAINER.getURI() + ">;rel=\"type\""); 454 } 455 } else { 456 servletResponse.addHeader("Link", "<" + LDP_NAMESPACE + "RDFSource>;rel=\"type\""); 457 } 458 459 } 460 461 /** 462 * Evaluate the cache control headers for the request to see if it can be served from 463 * the cache. 464 * 465 * @param request 466 * @param servletResponse 467 * @param resource 468 * @param session 469 * @throws javax.jcr.RepositoryException 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 482 * @param resource 483 */ 484 protected static void addCacheControlHeaders(final HttpServletResponse servletResponse, 485 final FedoraResource resource, 486 final Session session) { 487 488 final String txId = TransactionServiceImpl.getCurrentTransactionId(session); 489 if (txId != null) { 490 // Do not add caching headers if in a transaction 491 return; 492 } 493 494 final EntityTag etag = new EntityTag(resource.getEtagValue()); 495 final Date date = resource.getLastModifiedDate(); 496 497 if (!etag.getValue().isEmpty()) { 498 servletResponse.addHeader("ETag", etag.toString()); 499 } 500 501 if (date != null) { 502 servletResponse.addDateHeader("Last-Modified", date.getTime()); 503 } 504 } 505 506 /** 507 * Evaluate request preconditions to ensure the resource is the expected state 508 * @param request 509 * @param resource 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) throws MalformedRdfException { 604 resource.updateProperties(translator(), requestBody, resourceTriples); 605 } 606 607 /** 608 * Create a checksum URI object. 609 **/ 610 private static URI checksumURI( final String checksum ) { 611 if (!isBlank(checksum)) { 612 return URI.create(checksum); 613 } 614 return null; 615 } 616}