Skip to content

Commit

Permalink
Add caching headers over FedoraDatastreams and FedoraNodes API methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Woods committed May 9, 2013
1 parent d2acfc8 commit 6709d5d
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 69 deletions.
Expand Up @@ -14,6 +14,11 @@

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

Expand All @@ -28,8 +33,12 @@
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.CacheControl;
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;
import javax.ws.rs.core.Response;

import com.codahale.metrics.annotation.Timed;
Expand All @@ -42,6 +51,7 @@
import org.fcrepo.jaxb.responses.management.DatastreamProfile;
import org.fcrepo.services.DatastreamService;
import org.fcrepo.services.LowLevelStorageService;
import org.fcrepo.utils.ContentDigest;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -162,9 +172,10 @@ public Response deleteDatastreams(
@Path("/__content__")
@Produces("multipart/mixed")
@Timed
public Response getDatastreamsContents(@PathParam("path")
List<PathSegment> pathList, @QueryParam("dsid")
final List<String> dsids) throws RepositoryException, IOException {
public Response getDatastreamsContents(@PathParam("path") List<PathSegment> pathList,
@QueryParam("dsid") final List<String> dsids,
@Context final Request request)
throws RepositoryException, IOException {

final Session session = getAuthenticatedSession();

Expand All @@ -177,22 +188,67 @@ public Response getDatastreamsContents(@PathParam("path")
}
}

final MultiPart multipart = new MultiPart();
Date date = new Date();
final MessageDigest digest = ContentDigest.getSha1Digest();

// Ensure consistent order of dsids for additive digest creation.
Collections.sort(dsids);

final Iterator<String> i = dsids.iterator();
final Iterator<String> i = dsids.iterator();
final List<Datastream> datastreams = new ArrayList<>();
while (i.hasNext()) {
final String dsid = i.next();

try {
final Datastream ds =
datastreamService.getDatastream(session, path + "/" + dsid);
multipart.bodyPart(ds.getContent(), MediaType.valueOf(ds
.getMimeType()));
} catch (final PathNotFoundException e) {

datastreams.add(ds);

// Maintain a rolling digest
final URI contentDigest = ds.getContentDigest();
if (null != contentDigest) {
digest.update(contentDigest.toString().getBytes());
}

// Select the most recent date
if (ds.getLastModifiedDate().after(date)) {
date = ds.getLastModifiedDate();
}

} catch (final PathNotFoundException e) {
logger.warn("Path not found: {}", e.getMessage());
}
}
return Response.ok(multipart, MULTIPART_FORM_DATA).build();

final URI digestURI = ContentDigest.asURI(digest.getAlgorithm(),
digest.digest());
final EntityTag etag = new EntityTag(digestURI.toString());

final Date roundedDate = new Date();
roundedDate.setTime(date.getTime() - date.getTime() % 1000);

Response.ResponseBuilder builder =
request.evaluatePreconditions(roundedDate, etag);

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

// Preconditions were not met if builder is null
if (builder == null) {

final MultiPart multipart = new MultiPart();
for (Datastream ds : datastreams) {
multipart.bodyPart(ds.getContent(),
MediaType.valueOf(ds.getMimeType()));
}

builder = Response.ok(multipart, MULTIPART_FORM_DATA);
}

return builder.cacheControl(cc).lastModified(date).tag(etag).build();

} finally {
session.logout();
}
Expand Down
131 changes: 82 additions & 49 deletions fcrepo-http-api/src/main/java/org/fcrepo/api/FedoraNodes.java
Expand Up @@ -7,11 +7,11 @@
import static javax.ws.rs.core.MediaType.TEXT_XML;
import static javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.noContent;
import static javax.ws.rs.core.Response.ok;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

import javax.jcr.Node;
Expand All @@ -27,8 +27,11 @@
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;

import com.codahale.metrics.annotation.Timed;
Expand Down Expand Up @@ -59,70 +62,99 @@ public class FedoraNodes extends AbstractResource {

@GET
@Produces({TEXT_XML, APPLICATION_JSON, TEXT_HTML})
public Response describe(@PathParam("path")
final List<PathSegment> pathList) throws RepositoryException, IOException {
public Response describe(@PathParam("path")
final List<PathSegment> pathList,
@Context final Request request)
throws RepositoryException, IOException {

final String path = toPath(pathList);
logger.trace("getting profile for {}", path);
if ("/".equals(path)) {
return Response.ok(getRepositoryProfile()).build();
}

final Session session = getAuthenticatedSession();
try {
Node node = session.getNode(path);
try {
final Node node = session.getNode(path);

if (node.isNodeType("nt:file")) {
final Datastream ds = getDatastream(node);
final DatastreamProfile profile = getDatastreamProfile(ds);
return createResponse(request,
ds.getLastModifiedDate(),
profile);

} else if (node.isNodeType("nt:folder")) {
final FedoraObject obj = getFedoraObject(node);
final ObjectProfile profile = getObjectProfile(obj);
return createResponse(request,
obj.getLastModifiedDate(),
profile);

} else {
return Response.status(406)
.entity("Unexpected node type: " + node.getPrimaryNodeType())
.build();
}

if (node.isNodeType("nt:file")) {
return Response.ok(getDatastreamProfile(node)).build();
}
} finally {
session.logout();
}
}

if (node.isNodeType("nt:folder")) {
return Response.ok(getObjectProfile(node)).build();
}
private Response createResponse(Request request,
Date date,
Object entity) {
Response.ResponseBuilder builder = null;

return Response.status(406).entity("Unexpected node type: " + node.getPrimaryNodeType()).build();
} finally {
session.logout();
}
}
if (null != date) {
final Date roundedDate = new Date();
roundedDate.setTime(date.getTime() - date.getTime() % 1000);

/**
* Returns an object profile.
*
* @param path
* @return 200
* @throws RepositoryException
* @throws IOException
*/
public ObjectProfile getObjectProfile(Node node)
throws RepositoryException, IOException {
builder = request.evaluatePreconditions(roundedDate);
}

final String path = node.getPath();
logger.trace("getting object profile {}", path);
final ObjectProfile objectProfile = new ObjectProfile();
final FedoraObject obj = objectService.getObject(node.getSession(), path);
objectProfile.pid = obj.getName();
objectProfile.objLabel = obj.getLabel();
objectProfile.objOwnerId = obj.getOwnerId();
objectProfile.objCreateDate = obj.getCreated();
objectProfile.objLastModDate = obj.getLastModified();
objectProfile.objSize = obj.getSize();
objectProfile.objItemIndexViewURL =
uriInfo.getAbsolutePathBuilder().path("datastreams").build();
objectProfile.objState = ObjectProfile.ObjectStates.A;
objectProfile.objModels = obj.getModels();

return objectProfile;
// Preconditions were not met if builder is null
if (null == builder) {
builder = Response.ok(entity);
}

}
final CacheControl cc = new CacheControl();
cc.setMaxAge(0);
cc.setMustRevalidate(true);

public DatastreamProfile getDatastreamProfile(Node node) throws RepositoryException, IOException {
final String path = node.getPath();
logger.trace("Executing getDatastream() with path: {}", path);
return getDatastreamProfile(datastreamService.getDatastream(node.getSession(), path));
return builder.cacheControl(cc).lastModified(date).build();
}

}
private FedoraObject getFedoraObject(Node node) throws RepositoryException {
final String path = node.getPath();
logger.trace("getting object profile {}", path);
return objectService.getObject(node.getSession(), path);
}

private ObjectProfile getObjectProfile(FedoraObject obj) throws RepositoryException {
final ObjectProfile objectProfile = new ObjectProfile();
objectProfile.pid = obj.getName();
objectProfile.objLabel = obj.getLabel();
objectProfile.objOwnerId = obj.getOwnerId();
objectProfile.objCreateDate = obj.getCreated();
objectProfile.objLastModDate = obj.getLastModified();
objectProfile.objSize = obj.getSize();
objectProfile.objItemIndexViewURL =
uriInfo.getAbsolutePathBuilder().path("datastreams").build();
objectProfile.objState = ObjectProfile.ObjectStates.A;
objectProfile.objModels = obj.getModels();

return objectProfile;
}

private Datastream getDatastream(Node node) throws RepositoryException {
final String path = node.getPath();
logger.trace("Executing getDatastream() with path: {}", path);
return datastreamService.getDatastream(node.getSession(), path);
}

private DatastreamProfile getDatastreamProfile(final Datastream ds)
private DatastreamProfile getDatastreamProfile(final Datastream ds)
throws RepositoryException, IOException {
logger.trace("Executing getDSProfile() with node: " + ds.getDsId());
final DatastreamProfile dsProfile = new DatastreamProfile();
Expand All @@ -140,6 +172,7 @@ private DatastreamProfile getDatastreamProfile(final Datastream ds)
dsProfile.dsMIME = ds.getMimeType();
dsProfile.dsSize = ds.getSize();
dsProfile.dsCreateDate = ds.getCreatedDate().toString();
dsProfile.dsLastModifiedDate = ds.getLastModifiedDate().toString();
dsProfile.dsStores = new DatastreamProfile.DSStores(ds,
llStoreService.getLowLevelCacheEntries(ds.getNode()));
return dsProfile;
Expand Down
Expand Up @@ -16,13 +16,16 @@
import java.io.InputStream;
import java.security.Principal;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import javax.jcr.LoginException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
Expand Down Expand Up @@ -158,9 +161,10 @@ public void testGetDatastreamsContents() throws RepositoryException,
final Datastream mockDs = TestHelpers.mockDatastream(pid, dsId, dsContent);
when(mockDatastreams.getDatastream(mockSession, path)).thenReturn(mockDs);

final Request mockRequest = mock(Request.class);
final Response resp =
testObj.getDatastreamsContents(createPathList(pid), Arrays
.asList(new String[] {dsId}));
.asList(dsId), mockRequest);
final MultiPart multipart = (MultiPart) resp.getEntity();

verify(mockDs).getContent();
Expand All @@ -169,6 +173,31 @@ public void testGetDatastreamsContents() throws RepositoryException,
final InputStream actualContent =
(InputStream) multipart.getBodyParts().get(0).getEntity();
assertEquals("asdf", IOUtils.toString(actualContent, "UTF-8"));
assertEquals(Status.OK.getStatusCode(), resp.getStatus());
}

@Test
public void testGetDatastreamsContentsCached() throws RepositoryException,
IOException {
final String pid = "FedoraDatastreamsTest1";
final String dsId = "testDS";
final String path = "/" + pid + "/" + dsId;
final String dsContent = "asdf";
final Datastream mockDs = TestHelpers.mockDatastream(pid, dsId, dsContent);
when(mockDatastreams.getDatastream(mockSession, path)).thenReturn(mockDs);

final Request mockRequest = mock(Request.class);
when(mockRequest.evaluatePreconditions(any(Date.class),
any(EntityTag.class))).thenReturn(
Response.notModified());

final Response resp =
testObj.getDatastreamsContents(createPathList(pid), Arrays
.asList(dsId), mockRequest);

verify(mockDs, never()).getContent();
verify(mockSession, never()).save();
assertEquals(Status.NOT_MODIFIED.getStatusCode(), resp.getStatus());
}

@Test
Expand Down

0 comments on commit 6709d5d

Please sign in to comment.