1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package eu.ehri.extension.base;
21
22 import com.fasterxml.jackson.core.JsonFactory;
23 import com.fasterxml.jackson.core.JsonGenerator;
24 import com.fasterxml.jackson.databind.ObjectMapper;
25 import com.google.common.base.Charsets;
26 import com.google.common.collect.BiMap;
27 import com.google.common.collect.ImmutableBiMap;
28 import com.google.common.collect.ImmutableMap;
29 import com.google.common.collect.Lists;
30 import com.tinkerpop.blueprints.Vertex;
31 import com.tinkerpop.frames.FramedGraph;
32 import com.tinkerpop.frames.FramedGraphFactory;
33 import com.tinkerpop.frames.modules.javahandler.JavaHandlerModule;
34 import eu.ehri.extension.errors.MissingOrInvalidUser;
35 import eu.ehri.project.acl.AnonymousAccessor;
36 import eu.ehri.project.api.Api;
37 import eu.ehri.project.api.ApiFactory;
38 import eu.ehri.project.api.QueryApi;
39 import eu.ehri.project.core.GraphManager;
40 import eu.ehri.project.core.GraphManagerFactory;
41 import eu.ehri.project.core.Tx;
42 import eu.ehri.project.core.TxGraph;
43 import eu.ehri.project.core.impl.TxNeo4jGraph;
44 import eu.ehri.project.definitions.Entities;
45 import eu.ehri.project.exceptions.ItemNotFound;
46 import eu.ehri.project.exceptions.SerializationError;
47 import eu.ehri.project.models.UserProfile;
48 import eu.ehri.project.models.base.Accessible;
49 import eu.ehri.project.models.base.Accessor;
50 import eu.ehri.project.models.base.Actioner;
51 import eu.ehri.project.models.base.Entity;
52 import eu.ehri.project.persistence.Serializer;
53 import org.neo4j.graphdb.GraphDatabaseService;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 import javax.ws.rs.core.CacheControl;
58 import javax.ws.rs.core.Context;
59 import javax.ws.rs.core.HttpHeaders;
60 import javax.ws.rs.core.MediaType;
61 import javax.ws.rs.core.Request;
62 import javax.ws.rs.core.Response;
63 import javax.ws.rs.core.StreamingOutput;
64 import javax.ws.rs.core.UriInfo;
65 import java.io.UnsupportedEncodingException;
66 import java.net.URI;
67 import java.net.URLDecoder;
68 import java.nio.charset.StandardCharsets;
69 import java.util.Collection;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.Optional;
73 import java.util.function.Supplier;
74
75
76
77
78
79 public abstract class AbstractResource implements TxCheckedResource {
80
81 public static final int DEFAULT_LIST_LIMIT = QueryApi.DEFAULT_LIMIT;
82 public static final int ITEM_CACHE_TIME = 60 * 5;
83
84 public static final String RESOURCE_ENDPOINT_PREFIX = "classes";
85
86 protected static final ObjectMapper jsonMapper = new ObjectMapper();
87 protected static final JsonFactory jsonFactory = jsonMapper.getFactory();
88
89 protected static final Logger logger = LoggerFactory.getLogger(AbstractResource.class);
90 private static final FramedGraphFactory graphFactory = new FramedGraphFactory(new JavaHandlerModule());
91
92 public static final String CSV_MEDIA_TYPE = "text/csv";
93
94
95
96
97 public final static String TURTLE_MIMETYPE = "text/turtle";
98 public final static String RDF_XML_MIMETYPE = "application/rdf+xml";
99 public final static String N3_MIMETYPE = "application/n-triples";
100 protected final BiMap<String, String> RDF_MIMETYPE_FORMATS = ImmutableBiMap.of(
101 N3_MIMETYPE, "N3",
102 TURTLE_MIMETYPE, "TTL",
103 RDF_XML_MIMETYPE, "RDF/XML"
104 );
105
106
107
108
109 public static final String SORT_PARAM = "sort";
110 public static final String FILTER_PARAM = "filter";
111 public static final String LIMIT_PARAM = "limit";
112 public static final String OFFSET_PARAM = "offset";
113 public static final String ACCESSOR_PARAM = "accessibleTo";
114 public static final String GROUP_PARAM = "group";
115 public static final String ALL_PARAM = "all";
116 public static final String ID_PARAM = "id";
117 public static final String LOG_PARAM = "log";
118 public static final String VERSION_PARAM = "version";
119 public static final String SCOPE_PARAM = "scope";
120 public static final String TOLERANT_PARAM = "tolerant";
121 public static final String COMMIT_PARAM = "commit";
122
123
124
125
126 public static final String INCLUDE_PROPS_PARAM = "_ip";
127
128
129
130
131 public static final String RANGE_HEADER_NAME = "Content-Range";
132 public static final String PATCH_HEADER_NAME = "X-Patch";
133 public static final String AUTH_HEADER_NAME = "X-User";
134 public static final String LOG_MESSAGE_HEADER_NAME = "X-LogMessage";
135 public static final String STREAM_HEADER_NAME = "X-Stream";
136
137
138
139
140
141
142 @Context
143 protected HttpHeaders requestHeaders;
144
145 @Context
146 protected Request request;
147
148
149
150
151 @Context
152 protected UriInfo uriInfo;
153
154 protected final FramedGraph<? extends TxGraph> graph;
155 protected final GraphManager manager;
156 private final Serializer serializer;
157
158
159
160
161
162
163 public AbstractResource(@Context GraphDatabaseService database) {
164 graph = graphFactory.create(new TxNeo4jGraph(database));
165 manager = GraphManagerFactory.getInstance(graph);
166 serializer = new Serializer.Builder(graph).build();
167 }
168
169 public FramedGraph<? extends TxGraph> getGraph() {
170 return graph;
171 }
172
173
174
175
176
177
178 protected Tx beginTx() {
179 return graph.getBaseGraph().beginTx();
180 }
181
182
183
184
185
186
187
188
189
190 protected Serializer getSerializer() {
191 Optional<List<String>> includeProps = Optional.ofNullable(uriInfo.getQueryParameters(true)
192 .get(INCLUDE_PROPS_PARAM));
193 return includeProps.isPresent()
194 ? serializer.withIncludedProperties(includeProps.get())
195 : serializer;
196 }
197
198
199
200
201
202
203
204 protected List<String> getStringListQueryParam(String key) {
205 List<String> value = uriInfo.getQueryParameters().get(key);
206 return value == null ? Lists.<String>newArrayList() : value;
207 }
208
209
210
211
212
213
214
215
216
217 protected int getIntQueryParam(String key, int defaultValue) {
218 String value = uriInfo.getQueryParameters().getFirst(key);
219 try {
220 return Integer.parseInt(value);
221 } catch (Exception e) {
222 return defaultValue;
223 }
224 }
225
226
227
228
229
230
231 protected QueryApi getQuery() {
232 return api().query()
233 .setOffset(getIntQueryParam(OFFSET_PARAM, 0))
234 .setLimit(getIntQueryParam(LIMIT_PARAM, DEFAULT_LIST_LIMIT))
235 .filter(getStringListQueryParam(FILTER_PARAM))
236 .orderBy(getStringListQueryParam(SORT_PARAM))
237 .setStream(isStreaming());
238 }
239
240
241
242
243
244
245
246 protected Accessor getRequesterUserProfile() {
247 Optional<String> id = getRequesterIdentifier();
248 if (!id.isPresent()) {
249 return AnonymousAccessor.getInstance();
250 } else {
251 try {
252 return manager.getEntity(id.get(), Accessor.class);
253 } catch (ItemNotFound e) {
254 throw new MissingOrInvalidUser(id.get());
255 }
256 }
257 }
258
259
260
261
262
263
264 protected Api api() {
265 return ApiFactory.withLogging(graph, getRequesterUserProfile());
266 }
267
268
269
270
271
272
273 protected Api anonymousApi() {
274 return ApiFactory.noLogging(graph, AnonymousAccessor.getInstance());
275 }
276
277
278
279
280
281
282
283 protected UserProfile getCurrentUser() {
284 Accessor profile = getRequesterUserProfile();
285 if (profile.isAdmin() || profile.isAnonymous()
286 || !profile.getType().equals(Entities.USER_PROFILE)) {
287 throw new MissingOrInvalidUser(profile.getId());
288 }
289 return profile.as(UserProfile.class);
290 }
291
292
293
294
295
296
297
298 protected Actioner getCurrentActioner() {
299 return getRequesterUserProfile().as(Actioner.class);
300 }
301
302
303
304
305
306
307 protected Optional<String> getLogMessage() {
308 List<String> list = requestHeaders.getRequestHeader(LOG_MESSAGE_HEADER_NAME);
309 if (list != null && !list.isEmpty()) {
310 try {
311 return Optional.of(URLDecoder.decode(list.get(0), StandardCharsets.UTF_8.name()));
312 } catch (UnsupportedEncodingException e) {
313 logger.error("Unsupported encoding in header: {}", e);
314 return Optional.empty();
315 }
316 }
317 return Optional.empty();
318 }
319
320
321
322
323
324
325
326
327 protected Boolean isPatch() {
328 List<String> list = requestHeaders.getRequestHeader(PATCH_HEADER_NAME);
329 if (list != null && !list.isEmpty()) {
330 return Boolean.valueOf(list.get(0));
331 }
332 return false;
333 }
334
335
336
337
338
339
340
341
342 protected boolean isStreaming() {
343 List<String> list = requestHeaders.getRequestHeader(STREAM_HEADER_NAME);
344 if (list != null && !list.isEmpty()) {
345 return Boolean.valueOf(list.get(0));
346 }
347 return false;
348 }
349
350
351
352
353
354
355 private Optional<String> getRequesterIdentifier() {
356 List<String> list = requestHeaders.getRequestHeader(AUTH_HEADER_NAME);
357 if (list != null && !list.isEmpty()) {
358 return Optional.ofNullable(list.get(0));
359 }
360 return Optional.empty();
361 }
362
363
364
365
366
367
368
369
370
371 protected <T extends Entity> Response single(T item) {
372 try {
373 return Response.status(Response.Status.OK)
374 .entity(getSerializer().entityToJson(item).getBytes(Charsets.UTF_8))
375 .location(getItemUri(item))
376 .cacheControl(getCacheControl(item)).build();
377 } catch (SerializationError e) {
378 throw new RuntimeException(e);
379 }
380 }
381
382
383
384
385
386
387
388 protected <T extends Entity> Response streamingPage(Supplier<QueryApi.Page<T>> page) {
389 return streamingPage(page, getSerializer());
390 }
391
392
393
394
395
396
397
398
399
400 protected <T extends Entity> Response streamingPage(
401 final Supplier<QueryApi.Page<T>> page, final Serializer serializer) {
402 return streamingList(() -> page.get().getIterable(), serializer,
403 streamingResponseBuilder(page.get()));
404 }
405
406
407
408
409
410
411
412 protected Response streamingVertexList(Supplier<Iterable<Vertex>> vertices) {
413 return streamingVertexList(vertices, getSerializer());
414 }
415
416
417
418
419
420
421
422
423 protected Response streamingVertexList(Supplier<Iterable<Vertex>> vertices, Serializer serializer) {
424 return streamingVertexList(vertices, serializer, Response.ok());
425 }
426
427
428
429
430
431
432
433 protected <T extends Entity> Response streamingList(Supplier<Iterable<T>> list) {
434 return streamingList(list, getSerializer());
435 }
436
437
438
439
440
441
442
443 protected <T extends Entity> Response streamingListOfLists(Supplier<Iterable<? extends Collection<T>>> lists) {
444 return streamingGroup(lists, getSerializer(), Response.ok());
445 }
446
447
448
449
450
451
452
453
454 protected <T extends Entity> Response streamingList(Supplier<Iterable<T>> list, Serializer serializer) {
455 return streamingList(list, serializer, Response.ok());
456 }
457
458
459
460
461
462
463
464 protected URI getItemUri(Entity item) {
465 return uriInfo.getBaseUriBuilder()
466 .path(RESOURCE_ENDPOINT_PREFIX)
467 .path(item.getType())
468 .path(item.getId()).build();
469 }
470
471
472
473
474
475
476
477 protected Response creationResponse(Entity frame) {
478 try {
479 return Response.status(Response.Status.CREATED).location(getItemUri(frame))
480 .entity(getSerializer().entityToJson(frame))
481 .build();
482 } catch (SerializationError serializationError) {
483 throw new RuntimeException(serializationError);
484 }
485 }
486
487
488
489
490
491
492
493
494
495 protected <T extends Entity> CacheControl getCacheControl(T item) {
496 CacheControl cc = new CacheControl();
497 if (!(item instanceof Accessible)
498 || !(((Accessible) item).hasAccessRestriction())) {
499 cc.setMaxAge(ITEM_CACHE_TIME);
500 } else {
501 cc.setNoStore(true);
502 cc.setNoCache(true);
503 }
504 return cc;
505 }
506
507
508
509
510
511
512
513
514 protected String getRdfFormat(String format, String defaultFormat) {
515 if (format == null) {
516 for (String mimeValue : RDF_MIMETYPE_FORMATS.keySet()) {
517 MediaType mime = MediaType.valueOf(mimeValue);
518 if (requestHeaders.getAcceptableMediaTypes().contains(mime)) {
519 return RDF_MIMETYPE_FORMATS.get(mimeValue);
520 }
521 }
522 return defaultFormat;
523 } else {
524 return RDF_MIMETYPE_FORMATS.containsValue(format) ? format : defaultFormat;
525 }
526 }
527
528 private <T> Response.ResponseBuilder streamingResponseBuilder(QueryApi.Page<T> page) {
529 Response.ResponseBuilder builder = Response.ok();
530 for (Map.Entry<String, Object> entry : getHeaders(page).entrySet()) {
531 builder = builder.header(entry.getKey(), entry.getValue());
532 }
533 return builder;
534 }
535
536 private Map<String, Object> getHeaders(QueryApi.Page<?> page) {
537 return ImmutableMap.<String, Object>of(
538 RANGE_HEADER_NAME,
539 String.format("offset=%d; limit=%d; total=%d",
540 page.getOffset(), page.getLimit(), page.getTotal()));
541 }
542
543 private Response streamingVertexList(
544 Supplier<Iterable<Vertex>> page, Serializer serializer, Response.ResponseBuilder responseBuilder) {
545 return responseBuilder.entity((StreamingOutput) outputStream -> {
546 final Serializer cacheSerializer = serializer.withCache();
547 try (Tx tx = beginTx();
548 JsonGenerator g = jsonFactory.createGenerator(outputStream)) {
549 g.writeStartArray();
550 for (Vertex item : page.get()) {
551 g.writeRaw('\n');
552 jsonMapper.writeValue(g, item == null ? null : cacheSerializer.vertexToData(item));
553 }
554 g.writeEndArray();
555 tx.success();
556 } catch (SerializationError e) {
557 throw new RuntimeException(e);
558 }
559 }).type(MediaType.APPLICATION_JSON_TYPE).build();
560 }
561
562 private <T extends Entity> Response streamingList(
563 Supplier<Iterable<T>> page, Serializer serializer, Response.ResponseBuilder responseBuilder) {
564 return responseBuilder.entity((StreamingOutput) outputStream -> {
565 final Serializer cacheSerializer = serializer.withCache();
566 try (Tx tx = beginTx();
567 JsonGenerator g = jsonFactory.createGenerator(outputStream)) {
568 g.writeStartArray();
569 for (T item : page.get()) {
570 g.writeRaw('\n');
571 jsonMapper.writeValue(g, item == null ? null : cacheSerializer.entityToData(item));
572 }
573 g.writeEndArray();
574 tx.success();
575 } catch (SerializationError e) {
576 throw new RuntimeException(e);
577 }
578 }).type(MediaType.APPLICATION_JSON_TYPE).build();
579 }
580
581 private <T extends Entity> Response streamingGroup(
582 Supplier<Iterable<? extends Collection<T>>> groups, Serializer serializer, Response.ResponseBuilder responseBuilder) {
583 return responseBuilder.entity((StreamingOutput) outputStream -> {
584 final Serializer cacheSerializer = serializer.withCache();
585 try (Tx tx = beginTx();
586 JsonGenerator g = jsonFactory.createGenerator(outputStream)) {
587 g.writeStartArray();
588 for (Collection<T> collect : groups.get()) {
589 g.writeStartArray();
590 for (T item : collect) {
591 jsonMapper.writeValue(g, item == null ? null : cacheSerializer.entityToData(item));
592 }
593 g.writeEndArray();
594 g.writeRaw('\n');
595 }
596 g.writeEndArray();
597
598 tx.success();
599 } catch (SerializationError e) {
600 throw new RuntimeException(e);
601 }
602 }).type(MediaType.APPLICATION_JSON_TYPE).build();
603 }
604 }