Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #304 from futures/cache-control
Node and content resource requests should have ETag/Last-Modified header...

Resolves: https://www.pivotaltracker.com/story/show/69784478
  • Loading branch information
Andrew Woods committed Apr 19, 2014
2 parents 10e11e3 + fdb0ff7 commit ca5ddbf
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 198 deletions.
Expand Up @@ -24,6 +24,8 @@
import org.fcrepo.kernel.Datastream;

import javax.jcr.RepositoryException;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.Request;
Expand Down Expand Up @@ -53,64 +55,53 @@ public abstract class ContentExposingResource extends AbstractResource {
* for content (or a range of the content) into a Response
*/
protected Response getDatastreamContentResponse(final Datastream ds, final String rangeValue, final Request request,
final HttpServletResponse servletResponse,
final HttpIdentifierTranslator subjects) throws
RepositoryException, IOException {
final EntityTag etag =
new EntityTag(ds.getContentDigest().toString());
final Date date = ds.getLastModifiedDate();
final Date roundedDate = new Date();
roundedDate.setTime(date.getTime() - date.getTime() % 1000);
Response.ResponseBuilder builder =
request.evaluatePreconditions(roundedDate, etag);

// we include an explicit etag, because the default behavior is to use the JCR node's etag, not
// the jcr:content node digest.
checkCacheControlHeaders(request, servletResponse, ds);

final CacheControl cc = new CacheControl();
cc.setMaxAge(0);
cc.setMustRevalidate(true);
Response.ResponseBuilder builder;

if (builder == null) {

final InputStream content = ds.getContent();

if (rangeValue != null && rangeValue.startsWith("bytes")) {
final InputStream content = ds.getContent();

final Range range = Range.convert(rangeValue);
if (rangeValue != null && rangeValue.startsWith("bytes")) {

final long contentSize = ds.getContentSize();
final Range range = Range.convert(rangeValue);

final String endAsString;
final long contentSize = ds.getContentSize();

if (range.end() == -1) {
endAsString = Long.toString(contentSize - 1);
} else {
endAsString = Long.toString(range.end());
}
final String endAsString;

final String contentRangeValue =
String.format("bytes %s-%s/%s", range.start(),
endAsString, contentSize);

if (range.end() > contentSize ||
(range.end() == -1 && range.start() > contentSize)) {
builder =
status(
REQUESTED_RANGE_NOT_SATISFIABLE)
.header("Content-Range",
contentRangeValue);
} else {
final RangeRequestInputStream rangeInputStream =
new RangeRequestInputStream(content, range
.start(), range.size());
if (range.end() == -1) {
endAsString = Long.toString(contentSize - 1);
} else {
endAsString = Long.toString(range.end());
}

builder =
status(PARTIAL_CONTENT).entity(
rangeInputStream)
.header("Content-Range",
contentRangeValue);
}
final String contentRangeValue =
String.format("bytes %s-%s/%s", range.start(),
endAsString, contentSize);

if (range.end() > contentSize ||
(range.end() == -1 && range.start() > contentSize)) {
builder = status(REQUESTED_RANGE_NOT_SATISFIABLE)
.header("Content-Range", contentRangeValue);
} else {
builder = ok(content);
final RangeRequestInputStream rangeInputStream =
new RangeRequestInputStream(content, range.start(), range.size());

builder = status(PARTIAL_CONTENT).entity(rangeInputStream)
.header("Content-Range", contentRangeValue);
}

} else {
builder = ok(content);
}

final ContentDisposition contentDisposition = ContentDisposition.type("attachment")
Expand All @@ -124,9 +115,67 @@ protected Response getDatastreamContentResponse(final Datastream ds, final Strin
"Link",
subjects.getSubject(ds.getNode().getPath()) +
";rel=\"describedby\"").header("Accept-Ranges",
"bytes").cacheControl(cc).lastModified(date).tag(etag)
"bytes").cacheControl(cc)
.header("Content-Disposition", contentDisposition)
.build();
}

/**
* Evaluate the cache control headers for the request to see if it can be served from
* the cache.
*
* @param request
* @param servletResponse
* @param resource
* @throws javax.jcr.RepositoryException
*/
protected static void checkCacheControlHeaders(final Request request,
final HttpServletResponse servletResponse,
final Datastream resource) throws RepositoryException {

final EntityTag etag = new EntityTag(resource.getContentDigest().toString());
final Date date = resource.getLastModifiedDate();

final Date roundedDate = new Date();
if (date != null) {
roundedDate.setTime(date.getTime() - date.getTime() % 1000);
}
final Response.ResponseBuilder builder =
request.evaluatePreconditions(roundedDate, etag);

if (builder != null) {
final CacheControl cc = new CacheControl();
cc.setMaxAge(0);
cc.setMustRevalidate(true);
// here we are implicitly emitting a 304
// the exception is not an error, it's genuinely
// an exceptional condition
throw new WebApplicationException(builder.cacheControl(cc)
.lastModified(date).tag(etag).build());
}

addCacheControlHeaders(servletResponse, resource);
}

/**
* Add ETag and Last-Modified cache control headers to the response
* @param servletResponse
* @param resource
* @throws RepositoryException
*/
protected static void addCacheControlHeaders(final HttpServletResponse servletResponse,
final Datastream resource) throws RepositoryException {

final EntityTag etag = new EntityTag(resource.getContentDigest().toString());
final Date date = resource.getLastModifiedDate();

if (!etag.getValue().isEmpty()) {
servletResponse.addHeader("ETag", etag.toString());
}

if (date != null) {
servletResponse.addDateHeader("Last-Modified", date.getTime());
}
}

}
Expand Up @@ -29,16 +29,15 @@
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Request;
Expand All @@ -49,7 +48,6 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.Date;
import java.util.List;

import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
Expand All @@ -59,7 +57,6 @@
import static org.apache.http.HttpStatus.SC_CONFLICT;
import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
import static org.slf4j.LoggerFactory.getLogger;
import static org.fcrepo.jcr.FedoraJcrTypes.JCR_LASTMODIFIED;

/**
* Content controller for adding, reading, and manipulating
Expand Down Expand Up @@ -90,7 +87,7 @@ public Response create(@PathParam("path")
@HeaderParam("Content-Disposition") final String contentDisposition,
@QueryParam("checksum") final String checksum,
@HeaderParam("Content-Type") final MediaType requestContentType,
final InputStream requestBodyStream)
final InputStream requestBodyStream, @Context final HttpServletResponse servletResponse)
throws InvalidChecksumException, RepositoryException, URISyntaxException, ParseException {
final MediaType contentType =
requestContentType != null ? requestContentType
Expand Down Expand Up @@ -161,9 +158,9 @@ public Response create(@PathParam("path")

final ResponseBuilder builder = created(new URI(subjects.getSubject(
datastreamNode.getNode(JCR_CONTENT).getPath()).getURI()));
if ( datastreamNode.hasProperty(JCR_LASTMODIFIED) ) {
builder.lastModified(datastreamNode.getProperty(JCR_LASTMODIFIED).getDate().getTime());
}
final Datastream datastream = datastreamService.asDatastream(datastreamNode);

addCacheControlHeaders(servletResponse, datastream);

return builder.build();

Expand All @@ -189,7 +186,7 @@ public Response modifyContent(@PathParam("path") final List<PathSegment> pathLis
@HeaderParam("Content-Disposition") final String contentDisposition,
@HeaderParam("Content-Type") final MediaType requestContentType,
final InputStream requestBodyStream,
@Context final Request request)
@Context final Request request, @Context final HttpServletResponse servletResponse)
throws RepositoryException, InvalidChecksumException, URISyntaxException, ParseException {

try {
Expand All @@ -203,18 +200,7 @@ public Response modifyContent(@PathParam("path") final List<PathSegment> pathLis
final Datastream ds =
datastreamService.getDatastream(session, path);

final EntityTag etag =
new EntityTag(ds.getContentDigest().toString());
final Date date = ds.getLastModifiedDate();
final Date roundedDate = new Date();
roundedDate
.setTime(date.getTime() - date.getTime() % 1000);
final ResponseBuilder builder =
request.evaluatePreconditions(roundedDate, etag);

if (builder != null) {
throw new WebApplicationException(builder.build());
}
evaluateRequestPreconditions(request, ds);
}

LOGGER.debug("create Datastream {}", path);
Expand Down Expand Up @@ -244,7 +230,7 @@ public Response modifyContent(@PathParam("path") final List<PathSegment> pathLis
session.save();
versionService.nodeUpdated(datastreamNode);

ResponseBuilder builder = null;
ResponseBuilder builder;
if (isNew) {
final HttpIdentifierTranslator subjects =
new HttpIdentifierTranslator(session, FedoraNodes.class,
Expand All @@ -255,9 +241,11 @@ public Response modifyContent(@PathParam("path") final List<PathSegment> pathLis
} else {
builder = noContent();
}
if (datastreamNode.hasProperty(JCR_LASTMODIFIED)) {
builder.lastModified(datastreamNode.getProperty(JCR_LASTMODIFIED).getDate().getTime());
}

final Datastream datastream = datastreamService.asDatastream(datastreamNode);

addCacheControlHeaders(servletResponse, datastream);

return builder.build();
} finally {
session.logout();
Expand All @@ -273,10 +261,11 @@ public Response modifyContent(@PathParam("path") final List<PathSegment> pathLis
*/
@GET
@Timed
public Response getContent(@PathParam("path")
final List<PathSegment> pathList, @HeaderParam("Range")
final String rangeValue, @Context
final Request request) throws RepositoryException, IOException {
public Response getContent(@PathParam("path") final List<PathSegment> pathList,
@HeaderParam("Range") final String rangeValue,
@Context final Request request,
@Context final HttpServletResponse servletResponse)
throws RepositoryException, IOException {
try {
final String path = toPath(pathList);
LOGGER.info("Attempting get of {}.", path);
Expand All @@ -286,8 +275,8 @@ public Response getContent(@PathParam("path")
final HttpIdentifierTranslator subjects =
new HttpIdentifierTranslator(session, FedoraNodes.class,
uriInfo);
return getDatastreamContentResponse(ds, rangeValue, request,
subjects);
return getDatastreamContentResponse(ds, rangeValue, request, servletResponse,
subjects);

} finally {
session.logout();
Expand Down

0 comments on commit ca5ddbf

Please sign in to comment.