package com.wesabe.grendel.resources;
import java.security.SecureRandom;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.wesabe.grendel.auth.Credentials;
import com.wesabe.grendel.auth.Session;
import com.wesabe.grendel.entities.Document;
import com.wesabe.grendel.entities.dao.DocumentDAO;
import com.wesabe.grendel.entities.dao.UserDAO;
import com.wesabe.grendel.openpgp.CryptographicException;
import com.wideplay.warp.persist.Transactional;
/**
* A class which exposes {@link Document} as a resource.
*
* @author coda
*/
@Path("/users/{user_id}/documents/{name}")
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.WILDCARD)
public class DocumentResource {
private static final CacheControl CACHE_SETTINGS;
static {
CACHE_SETTINGS = new CacheControl();
CACHE_SETTINGS.setNoCache(true);
CACHE_SETTINGS.setNoStore(true);
CACHE_SETTINGS.setPrivate(true);
}
private final Provider<SecureRandom> randomProvider;
private final UserDAO userDAO;
private final DocumentDAO documentDAO;
@Inject
public DocumentResource(Provider<SecureRandom> randomProvider, UserDAO userDAO,
DocumentDAO documentDAO) {
this.randomProvider = randomProvider;
this.userDAO = userDAO;
this.documentDAO = documentDAO;
}
/**
* Responds to a {@link GET} request by decrypting the {@link Document} body
* and returning it.
* <p>
* <strong>N.B.:</strong> Requires Basic authentication.
* @throws CryptographicException
*/
@GET
public Response show(@Context Request request, @Context Credentials credentials,
@PathParam("user_id") String userId, @PathParam("name") String name) throws CryptographicException {
final Session session = credentials.buildSession(userDAO, userId);
final Document doc = documentDAO.findByOwnerAndName(session.getUser(), name);
if (doc == null) {
throw new WebApplicationException(Status.NOT_FOUND);
}
checkPreconditions(request, doc);
final byte[] body = doc.decryptBody(session.getKeySet());
return Response.ok()
.entity(body)
.type(doc.getContentType())
.cacheControl(CACHE_SETTINGS)
.lastModified(doc.getModifiedAt().toDate())
.tag(doc.getEtag())
.build();
}
/**
* Responds to a {@link DELETE} request by deleting the {@link Document}.
* <p>
* <strong>N.B.:</strong> Requires Basic authentication.
*/
@DELETE
@Transactional
public Response delete(@Context Request request, @Context Credentials credentials,
@PathParam("user_id") String userId, @PathParam("name") String name) {
final Session session = credentials.buildSession(userDAO, userId);
final Document doc = documentDAO.findByOwnerAndName(session.getUser(), name);
if (doc == null) {
throw new WebApplicationException(Status.NOT_FOUND);
}
checkPreconditions(request, doc);
documentDAO.delete(doc);
return Response.noContent().build();
}
/**
* Responds to a {@link PUT} request by replacing the specified
* {@link Document} with the request entity.
* <p>
* <strong>N.B.:</strong> Requires Basic authentication.
* @throws CryptographicException
*/
@PUT
@Transactional
public Response store(@Context Request request, @Context HttpHeaders headers,
@Context Credentials credentials, @PathParam("user_id") String userId,
@PathParam("name") String name, byte[] body) throws CryptographicException {
final Session session = credentials.buildSession(userDAO, userId);
Document doc = documentDAO.findByOwnerAndName(session.getUser(), name);
if (doc == null) {
doc = documentDAO.newDocument(session.getUser(), name, headers.getMediaType());
} else {
checkPreconditions(request, doc);
}
doc.setModifiedAt(new DateTime(DateTimeZone.UTC));
doc.encryptAndSetBody(
session.getKeySet(),
randomProvider.get(),
body
);
documentDAO.saveOrUpdate(doc);
return Response
.noContent()
.tag(doc.getEtag())
.build();
}
/**
* If the request has {@code If-Modified-Since} or {@code If-None-Match}
* headers, and the resource has a matching {@link Document#getModifiedAt()}
* or {@link Document#getEtag()}, returns a {@code 304 Unmodified},
* indicating the client has the most recent version of the resource.
*
* If the request has a {@code If-Unmodified-Since} or {@code If-Match}
* headers, and the resource has a more recent
* {@link Document#getModifiedAt()} or {@link Document#getEtag()}, returns
* a {@code 412 Precondition Failed}, indicating the client should re-read
* the resource before overwriting it.
*/
private void checkPreconditions(Request request, Document document) {
final EntityTag eTag = new EntityTag(document.getEtag());
final ResponseBuilder response = request.evaluatePreconditions(document.getModifiedAt().toDate(), eTag);
if (response != null) {
throw new WebApplicationException(response.build());
}
}
}