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 }