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;
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
175 this.context.variant = computeVariant(responseType);
176
177
178 final ResponseBuilder builder;
179 if (!exists) {
180 builder = this.request.evaluatePreconditions();
181
182 } else {
183
184 final Date lastModified = getLastModified != null ? getLastModified : getApplication()
185 .getLastModified();
186
187
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
193 builder = this.request.evaluatePreconditions(lastModified, etag);
194
195
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
204 if (builder != null) {
205
206
207 throw new WebApplicationException(builder.build());
208 }
209
210
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
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
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
259 final String[] encodings = new String[] { "identity", "gzip", "deflate" };
260
261
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);
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 }