package eu.fbk.knowledgestore.server.http.jaxrs;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.List;
import javax.annotation.Nullable;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.GenericType;
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 javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Variant;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import org.openrdf.model.URI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.fbk.knowledgestore.KnowledgeStore;
import eu.fbk.knowledgestore.OperationException;
import eu.fbk.knowledgestore.Outcome;
import eu.fbk.knowledgestore.Session;
import eu.fbk.knowledgestore.internal.jaxrs.Protocol;
import eu.fbk.knowledgestore.server.http.UIConfig;
public abstract class Resource {
private static final Logger LOGGER = LoggerFactory.getLogger(Resource.class);
private static final ThreadLocal<RequestContext> THREAD_CONTEXT //
= new ThreadLocal<RequestContext>();
@Context
private javax.ws.rs.core.Application application;
@Context
private Request request;
@Context
private ResourceInfo resource;
@Context
private UriInfo uri;
@Nullable
private final RequestContext context; // A solution based on injection should be rather used
Resource() {
this.context = Preconditions.checkNotNull(THREAD_CONTEXT.get());
}
final UIConfig getUIConfig() {
return Application.unwrap(this.application).getUIConfig();
}
final Application getApplication() {
return Application.unwrap(this.application);
}
final KnowledgeStore getStore() {
return Application.unwrap(this.application).getStore();
}
final Session getSession() {
if (this.context.session == null) {
this.context.session = getStore().newSession(getUsername(), null);
this.context.closeables.add(this.context.session);
}
return this.context.session;
}
final URI getInvocationID() {
return this.context.invocationID;
}
@Nullable
final URI getObjectID() {
return this.context.objectID;
}
@Nullable
final String getUsername() {
return this.context.username;
}
final String getMethod() {
return this.request.getMethod();
}
final UriInfo getUriInfo() {
return this.uri;
}
final boolean isChunkedInput() {
return this.context.chunkedInput;
}
final boolean isCachingEnabled() {
return this.context.cachingEnabled;
}
final long getTimeout() {
return this.context.timeout;
}
final <T extends Closeable> T closeQuietly(@Nullable final T closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (final Throwable ex) {
LOGGER.error("Exception caught closing " + closeable.getClass().getSimpleName(),
ex);
}
}
return closeable;
}
final <T extends Closeable> T closeOnCompletion(@Nullable final T closeable) {
if (closeable != null) {
this.context.closeables.add(closeable);
}
return closeable;
}
final void check(final boolean condition, final Outcome.Status errorStatus,
@Nullable final String errorMessage, final Object... errorArgs)
throws OperationException {
if (!condition) {
throw new OperationException(newOutcome(errorStatus, errorMessage == null ? null
: String.format(errorMessage, errorArgs)));
}
}
final <T> T checkNotNull(final T object, final Outcome.Status errorStatus,
@Nullable final String errorMessage, final Object... errorArgs)
throws OperationException {
if (object == null) {
throw new OperationException(newOutcome(errorStatus, errorMessage == null ? null
: String.format(errorMessage, errorArgs)));
}
return object;
}
final void init(final boolean modification, @Nullable final String responseType)
throws OperationException {
doInit(modification, false, responseType, null, null);
}
final void init(final boolean modification, @Nullable final String responseType,
@Nullable final Date getLastModified, @Nullable final String getTag)
throws OperationException {
doInit(modification, true, responseType, getLastModified, getTag);
}
private void doInit(final boolean modification, final boolean exists,
@Nullable final String responseType, @Nullable final Date getLastModified,
@Nullable final String getTag) throws OperationException {
// Determine returned variant
this.context.variant = computeVariant(responseType);
// Evaluate preconditions, based on available parameters (last modified, tag)
final ResponseBuilder builder;
if (!exists) {
builder = this.request.evaluatePreconditions();
} else {
// Initialize last modified
final Date lastModified = getLastModified != null ? getLastModified : getApplication()
.getLastModified();
// Initialize etag
final EntityTag etag = new EntityTag(String.format("%s,%s,%s", getTag != null ? getTag
: Long.toString(lastModified.getTime(), 16), this.context.variant
.getMediaType().toString(), this.context.variant.getEncoding()));
// Check preconditions
builder = this.request.evaluatePreconditions(lastModified, etag);
// Store last modified and etag for later inclusion in response, in case of retrieval
if ("GET".equalsIgnoreCase(this.request.getMethod())
|| "HEAD".equalsIgnoreCase(this.request.getMethod())) {
this.context.lastModified = lastModified;
this.context.etag = etag;
}
}
// If preconditions failed, return the Response built by JAX-RS
if (builder != null) {
// Note: no Outcome entity sent here as it can confuse clients; also, in case of 304
// Not Modified, an entity MUST not be sent.
throw new WebApplicationException(builder.build());
}
// Interrupt processing in case of a probe request
if (this.uri.getQueryParameters().containsKey(Protocol.PARAMETER_PROBE)) {
String newURI = this.uri.getRequestUri().toString();
int start = newURI.indexOf('?' + Protocol.PARAMETER_PROBE);
if (start < 0) {
start = newURI.indexOf('&' + Protocol.PARAMETER_PROBE);
}
int end = newURI.indexOf('&', start + 1);
if (end < 0) {
end = newURI.length();
}
newURI = newURI.substring(0, start) + newURI.substring(end);
final Response redirect = Response.status(Status.FOUND)
.location(java.net.URI.create(newURI)).build();
throw new WebApplicationException(redirect);
}
// Register modification; unregister it when request processing completes
if (modification) {
getApplication().beginModification();
closeOnCompletion(new Closeable() {
@Override
public void close() throws IOException {
getApplication().endModification();
}
});
}
}
private Variant computeVariant(@Nullable final String mimeType) throws OperationException {
// Determine supported media types from supplied type or @Produces annotation
MediaType[] types = null;
if (mimeType != null) {
types = parseMediaTypes(mimeType);
} else {
types = new MediaType[] { MediaType.WILDCARD_TYPE };
final Method method = this.resource.getResourceMethod();
if (method != null) {
final Produces produces = method.getAnnotation(Produces.class);
if (produces != null) {
types = parseMediaTypes(produces.value());
}
}
}
// Determine supported encodings from supplied encoding or using defaults
final String[] encodings = new String[] { "identity", "gzip", "deflate" };
// Perform negotiation and return the result, failing if there is no acceptable variant
final Variant variant = this.request.selectVariant(Variant.mediaTypes(types)
.encodings(encodings).build());
check(variant != null, Outcome.Status.ERROR_NOT_ACCEPTABLE, null);
return variant;
}
private MediaType[] parseMediaTypes(final String... strings) {
final List<MediaType> list = Lists.newArrayList();
for (final String string : strings) {
for (final String token : Splitter.on(',').trimResults().omitEmptyStrings()
.split(string)) {
list.add(MediaType.valueOf(token));
}
}
return list.toArray(new MediaType[list.size()]);
}
final ResponseBuilder newResponseBuilder(final int status, @Nullable final Object entity,
@Nullable final GenericType<?> type) {
return newResponseBuilder(Status.fromStatusCode(status), entity, type);
}
final ResponseBuilder newResponseBuilder(final Status status, @Nullable final Object entity,
@Nullable final GenericType<?> type) {
Preconditions.checkState(this.context.variant != null);
final ResponseBuilder builder = Response.status(status);
if (entity != null) {
builder.entity(type == null ? entity : new GenericEntity<Object>(entity, type
.getType()));
builder.variant(this.context.variant);
final CacheControl cacheControl = new CacheControl();
cacheControl.setNoStore(true);
if ("GET".equalsIgnoreCase(this.request.getMethod())
|| "HEAD".equalsIgnoreCase(this.request.getMethod())) {
builder.lastModified(this.context.lastModified);
builder.tag(this.context.etag);
if (isCachingEnabled()) {
cacheControl.setNoStore(false);
cacheControl.setMaxAge(0); // always stale, must revalidate each time
cacheControl.setMustRevalidate(true);
cacheControl.setPrivate(getUsername() != null);
cacheControl.setNoTransform(true);
}
}
builder.cacheControl(cacheControl);
}
return builder;
}
final Outcome newOutcome(final Outcome.Status status, @Nullable final String message,
final Object... messageArgs) {
return Outcome.create(status, getInvocationID(), getObjectID(), message == null ? null
: String.format(message, messageArgs));
}
final OperationException newException(final Outcome.Status status,
@Nullable final Throwable cause, @Nullable final String message, final Object... args) {
String actualMessage = message;
if (cause != null) {
actualMessage = message == null ? cause.getMessage() : message + " - "
+ cause.getMessage();
}
return new OperationException(newOutcome(status, actualMessage, args), cause);
}
static void begin(final URI invocationID, @Nullable final URI objectID,
@Nullable final String username, final boolean chunkedInput,
final boolean cachingEnabled, final long timeout) {
THREAD_CONTEXT.set(new RequestContext(invocationID, objectID, username, chunkedInput,
cachingEnabled, timeout));
}
static void end() {
final RequestContext context = THREAD_CONTEXT.get();
if (context != null) {
context.close();
THREAD_CONTEXT.set(null);
}
}
private static final class RequestContext implements Closeable {
final URI invocationID;
@Nullable
final URI objectID;
@Nullable
final String username;
final boolean chunkedInput;
final boolean cachingEnabled;
final long timeout;
final List<Closeable> closeables;
@Nullable
Session session;
@Nullable
Variant variant;
@Nullable
Date lastModified;
@Nullable
EntityTag etag;
private boolean closed;
RequestContext(final URI invocationID, final URI objectID, final String username,
final boolean chunkedInput, final boolean cacheEnabled, final long timeout) {
this.invocationID = Preconditions.checkNotNull(invocationID);
this.objectID = objectID;
this.username = username;
this.chunkedInput = chunkedInput;
this.cachingEnabled = cacheEnabled;
this.timeout = timeout;
this.closeables = Lists.newArrayList();
this.closed = false;
}
@Override
public void close() {
if (this.closed) {
return;
}
try {
for (final Closeable closeable : this.closeables) {
try {
closeable.close();
} catch (final Throwable ex) {
LOGGER.error("Error closing " + closeable.getClass().getSimpleName(), ex);
}
}
this.closeables.clear();
this.session = null;
this.variant = null;
this.lastModified = null;
this.etag = null;
} finally {
this.closed = true;
}
}
}
}