1   package eu.fbk.knowledgestore.server.http.jaxrs;
2   
3   import java.io.Closeable;
4   import java.io.IOException;
5   import java.lang.reflect.Method;
6   import java.util.Date;
7   import java.util.List;
8   
9   import javax.annotation.Nullable;
10  import javax.ws.rs.Produces;
11  import javax.ws.rs.WebApplicationException;
12  import javax.ws.rs.container.ResourceInfo;
13  import javax.ws.rs.core.CacheControl;
14  import javax.ws.rs.core.Context;
15  import javax.ws.rs.core.EntityTag;
16  import javax.ws.rs.core.GenericEntity;
17  import javax.ws.rs.core.GenericType;
18  import javax.ws.rs.core.MediaType;
19  import javax.ws.rs.core.Request;
20  import javax.ws.rs.core.Response;
21  import javax.ws.rs.core.Response.ResponseBuilder;
22  import javax.ws.rs.core.Response.Status;
23  import javax.ws.rs.core.UriInfo;
24  import javax.ws.rs.core.Variant;
25  
26  import com.google.common.base.Preconditions;
27  import com.google.common.base.Splitter;
28  import com.google.common.collect.Lists;
29  
30  import org.openrdf.model.URI;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  import eu.fbk.knowledgestore.KnowledgeStore;
35  import eu.fbk.knowledgestore.OperationException;
36  import eu.fbk.knowledgestore.Outcome;
37  import eu.fbk.knowledgestore.Session;
38  import eu.fbk.knowledgestore.internal.jaxrs.Protocol;
39  import eu.fbk.knowledgestore.server.http.UIConfig;
40  
41  public abstract class Resource {
42  
43      private static final Logger LOGGER = LoggerFactory.getLogger(Resource.class);
44  
45      private static final ThreadLocal<RequestContext> THREAD_CONTEXT //
46      = new ThreadLocal<RequestContext>();
47  
48      @Context
49      private javax.ws.rs.core.Application application;
50  
51      @Context
52      private Request request;
53  
54      @Context
55      private ResourceInfo resource;
56  
57      @Context
58      private UriInfo uri;
59  
60      @Nullable
61      private final RequestContext context; // A solution based on injection should be rather used
62  
63      Resource() {
64          this.context = Preconditions.checkNotNull(THREAD_CONTEXT.get());
65      }
66  
67      final UIConfig getUIConfig() {
68          return Application.unwrap(this.application).getUIConfig();
69      }
70  
71      final Application getApplication() {
72          return Application.unwrap(this.application);
73      }
74  
75      final KnowledgeStore getStore() {
76          return Application.unwrap(this.application).getStore();
77      }
78  
79      final Session getSession() {
80          if (this.context.session == null) {
81              this.context.session = getStore().newSession(getUsername(), null);
82              this.context.closeables.add(this.context.session);
83          }
84          return this.context.session;
85      }
86  
87      final URI getInvocationID() {
88          return this.context.invocationID;
89      }
90  
91      @Nullable
92      final URI getObjectID() {
93          return this.context.objectID;
94      }
95  
96      @Nullable
97      final String getUsername() {
98          return this.context.username;
99      }
100 
101     final String getMethod() {
102         return this.request.getMethod();
103     }
104 
105     final UriInfo getUriInfo() {
106         return this.uri;
107     }
108 
109     final boolean isChunkedInput() {
110         return this.context.chunkedInput;
111     }
112 
113     final boolean isCachingEnabled() {
114         return this.context.cachingEnabled;
115     }
116 
117     final long getTimeout() {
118         return this.context.timeout;
119     }
120 
121     final <T extends Closeable> T closeQuietly(@Nullable final T closeable) {
122         if (closeable != null) {
123             try {
124                 closeable.close();
125             } catch (final Throwable ex) {
126                 LOGGER.error("Exception caught closing " + closeable.getClass().getSimpleName(),
127                         ex);
128             }
129         }
130         return closeable;
131     }
132 
133     final <T extends Closeable> T closeOnCompletion(@Nullable final T closeable) {
134         if (closeable != null) {
135             this.context.closeables.add(closeable);
136         }
137         return closeable;
138     }
139 
140     final void check(final boolean condition, final Outcome.Status errorStatus,
141             @Nullable final String errorMessage, final Object... errorArgs)
142             throws OperationException {
143         if (!condition) {
144             throw new OperationException(newOutcome(errorStatus, errorMessage == null ? null
145                     : String.format(errorMessage, errorArgs)));
146         }
147     }
148 
149     final <T> T checkNotNull(final T object, final Outcome.Status errorStatus,
150             @Nullable final String errorMessage, final Object... errorArgs)
151             throws OperationException {
152         if (object == null) {
153             throw new OperationException(newOutcome(errorStatus, errorMessage == null ? null
154                     : String.format(errorMessage, errorArgs)));
155         }
156         return object;
157     }
158 
159     final void init(final boolean modification, @Nullable final String responseType)
160             throws OperationException {
161         doInit(modification, false, responseType, null, null);
162     }
163 
164     final void init(final boolean modification, @Nullable final String responseType,
165             @Nullable final Date getLastModified, @Nullable final String getTag)
166             throws OperationException {
167         doInit(modification, true, responseType, getLastModified, getTag);
168     }
169 
170     private void doInit(final boolean modification, final boolean exists,
171             @Nullable final String responseType, @Nullable final Date getLastModified,
172             @Nullable final String getTag) throws OperationException {
173 
174         // Determine returned variant
175         this.context.variant = computeVariant(responseType);
176 
177         // Evaluate preconditions, based on available parameters (last modified, tag)
178         final ResponseBuilder builder;
179         if (!exists) {
180             builder = this.request.evaluatePreconditions();
181 
182         } else {
183             // Initialize last modified
184             final Date lastModified = getLastModified != null ? getLastModified : getApplication()
185                     .getLastModified();
186 
187             // Initialize etag
188             final EntityTag etag = new EntityTag(String.format("%s,%s,%s", getTag != null ? getTag
189                     : Long.toString(lastModified.getTime(), 16), this.context.variant
190                     .getMediaType().toString(), this.context.variant.getEncoding()));
191 
192             // Check preconditions
193             builder = this.request.evaluatePreconditions(lastModified, etag);
194 
195             // Store last modified and etag for later inclusion in response, in case of retrieval
196             if ("GET".equalsIgnoreCase(this.request.getMethod())
197                     || "HEAD".equalsIgnoreCase(this.request.getMethod())) {
198                 this.context.lastModified = lastModified;
199                 this.context.etag = etag;
200             }
201         }
202 
203         // If preconditions failed, return the Response built by JAX-RS
204         if (builder != null) {
205             // Note: no Outcome entity sent here as it can confuse clients; also, in case of 304
206             // Not Modified, an entity MUST not be sent.
207             throw new WebApplicationException(builder.build());
208         }
209 
210         // Interrupt processing in case of a probe request
211         if (this.uri.getQueryParameters().containsKey(Protocol.PARAMETER_PROBE)) {
212             String newURI = this.uri.getRequestUri().toString();
213             int start = newURI.indexOf('?' + Protocol.PARAMETER_PROBE);
214             if (start < 0) {
215                 start = newURI.indexOf('&' + Protocol.PARAMETER_PROBE);
216             }
217             int end = newURI.indexOf('&', start + 1);
218             if (end < 0) {
219                 end = newURI.length();
220             }
221             newURI = newURI.substring(0, start) + newURI.substring(end);
222             final Response redirect = Response.status(Status.FOUND)
223                     .location(java.net.URI.create(newURI)).build();
224             throw new WebApplicationException(redirect);
225         }
226 
227         // Register modification; unregister it when request processing completes
228         if (modification) {
229             getApplication().beginModification();
230             closeOnCompletion(new Closeable() {
231 
232                 @Override
233                 public void close() throws IOException {
234                     getApplication().endModification();
235                 }
236 
237             });
238         }
239     }
240 
241     private Variant computeVariant(@Nullable final String mimeType) throws OperationException {
242 
243         // Determine supported media types from supplied type or @Produces annotation
244         MediaType[] types = null;
245         if (mimeType != null) {
246             types = parseMediaTypes(mimeType);
247         } else {
248             types = new MediaType[] { MediaType.WILDCARD_TYPE };
249             final Method method = this.resource.getResourceMethod();
250             if (method != null) {
251                 final Produces produces = method.getAnnotation(Produces.class);
252                 if (produces != null) {
253                     types = parseMediaTypes(produces.value());
254                 }
255             }
256         }
257 
258         // Determine supported encodings from supplied encoding or using defaults
259         final String[] encodings = new String[] { "identity", "gzip", "deflate" };
260 
261         // Perform negotiation and return the result, failing if there is no acceptable variant
262         final Variant variant = this.request.selectVariant(Variant.mediaTypes(types)
263                 .encodings(encodings).build());
264         check(variant != null, Outcome.Status.ERROR_NOT_ACCEPTABLE, null);
265         return variant;
266     }
267 
268     private MediaType[] parseMediaTypes(final String... strings) {
269         final List<MediaType> list = Lists.newArrayList();
270         for (final String string : strings) {
271             for (final String token : Splitter.on(',').trimResults().omitEmptyStrings()
272                     .split(string)) {
273                 list.add(MediaType.valueOf(token));
274             }
275         }
276         return list.toArray(new MediaType[list.size()]);
277     }
278 
279     final ResponseBuilder newResponseBuilder(final int status, @Nullable final Object entity,
280             @Nullable final GenericType<?> type) {
281         return newResponseBuilder(Status.fromStatusCode(status), entity, type);
282     }
283 
284     final ResponseBuilder newResponseBuilder(final Status status, @Nullable final Object entity,
285             @Nullable final GenericType<?> type) {
286         Preconditions.checkState(this.context.variant != null);
287         final ResponseBuilder builder = Response.status(status);
288         if (entity != null) {
289             builder.entity(type == null ? entity : new GenericEntity<Object>(entity, type
290                     .getType()));
291             builder.variant(this.context.variant);
292             final CacheControl cacheControl = new CacheControl();
293             cacheControl.setNoStore(true);
294             if ("GET".equalsIgnoreCase(this.request.getMethod())
295                     || "HEAD".equalsIgnoreCase(this.request.getMethod())) {
296                 builder.lastModified(this.context.lastModified);
297                 builder.tag(this.context.etag);
298                 if (isCachingEnabled()) {
299                     cacheControl.setNoStore(false);
300                     cacheControl.setMaxAge(0); // always stale, must revalidate each time
301                     cacheControl.setMustRevalidate(true);
302                     cacheControl.setPrivate(getUsername() != null);
303                     cacheControl.setNoTransform(true);
304                 }
305             }
306             builder.cacheControl(cacheControl);
307         }
308         return builder;
309     }
310 
311     final Outcome newOutcome(final Outcome.Status status, @Nullable final String message,
312             final Object... messageArgs) {
313         return Outcome.create(status, getInvocationID(), getObjectID(), message == null ? null
314                 : String.format(message, messageArgs));
315     }
316 
317     final OperationException newException(final Outcome.Status status,
318             @Nullable final Throwable cause, @Nullable final String message, final Object... args) {
319         String actualMessage = message;
320         if (cause != null) {
321             actualMessage = message == null ? cause.getMessage() : message + " - "
322                     + cause.getMessage();
323         }
324         return new OperationException(newOutcome(status, actualMessage, args), cause);
325     }
326 
327     static void begin(final URI invocationID, @Nullable final URI objectID,
328             @Nullable final String username, final boolean chunkedInput,
329             final boolean cachingEnabled, final long timeout) {
330         THREAD_CONTEXT.set(new RequestContext(invocationID, objectID, username, chunkedInput,
331                 cachingEnabled, timeout));
332     }
333 
334     static void end() {
335         final RequestContext context = THREAD_CONTEXT.get();
336         if (context != null) {
337             context.close();
338             THREAD_CONTEXT.set(null);
339         }
340     }
341 
342     private static final class RequestContext implements Closeable {
343 
344         final URI invocationID;
345 
346         @Nullable
347         final URI objectID;
348 
349         @Nullable
350         final String username;
351 
352         final boolean chunkedInput;
353 
354         final boolean cachingEnabled;
355 
356         final long timeout;
357 
358         final List<Closeable> closeables;
359 
360         @Nullable
361         Session session;
362 
363         @Nullable
364         Variant variant;
365 
366         @Nullable
367         Date lastModified;
368 
369         @Nullable
370         EntityTag etag;
371 
372         private boolean closed;
373 
374         RequestContext(final URI invocationID, final URI objectID, final String username,
375                 final boolean chunkedInput, final boolean cacheEnabled, final long timeout) {
376             this.invocationID = Preconditions.checkNotNull(invocationID);
377             this.objectID = objectID;
378             this.username = username;
379             this.chunkedInput = chunkedInput;
380             this.cachingEnabled = cacheEnabled;
381             this.timeout = timeout;
382             this.closeables = Lists.newArrayList();
383             this.closed = false;
384         }
385 
386         @Override
387         public void close() {
388             if (this.closed) {
389                 return;
390             }
391             try {
392                 for (final Closeable closeable : this.closeables) {
393                     try {
394                         closeable.close();
395                     } catch (final Throwable ex) {
396                         LOGGER.error("Error closing " + closeable.getClass().getSimpleName(), ex);
397                     }
398                 }
399                 this.closeables.clear();
400                 this.session = null;
401                 this.variant = null;
402                 this.lastModified = null;
403                 this.etag = null;
404             } finally {
405                 this.closed = true;
406             }
407         }
408 
409     }
410 
411 }