1 package eu.fbk.knowledgestore.server.http.jaxrs;
2
3 import com.google.common.base.MoreObjects;
4 import com.google.common.base.Preconditions;
5 import com.google.common.base.Strings;
6 import com.google.common.collect.ImmutableList;
7 import com.google.common.collect.ImmutableMap;
8 import com.google.common.collect.ImmutableSet;
9 import com.google.common.collect.Lists;
10 import com.google.common.net.HttpHeaders;
11 import eu.fbk.knowledgestore.KnowledgeStore;
12 import eu.fbk.knowledgestore.OperationException;
13 import eu.fbk.knowledgestore.Outcome;
14 import eu.fbk.knowledgestore.data.*;
15 import eu.fbk.knowledgestore.internal.Logging;
16 import eu.fbk.knowledgestore.internal.Util;
17 import eu.fbk.knowledgestore.internal.jaxrs.Protocol;
18 import eu.fbk.knowledgestore.internal.jaxrs.Serializer;
19 import eu.fbk.knowledgestore.server.http.CustomConfig;
20 import eu.fbk.knowledgestore.server.http.UIConfig;
21 import org.eclipse.jetty.server.Server;
22 import org.glassfish.jersey.message.DeflateEncoder;
23 import org.glassfish.jersey.message.GZipEncoder;
24 import org.glassfish.jersey.message.internal.HttpDateFormat;
25 import org.glassfish.jersey.server.ResourceConfig;
26 import org.glassfish.jersey.server.ServerProperties;
27 import org.glassfish.jersey.server.mvc.mustache.MustacheMvcFeature;
28 import org.openrdf.model.URI;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.slf4j.MDC;
32
33 import javax.annotation.Nullable;
34 import javax.servlet.ServletContext;
35 import javax.ws.rs.Produces;
36 import javax.ws.rs.WebApplicationException;
37 import javax.ws.rs.container.*;
38 import javax.ws.rs.core.*;
39 import javax.ws.rs.core.Response.ResponseBuilder;
40 import javax.ws.rs.core.Response.Status;
41 import javax.ws.rs.ext.*;
42 import java.io.IOException;
43 import java.lang.annotation.Annotation;
44 import java.lang.reflect.Type;
45 import java.security.Principal;
46 import java.text.SimpleDateFormat;
47 import java.util.*;
48 import java.util.concurrent.Future;
49 import java.util.concurrent.TimeUnit;
50
51 public final class Application extends javax.ws.rs.core.Application {
52
53 public static final String STORE_ATTRIBUTE = "store";
54
55 public static final String TRACING_ATTRIBUTE = "tracing";
56
57 public static final String RESOURCE_ATTRIBUTE = "resource";
58
59 public static final String UI_ATTRIBUTE = "ui";
60
61 public static final String CUSTOM_ATTRIBUTE = "custom";
62
63 public static final int DEFAULT_TIMEOUT = 600000;
64
65 public static final int GRACE_PERIOD = 5000;
66
67 private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);
68
69 private static final String SERVER = String.format("KnowledgeStore/%s Jetty/%s",
70 Util.getVersion("eu.fbk.knowledgestore", "ks-core", "devel"), Server.getVersion());
71
72 private static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm");
73
74 private static ThreadLocal<URI> INVOCATION_ID = new ThreadLocal<URI>();
75
76 private static ThreadLocal<URI> OBJECT_ID = new ThreadLocal<URI>();
77
78 private static ThreadLocal<List<MediaType>> ACCEPT = new ThreadLocal<List<MediaType>>();
79
80 private static ThreadLocal<Future<?>> TIMEOUT_FUTURE = new ThreadLocal<Future<?>>();
81
82 private static long invocationCounter = 0;
83
84 private final UIConfig uiConfig;
85
86 private final KnowledgeStore store;
87
88 private final Set<Class<?>> classes;
89
90 private final Set<Object> singletons;
91
92 private final Map<String, Object> properties;
93 private final Map<String, CustomConfig> customConfigs;
94
95 private int pendingModifications;
96
97 private Date lastModified;
98
99 @SuppressWarnings("unchecked")
100 public Application(@Context final ServletContext context) {
101 this((UIConfig) context.getAttribute(UI_ATTRIBUTE),
102 (KnowledgeStore) context.getAttribute(STORE_ATTRIBUTE),
103 (Boolean) context.getAttribute(TRACING_ATTRIBUTE),
104 (Iterable<? extends Class<?>>) context.getAttribute(RESOURCE_ATTRIBUTE),
105 (Iterable<CustomConfig>) context.getAttribute(CUSTOM_ATTRIBUTE));
106 }
107
108 public Application(final UIConfig uiConfig, final KnowledgeStore store,
109 final Boolean enableTracing, final Iterable<? extends Class<?>> resourceClasses,
110 final Iterable<CustomConfig> configs) {
111
112
113 this.store = Preconditions.checkNotNull(store);
114 this.uiConfig = Preconditions.checkNotNull(uiConfig);
115 customConfigs = new HashMap<>();
116 if (configs != null) {
117 for (CustomConfig config : configs) {
118 customConfigs.put(config.getName(), config);
119 }
120 }
121
122
123 final ImmutableSet.Builder<Class<?>> classes = ImmutableSet.builder();
124 classes.add(DeflateEncoder.class);
125 classes.add(GZipEncoder.class);
126 for (final Class<?> resourceClass : resourceClasses) {
127 classes.add(resourceClass);
128 }
129 classes.add(Converter.class);
130 classes.add(Filter.class);
131 classes.add(Mapper.class);
132 classes.add(Serializer.class);
133 classes.add(MustacheMvcFeature.class);
134 this.classes = classes.build();
135
136
137 this.singletons = ImmutableSet.of();
138
139
140 final ImmutableMap.Builder<String, Object> properties = ImmutableMap.builder();
141 properties.put(ServerProperties.APPLICATION_NAME, "KnowledgeStore");
142 if (Boolean.TRUE.equals(enableTracing)) {
143 properties.put(ServerProperties.TRACING, "ALL");
144 properties.put(ServerProperties.TRACING_THRESHOLD, "TRACE");
145
146
147
148
149 properties.put(ServerProperties.MONITORING_STATISTICS_ENABLED, true);
150 properties.put(ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED, true);
151 }
152 properties.put(ServerProperties.WADL_FEATURE_DISABLE, false);
153 properties.put(ServerProperties.JSON_PROCESSING_FEATURE_DISABLE, true);
154 properties.put(ServerProperties.METAINF_SERVICES_LOOKUP_DISABLE, true);
155 properties.put(ServerProperties.MOXY_JSON_FEATURE_DISABLE, true);
156 properties.put(ServerProperties.OUTBOUND_CONTENT_LENGTH_BUFFER, 8192);
157 properties.put(MustacheMvcFeature.CACHE_TEMPLATES, true);
158 properties.put(MustacheMvcFeature.TEMPLATE_BASE_PATH,
159 "/eu/fbk/knowledgestore/server/http/jaxrs/");
160 this.properties = properties.build();
161
162
163 this.pendingModifications = 0;
164 this.lastModified = new Date();
165 }
166
167 public Map<String, CustomConfig> getCustomConfigs() {
168 return customConfigs;
169 }
170
171 public UIConfig getUIConfig() {
172 return this.uiConfig;
173 }
174
175 public KnowledgeStore getStore() {
176 return this.store;
177 }
178
179 @Override
180 public Set<Class<?>> getClasses() {
181 return this.classes;
182 }
183
184 @Override
185 public Set<Object> getSingletons() {
186 return this.singletons;
187 }
188
189 @Override
190 public Map<String, Object> getProperties() {
191 return this.properties;
192 }
193
194 public synchronized Date getLastModified() {
195 return this.pendingModifications == 0 ? this.lastModified : new Date();
196 }
197
198 synchronized void beginModification() {
199 ++this.pendingModifications;
200 }
201
202 synchronized void endModification() {
203 --this.pendingModifications;
204 if (this.pendingModifications == 0) {
205 this.lastModified = new Date();
206 }
207 }
208
209 static Application unwrap(final javax.ws.rs.core.Application application) {
210 if (application instanceof Application) {
211 return (Application) application;
212 } else if (application instanceof ResourceConfig) {
213 return (Application) ((ResourceConfig) application).getApplication();
214 }
215 Preconditions.checkNotNull(application, "Null application");
216 throw new IllegalArgumentException("Invalid application class "
217 + application.getClass().getName());
218 }
219
220 @Provider
221 static final class Converter implements ParamConverterProvider {
222
223 private static final ParamConverter<URI> URI_CONVERTER = new ParamConverter<URI>() {
224
225 @Override
226 public URI fromString(final String string) {
227 try {
228 return (URI) Data.parseValue(string, Data.getNamespaceMap());
229 } catch (final ParseException ex) {
230 throw new WebApplicationException(ex.getMessage(), Status.BAD_REQUEST);
231 }
232 }
233
234 @Override
235 public String toString(final URI uri) {
236 return Data.toString(uri, null);
237 }
238
239 };
240
241 private static final ParamConverter<XPath> XPATH_CONVERTER = new ParamConverter<XPath>() {
242
243 @Override
244 public XPath fromString(final String string) {
245 try {
246 return XPath.parse(Data.getNamespaceMap(), string);
247 } catch (final ParseException ex) {
248 throw new WebApplicationException(ex.getMessage(), Status.BAD_REQUEST);
249 }
250 }
251
252 @Override
253 public String toString(final XPath xpath) {
254 return xpath.toString();
255 }
256
257 };
258
259 private static final ParamConverter<Criteria> CRITERIA_CONVERTER = new ParamConverter<Criteria>() {
260
261 @Override
262 public Criteria fromString(final String string) {
263 try {
264 return Criteria.parse(string, Data.getNamespaceMap());
265 } catch (final ParseException ex) {
266 throw new WebApplicationException(ex.getMessage(), Status.BAD_REQUEST);
267 }
268 }
269
270 @Override
271 public String toString(final Criteria criteria) {
272 return criteria.toString();
273 }
274
275 };
276
277 @SuppressWarnings("unchecked")
278 @Override
279 public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType,
280 final Annotation[] annotations) {
281
282 if (rawType.equals(URI.class)) {
283 return (ParamConverter<T>) URI_CONVERTER;
284 } else if (rawType.equals(XPath.class)) {
285 return (ParamConverter<T>) XPATH_CONVERTER;
286 } else if (rawType.equals(Criteria.class)) {
287 return (ParamConverter<T>) CRITERIA_CONVERTER;
288 }
289 return null;
290 }
291
292 }
293
294 @Provider
295 @PreMatching
296 static final class Filter implements ContainerRequestFilter, ContainerResponseFilter,
297 WriterInterceptor {
298
299 private static final String PROPERTY_TIMESTAMP = "timestamp";
300
301 @Override
302 public void filter(final ContainerRequestContext request) throws IOException {
303
304
305 final long timestamp = System.currentTimeMillis();
306 request.setProperty(PROPERTY_TIMESTAMP, timestamp);
307
308
309 List<MediaType> acceptTypes = request.getAcceptableMediaTypes();
310 String accept = request.getUriInfo().getQueryParameters()
311 .getFirst(Protocol.PARAMETER_ACCEPT);
312 if (accept == null) {
313 accept = MoreObjects.firstNonNull(request.getHeaderString(HttpHeaders.ACCEPT),
314 "*/*");
315 } else {
316 request.getHeaders().putSingle(HttpHeaders.ACCEPT, accept);
317 acceptTypes = Lists.newArrayList();
318 for (final String type : accept.split(",")) {
319 acceptTypes.add(MediaType.valueOf(type.trim()));
320 }
321 }
322
323
324 long timeout = DEFAULT_TIMEOUT;
325 try {
326 final Thread thread = Thread.currentThread();
327 final String timeoutString = Strings.nullToEmpty(
328 request.getUriInfo().getQueryParameters()
329 .getFirst(Protocol.PARAMETER_TIMEOUT)).trim();
330 final long theTimeout = "".equals(timeoutString) ? DEFAULT_TIMEOUT : Long
331 .parseLong(timeoutString) * 1000;
332 timeout = theTimeout;
333 TIMEOUT_FUTURE.set(Data.getExecutor().schedule(new Runnable() {
334
335 @Override
336 public void run() {
337 synchronized (Filter.this) {
338 LOGGER.info("Http: Request timed out after {} ms", theTimeout);
339 thread.interrupt();
340 }
341 }
342
343 }, timeout + GRACE_PERIOD, TimeUnit.MILLISECONDS));
344 } catch (final Throwable ex) {
345
346 }
347
348
349 final URI invocationID = extractInvocationID(request);
350 final URI objectID = extractObjectID(request);
351 final String username = extractUsername(request);
352 final boolean chunkedInput = extractChunkedInput(request);
353 final boolean cachingEnabled = extractCachingEnabled(request);
354
355
356 INVOCATION_ID.set(invocationID);
357 OBJECT_ID.set(objectID);
358 ACCEPT.set(acceptTypes);
359
360
361
362 MDC.put(Logging.MDC_CONTEXT, invocationID.stringValue());
363
364
365 Resource.begin(invocationID, objectID, username, chunkedInput, cachingEnabled, timeout);
366
367
368 request.getHeaders().putSingle(Protocol.HEADER_INVOCATION, invocationID.stringValue());
369
370
371 if (LOGGER.isDebugEnabled()) {
372 final String etag = request.getHeaders().getFirst(HttpHeaders.IF_NONE_MATCH);
373 final String lastModified = reformatDate(request.getHeaders().getFirst(
374 HttpHeaders.IF_MODIFIED_SINCE));
375 final StringBuilder builder = new StringBuilder("Http: ");
376 builder.append(request.getMethod());
377 builder.append(' ').append(request.getUriInfo().getRequestUri());
378 builder.append(' ').append(accept);
379 final String type = request.getHeaderString(HttpHeaders.CONTENT_TYPE);
380 if (type != null) {
381 builder.append(' ').append(type);
382 }
383 final String encoding = request.getHeaderString(HttpHeaders.CONTENT_ENCODING);
384 if (encoding != null) {
385 builder.append(' ').append(encoding);
386 }
387 if (etag != null) {
388 builder.append(' ').append(etag);
389 }
390 if (lastModified != null) {
391 builder.append('/').append(lastModified);
392 }
393 final Principal user = request.getSecurityContext().getUserPrincipal();
394 if (user != null) {
395 builder.append(' ').append(user.getName());
396 }
397 LOGGER.debug(builder.toString());
398 }
399 }
400
401 @Override
402 public void filter(final ContainerRequestContext request,
403 final ContainerResponseContext response) throws IOException {
404
405 try {
406
407 final URI invocationID = INVOCATION_ID.get();
408
409
410 response.getHeaders().putSingle("Server", SERVER);
411 response.getHeaders().add(Protocol.HEADER_INVOCATION, invocationID.stringValue());
412
413
414 if (LOGGER.isDebugEnabled()) {
415 final long elapsed = System.currentTimeMillis()
416 - (Long) request.getProperty(PROPERTY_TIMESTAMP);
417 final StringBuilder builder = new StringBuilder();
418 builder.append("Http: status ");
419 builder.append(response.getStatus());
420 if (response.hasEntity()) {
421 final String etag = response.getHeaderString(HttpHeaders.ETAG);
422 if (etag != null) {
423 builder.append(", ").append(etag);
424 } else {
425 builder.append(", ").append(response.getMediaType());
426 }
427 try {
428 final Date lastModified = response.getLastModified();
429 if (lastModified != null) {
430 synchronized (DATE_FORMAT) {
431 builder.append(", ").append(DATE_FORMAT.format(lastModified));
432 }
433 }
434 } catch (final Throwable ex) {
435
436 }
437 }
438 builder.append(", ").append(elapsed).append(" ms");
439 LOGGER.debug(builder.toString());
440 }
441
442 } finally {
443
444 if (response.getEntity() == null) {
445 complete();
446 }
447 }
448 }
449
450 @Override
451 public void aroundWriteTo(final WriterInterceptorContext context) throws IOException,
452 WebApplicationException {
453
454 try {
455
456 context.proceed();
457
458 } finally {
459
460 complete();
461 }
462 }
463
464 private void complete() {
465 Resource.end();
466 final Future<?> future = TIMEOUT_FUTURE.get();
467 if (future != null) {
468 TIMEOUT_FUTURE.set(null);
469 future.cancel(false);
470
471 synchronized (Filter.this) {
472 Thread.interrupted();
473 }
474 }
475 MDC.remove(Logging.MDC_CONTEXT);
476 }
477
478 private static URI extractInvocationID(final ContainerRequestContext request) {
479 final String id = request.getHeaderString(Protocol.HEADER_INVOCATION);
480 if (id != null) {
481 try {
482 return Data.getValueFactory().createURI(id);
483 } catch (final Throwable ex) {
484
485 }
486 }
487 final long ts = System.currentTimeMillis();
488 long counterSnapshot;
489 synchronized (Application.class) {
490 ++invocationCounter;
491 if (invocationCounter < ts) {
492 invocationCounter = ts;
493 }
494 counterSnapshot = invocationCounter;
495 }
496 return Data.getValueFactory().createURI("req:" + Long.toString(counterSnapshot, 32));
497 }
498
499 private static URI extractObjectID(final ContainerRequestContext request) {
500 final List<String> ids = request.getUriInfo().getQueryParameters().get("id");
501 if (ids != null && ids.size() == 1) {
502 try {
503 return (URI) Data.parseValue(ids.get(0), Data.getNamespaceMap());
504 } catch (final Throwable ex) {
505
506 }
507 }
508 return null;
509 }
510
511 private static String extractUsername(final ContainerRequestContext request) {
512 final SecurityContext context = request.getSecurityContext();
513 if (context != null && context.getUserPrincipal() != null) {
514 final Principal principal = context.getUserPrincipal();
515 if (principal != null) {
516 return principal.getName();
517 }
518 }
519 return null;
520 }
521
522 private static boolean extractChunkedInput(final ContainerRequestContext request) {
523 final List<String> values = request.getHeaders().get(Protocol.HEADER_CHUNKED);
524 return values != null && values.size() == 1 && "true".equalsIgnoreCase(values.get(0));
525 }
526
527 private static boolean extractCachingEnabled(final ContainerRequestContext request) {
528 final List<String> values = request.getHeaders().get("Cache-Control");
529 if (values != null && values.size() == 1) {
530 try {
531 final CacheControl cacheControl = CacheControl.valueOf(values.get(0));
532 return !cacheControl.isNoCache() && !cacheControl.isNoStore();
533 } catch (final Throwable ex) {
534
535 }
536 }
537 return true;
538 }
539
540 @Nullable
541 private static String reformatDate(@Nullable final String httpDate) {
542 if (httpDate != null) {
543 try {
544 final Date date = HttpDateFormat.readDate(httpDate);
545 synchronized (DATE_FORMAT) {
546 return DATE_FORMAT.format(date);
547 }
548 } catch (final Throwable ex) {
549
550 }
551 }
552 return null;
553 }
554
555 }
556
557 @Provider
558 @Produces(Protocol.MIME_TYPES_RDF)
559 static final class Mapper implements ExceptionMapper<Throwable> {
560
561 private static final List<MediaType> RDF_TYPES;
562
563 static {
564 final ImmutableList.Builder<MediaType> builder = ImmutableList.builder();
565 for (final String token : Protocol.MIME_TYPES_RDF.split(",")) {
566 builder.add(MediaType.valueOf(token.trim()));
567 }
568 RDF_TYPES = builder.build();
569 }
570
571 private static MediaType selectType() {
572 for (final MediaType acceptableType : ACCEPT.get()) {
573 for (final MediaType supportedType : RDF_TYPES) {
574 if (acceptableType.isCompatible(supportedType)) {
575 return supportedType;
576 }
577 }
578 }
579 return RDF_TYPES.get(0);
580 }
581
582 @Override
583 public Response toResponse(final Throwable throwable) {
584
585
586 final Throwable ex = throwable instanceof RuntimeException
587 && throwable.getCause() instanceof OperationException ? throwable.getCause()
588 : throwable;
589
590
591 final URI invocationID = INVOCATION_ID.get();
592 final URI objectID = OBJECT_ID.get();
593
594
595 int httpStatus;
596 MultivaluedMap<String, Object> headers = null;
597 Outcome outcome = null;
598
599 if (ex instanceof OperationException) {
600 outcome = ((OperationException) ex).getOutcome();
601 httpStatus = outcome.getStatus().getHTTPStatus();
602
603 } else if (ex instanceof WebApplicationException) {
604 Outcome.Status status = null;
605 final Response exResponse = ((WebApplicationException) ex).getResponse();
606 headers = exResponse.getHeaders();
607 httpStatus = exResponse.getStatus();
608 if (httpStatus >= 400 && httpStatus != Status.PRECONDITION_FAILED.getStatusCode()) {
609 status = Outcome.Status.valueOf(httpStatus);
610 outcome = Outcome.create(status, invocationID, objectID, exResponse
611 .hasEntity() ? exResponse.getEntity().toString() : ex.getMessage());
612 }
613
614 } else {
615 httpStatus = Status.INTERNAL_SERVER_ERROR.getStatusCode();
616 outcome = Outcome.create(Outcome.Status.ERROR_UNEXPECTED, invocationID, objectID,
617 ex.getMessage() + " [" + ex.getClass().getSimpleName() + "]");
618 }
619
620
621 if (httpStatus >= 500) {
622 LOGGER.error("Http: reporting server error " + httpStatus, ex);
623 } else if (httpStatus >= 400) {
624 LOGGER.debug("Http: reporting client error: " + httpStatus + " - "
625 + ex.getMessage() + " (" + ex.getClass().getSimpleName() + ")");
626 }
627
628
629 final ResponseBuilder builder = Response.status(httpStatus);
630 if (outcome != null && httpStatus >= 400
631 && httpStatus != Status.PRECONDITION_FAILED.getStatusCode()) {
632 final CacheControl cacheControl = new CacheControl();
633 cacheControl.setNoStore(true);
634 builder.entity(
635 new GenericEntity<Stream<Outcome>>(Stream.create(outcome),
636 Protocol.STREAM_OF_OUTCOMES.getType())).cacheControl(cacheControl)
637 .type(selectType());
638 }
639 if (headers != null) {
640 for (final Map.Entry<String, List<Object>> entry : headers.entrySet()) {
641 final String name = entry.getKey();
642 for (final Object value : entry.getValue()) {
643 builder.header(name, value);
644 }
645 }
646 }
647 return builder.build();
648 }
649 }
650
651 }