View Javadoc

1   /*
2    * Copyright 2015 Data Archiving and Networked Services (an institute of
3    * Koninklijke Nederlandse Akademie van Wetenschappen), King's College London,
4    * Georg-August-Universitaet Goettingen Stiftung Oeffentlichen Rechts
5    *
6    * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
7    * the European Commission - subsequent versions of the EUPL (the "Licence");
8    * You may not use this work except in compliance with the Licence.
9    * You may obtain a copy of the Licence at:
10   *
11   * https://joinup.ec.europa.eu/software/page/eupl
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the Licence is distributed on an "AS IS" basis,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the Licence for the specific language governing
17   * permissions and limitations under the Licence.
18   */
19  
20  package eu.ehri.extension;
21  
22  import com.fasterxml.jackson.core.type.TypeReference;
23  import com.fasterxml.jackson.databind.JsonMappingException;
24  import com.google.common.base.Charsets;
25  import com.google.common.collect.Iterables;
26  import com.google.common.collect.Lists;
27  import com.tinkerpop.blueprints.Vertex;
28  import com.tinkerpop.pipes.PipeFunction;
29  import eu.ehri.extension.base.AbstractAccessibleResource;
30  import eu.ehri.project.acl.AclManager;
31  import eu.ehri.project.api.EventsApi;
32  import eu.ehri.project.api.impl.ApiImpl;
33  import eu.ehri.project.core.Tx;
34  import eu.ehri.project.exceptions.AccessDenied;
35  import eu.ehri.project.exceptions.DeserializationError;
36  import eu.ehri.project.exceptions.ItemNotFound;
37  import eu.ehri.project.exceptions.PermissionDenied;
38  import eu.ehri.project.exceptions.SerializationError;
39  import eu.ehri.project.exceptions.ValidationError;
40  import eu.ehri.project.exporters.dc.DublinCore11Exporter;
41  import eu.ehri.project.exporters.dc.DublinCoreExporter;
42  import eu.ehri.project.models.AccessPoint;
43  import eu.ehri.project.models.Annotation;
44  import eu.ehri.project.models.Link;
45  import eu.ehri.project.models.PermissionGrant;
46  import eu.ehri.project.models.base.Accessible;
47  import eu.ehri.project.models.base.Accessor;
48  import eu.ehri.project.models.base.Annotatable;
49  import eu.ehri.project.models.base.Described;
50  import eu.ehri.project.models.base.Description;
51  import eu.ehri.project.models.base.Entity;
52  import eu.ehri.project.models.base.Linkable;
53  import eu.ehri.project.models.base.PermissionGrantTarget;
54  import eu.ehri.project.models.base.PermissionScope;
55  import eu.ehri.project.models.base.Versioned;
56  import eu.ehri.project.models.events.Version;
57  import eu.ehri.project.persistence.Bundle;
58  import eu.ehri.project.persistence.Mutation;
59  import org.neo4j.graphdb.GraphDatabaseService;
60  import org.w3c.dom.Document;
61  
62  import javax.ws.rs.Consumes;
63  import javax.ws.rs.DELETE;
64  import javax.ws.rs.DefaultValue;
65  import javax.ws.rs.GET;
66  import javax.ws.rs.POST;
67  import javax.ws.rs.PUT;
68  import javax.ws.rs.Path;
69  import javax.ws.rs.PathParam;
70  import javax.ws.rs.Produces;
71  import javax.ws.rs.QueryParam;
72  import javax.ws.rs.core.Context;
73  import javax.ws.rs.core.MediaType;
74  import javax.ws.rs.core.Response;
75  import java.io.IOException;
76  import java.util.List;
77  import java.util.Set;
78  import java.util.function.Function;
79  
80  /**
81   * Provides a means of fetching items and lists of items
82   * regardless of their specific type.
83   */
84  @Path(GenericResource.ENDPOINT)
85  public class GenericResource extends AbstractAccessibleResource<Accessible> {
86  
87      public static final String ENDPOINT = "entities";
88  
89      public GenericResource(@Context GraphDatabaseService database) {
90          super(database, Accessible.class);
91      }
92  
93      /**
94       * Fetch a list of items by their ID.
95       *
96       * Note: if <i>both</i> global IDs and graph IDs (GIDs) are given as query
97       * parameters, GIDs will be returned at the head of the response list.
98       *
99       * @param ids  a list of string IDs
100      * @param gids a list of graph number IDs
101      * @return A serialized list of items, with nulls for items that are not
102      * found or inaccessible.
103      */
104     @GET
105     @Produces(MediaType.APPLICATION_JSON)
106     public Response list(@QueryParam("id") List<String> ids, @QueryParam("gid") List<Long> gids) {
107         try (Tx tx = beginTx()) {
108             PipeFunction<Vertex, Boolean> aclFilter = AclManager
109                     .getAclFilterFunction(getRequesterUserProfile());
110             PipeFunction<Vertex, Boolean> contentFilter = aclManager
111                     .getContentTypeFilterFunction();
112             Function<Vertex, Vertex> filter = v ->
113                     (contentFilter.compute(v) && aclFilter.compute(v)) ? v : null;
114 
115             Iterable<Vertex> byGid = Iterables.transform(gids, graph::getVertex);
116             Iterable<Vertex> byId = manager.getVertices(ids);
117             Iterable<Vertex> all = Iterables.concat(byGid, byId);
118             Response response = streamingVertexList(() -> Iterables.transform(all, filter::apply));
119             tx.success();
120             return response;
121         }
122     }
123 
124     /**
125      * Fetch a list of items by their ID.
126      *
127      * Note: if <i>both</i> global IDs and graph IDs (GIDs) are given as query
128      * parameters, GIDs will be returned at the head of the response list.
129      *
130      * @param json a JSON-encoded list of IDs, which can consist of
131      *             either strings or (if a number) graph (long) ids.
132      * @return A serialized list of items, with nulls for items that are not
133      * found or inaccessible.
134      */
135     @POST
136     @Produces(MediaType.APPLICATION_JSON)
137     @Consumes(MediaType.APPLICATION_JSON)
138     public Response listFromJson(String json)
139             throws ItemNotFound, PermissionDenied, DeserializationError, IOException {
140         IdSet set = parseGraphIds(json);
141         return this.list(set.ids, set.gids);
142     }
143 
144     /**
145      * Fetch an item of any type by ID.
146      *
147      * @param id the item's ID
148      * @return A serialized representation.
149      */
150     @GET
151     @Produces(MediaType.APPLICATION_JSON)
152     @Path("{id:[^/]+}")
153     public Response get(@PathParam("id") String id) throws ItemNotFound, AccessDenied {
154         try (final Tx tx = beginTx()) {
155             Vertex item = manager.getVertex(id);
156 
157             // If the item doesn't exist or isn't a content type throw 404
158             Accessor currentUser = getRequesterUserProfile();
159             if (item == null || !aclManager.getContentTypeFilterFunction().compute(item)) {
160                 throw new ItemNotFound(id);
161             } else if (!AclManager.getAclFilterFunction(currentUser).compute(item)) {
162                 throw new AccessDenied(currentUser.getId(), id);
163             }
164 
165             Response response = single(graph.frame(item, Accessible.class));
166             tx.success();
167             return response;
168         }
169     }
170 
171     /**
172      * Get the accessors who are able to view an item.
173      *
174      * @param id the ID of the item
175      * @return a list of accessor frames
176      */
177     @GET
178     @Produces(MediaType.APPLICATION_JSON)
179     @Path("{id:[^/]+}/access")
180     public Response visibility(@PathParam("id") String id)
181             throws PermissionDenied, ItemNotFound, SerializationError {
182         try (final Tx tx = beginTx()) {
183             Accessible item = manager.getEntity(id, Accessible.class);
184             Iterable<Accessor> accessors = item.getAccessors();
185             Response response = streamingList(() -> accessors);
186             tx.success();
187             return response;
188         }
189     }
190 
191     /**
192      * Set the accessors who are able to view an item. If no accessors
193      * are set, the item is globally readable.
194      *
195      * @param id          the ID of the item
196      * @param accessorIds the IDs of the users who can access this item.
197      * @return the updated object
198      */
199     @POST
200     @Produces(MediaType.APPLICATION_JSON)
201     @Path("{id:[^/]+}/access")
202     public Response setVisibility(@PathParam("id") String id,
203             @QueryParam(ACCESSOR_PARAM) List<String> accessorIds)
204             throws PermissionDenied, ItemNotFound, SerializationError {
205         try (final Tx tx = beginTx()) {
206             Accessible item = api().detail(id, Accessible.class);
207             Accessor current = getRequesterUserProfile();
208             Set<Accessor> accessors = getAccessors(accessorIds, current);
209             api().acl().setAccessors(item, accessors);
210             Response response = single(item);
211             tx.success();
212             return response;
213         }
214     }
215 
216     /**
217      * Up vote an item.
218      *
219      * @param id the ID of the item to promote.
220      * @return 200 response
221      */
222     @POST
223     @Produces(MediaType.APPLICATION_JSON)
224     @Path("{id:[^/]+}/promote")
225     public Response addPromotion(@PathParam("id") String id)
226             throws PermissionDenied, ItemNotFound {
227         try (final Tx tx = beginTx()) {
228             Response item = single(api().promote(id));
229             tx.success();
230             return item;
231         } catch (ApiImpl.NotPromotableError e) {
232             return Response.status(Response.Status.BAD_REQUEST.getStatusCode())
233                     .entity(e.getMessage()).build();
234         }
235     }
236 
237     /**
238      * Remove an up vote.
239      *
240      * @param id the ID of the item to remove
241      * @return 200 response
242      */
243     @DELETE
244     @Produces(MediaType.APPLICATION_JSON)
245     @Path("{id:[^/]+}/promote")
246     public Response removePromotion(@PathParam("id") String id)
247             throws PermissionDenied, ItemNotFound, ValidationError {
248         try (final Tx tx = beginTx()) {
249             Response response = single(api().removePromotion(id));
250             tx.success();
251             return response;
252         }
253     }
254 
255     /**
256      * Down vote an item
257      *
258      * @param id the ID of the item to promote.
259      * @return 200 response
260      */
261     @POST
262     @Produces(MediaType.APPLICATION_JSON)
263     @Path("{id:[^/]+}/demote")
264     public Response addDemotion(@PathParam("id") String id)
265             throws PermissionDenied, ItemNotFound {
266         try (final Tx tx = beginTx()) {
267             Response item = single(api().demote(id));
268             tx.success();
269             return item;
270         } catch (ApiImpl.NotPromotableError e) {
271             return Response.status(Response.Status.BAD_REQUEST.getStatusCode())
272                     .entity(e.getMessage()).build();
273         }
274     }
275 
276     /**
277      * Remove a down vote.
278      *
279      * @param id the ID of the item to remove
280      * @return 200 response
281      */
282     @DELETE
283     @Produces(MediaType.APPLICATION_JSON)
284     @Path("{id:[^/]+}/demote")
285     public Response removeDemotion(@PathParam("id") String id)
286             throws PermissionDenied, ItemNotFound, ValidationError {
287         try (final Tx tx = beginTx()) {
288             Response item = single(api().removeDemotion(id));
289             tx.success();
290             return item;
291         }
292     }
293 
294     /**
295      * Lookup and page the history for a given item.
296      *
297      * @param id          the event id
298      * @param aggregation the aggregation strategy
299      * @return A list of events
300      */
301     @GET
302     @Produces(MediaType.APPLICATION_JSON)
303     @Path("{id:[^/]+}/events")
304     public Response events(
305             @PathParam("id") String id,
306             @QueryParam(AGGREGATION_PARAM) @DefaultValue("user") EventsApi.Aggregation aggregation)
307             throws ItemNotFound, AccessDenied {
308         try (final Tx tx = beginTx()) {
309             Accessible item = api().detail(id, Accessible.class);
310             EventsApi eventsApi = getEventsApi()
311                     .withAggregation(aggregation);
312             Response response = streamingListOfLists(() -> eventsApi.aggregateForItem(item));
313             tx.success();
314             return response;
315         }
316     }
317 
318     /**
319      * Return a (paged) list of annotations for the given item.
320      *
321      * @param id the item ID
322      * @return a list of annotations on the item
323      */
324     @GET
325     @Produces(MediaType.APPLICATION_JSON)
326     @Path("{id:[^/]+}/annotations")
327     public Response annotations(
328             @PathParam("id") String id) throws ItemNotFound {
329         try (final Tx tx = beginTx()) {
330             Annotatable entity = manager.getEntity(id, Annotatable.class);
331             Response response = streamingPage(() -> getQuery().page(
332                     entity.getAnnotations(),
333                     Annotation.class));
334             tx.success();
335             return response;
336         }
337     }
338 
339     /**
340      * Returns a list of items linked to the given description.
341      *
342      * @param id the item ID
343      * @return a list of links with the item as a target
344      */
345     @GET
346     @Produces(MediaType.APPLICATION_JSON)
347     @Path("{id:[^/]+}/links")
348     public Response links(@PathParam("id") String id) throws ItemNotFound {
349         try (final Tx tx = beginTx()) {
350             Linkable entity = manager.getEntity(id, Linkable.class);
351             Response response = streamingPage(() -> getQuery().page(
352                     entity.getLinks(),
353                     Link.class));
354             tx.success();
355             return response;
356         }
357     }
358 
359     /**
360      * List all the permission grants that relate specifically to this item.
361      *
362      * @return a list of grants for this item
363      */
364     @GET
365     @Produces(MediaType.APPLICATION_JSON)
366     @Path("{id:[^/]+}/permission-grants")
367     public Response permissionGrants(@PathParam("id") String id)
368             throws PermissionDenied, ItemNotFound {
369         try (final Tx tx = beginTx()) {
370             PermissionGrantTarget target = manager.getEntity(id, PermissionGrantTarget.class);
371             Response response = streamingPage(() -> getQuery()
372                     .page(target.getPermissionGrants(),
373                             PermissionGrant.class));
374             tx.success();
375             return response;
376         }
377     }
378 
379     /**
380      * List all the permission grants that relate specifically to this scope.
381      *
382      * @return a list of grants for which this item is the scope
383      */
384     @GET
385     @Produces(MediaType.APPLICATION_JSON)
386     @Path("{id:[^/]+}/scope-permission-grants")
387     public Response permissionGrantsAsScope(@PathParam("id") String id)
388             throws ItemNotFound {
389         try (final Tx tx = beginTx()) {
390             PermissionScope scope = manager.getEntity(id, PermissionScope.class);
391             Response response = streamingPage(() -> getQuery()
392                     .page(scope.getPermissionGrants(),
393                             PermissionGrant.class));
394             tx.success();
395             return response;
396         }
397     }
398 
399     /**
400      * Create a new description for this item.
401      *
402      * @param id     the item ID
403      * @param bundle the description bundle
404      * @return the new description
405      */
406     @POST
407     @Consumes(MediaType.APPLICATION_JSON)
408     @Produces(MediaType.APPLICATION_JSON)
409     @Path("{id:[^/]+}/descriptions")
410     public Response createDescription(@PathParam("id") String id, Bundle bundle)
411             throws PermissionDenied, ValidationError,
412             DeserializationError, ItemNotFound {
413         try (final Tx tx = beginTx()) {
414             Described item = api().detail(id, Described.class);
415             Description desc = api().createDependent(id, bundle,
416                     Description.class, getLogMessage());
417             item.addDescription(desc);
418             Response response = buildResponse(item, desc, Response.Status.CREATED);
419             tx.success();
420             return response;
421         } catch (SerializationError serializationError) {
422             throw new RuntimeException(serializationError);
423         }
424     }
425 
426     /**
427      * Update a description belonging to the given item.
428      *
429      * @param id     the item ID
430      * @param bundle the description bundle
431      * @return the new description
432      */
433     @PUT
434     @Consumes(MediaType.APPLICATION_JSON)
435     @Produces(MediaType.APPLICATION_JSON)
436     @Path("{id:[^/]+}/descriptions")
437     public Response updateDescription(@PathParam("id") String id, Bundle bundle)
438             throws PermissionDenied, ValidationError,
439             DeserializationError, ItemNotFound, SerializationError {
440         try (final Tx tx = beginTx()) {
441             Described item = api().detail(id, Described.class);
442             Mutation<Description> desc = api().updateDependent(id, bundle,
443                     Description.class, getLogMessage());
444             Response response = buildResponse(item, desc.getNode(), Response.Status.OK);
445             tx.success();
446             return response;
447         } catch (SerializationError serializationError) {
448             throw new RuntimeException(serializationError);
449         }
450     }
451 
452     /**
453      * Update a description with the given ID, belonging to the given parent item.
454      *
455      * @param id     the item ID
456      * @param did    the description ID
457      * @param bundle the description bundle
458      * @return the new description
459      */
460     @PUT
461     @Consumes(MediaType.APPLICATION_JSON)
462     @Produces(MediaType.APPLICATION_JSON)
463     @Path("{id:[^/]+}/descriptions/{did:[^/]+}")
464     public Response updateDescriptionWithId(@PathParam("id") String id,
465             @PathParam("did") String did, Bundle bundle)
466             throws AccessDenied, PermissionDenied, ValidationError,
467             DeserializationError, ItemNotFound, SerializationError {
468         return updateDescription(id, bundle.withId(did));
469     }
470 
471     /**
472      * Delete a description with the given ID, belonging to the given parent item.
473      *
474      * @param id  the item ID
475      * @param did the description ID
476      */
477     @DELETE
478     @Path("{id:[^/]+}/descriptions/{did:[^/]+}")
479     public void deleteDescription(
480             @PathParam("id") String id, @PathParam("did") String did)
481             throws PermissionDenied, ItemNotFound, ValidationError, SerializationError {
482         try (final Tx tx = beginTx()) {
483             api().deleteDependent(id, did, getLogMessage());
484             tx.success();
485         } catch (SerializationError serializationError) {
486             throw new RuntimeException(serializationError);
487         }
488     }
489 
490     /**
491      * Create an access point on the given description, for the given
492      * parent item.
493      *
494      * @param id     the parent item's ID
495      * @param did    the description's ID
496      * @param bundle the access point data
497      * @return the new access point
498      */
499     @POST
500     @Consumes(MediaType.APPLICATION_JSON)
501     @Produces(MediaType.APPLICATION_JSON)
502     @Path("{id:[^/]+}/descriptions/{did:[^/]+}/access-points")
503     public Response createAccessPoint(@PathParam("id") String id,
504             @PathParam("did") String did, Bundle bundle)
505             throws PermissionDenied, ValidationError, DeserializationError, ItemNotFound {
506         try (final Tx tx = beginTx()) {
507             Described item = api().detail(id, Described.class);
508             Description desc = api().detail(did, Description.class);
509             AccessPoint rel = api().createDependent(id, bundle,
510                     AccessPoint.class, getLogMessage());
511             desc.addAccessPoint(rel);
512             Response response = buildResponse(item, rel, Response.Status.CREATED);
513             tx.success();
514             return response;
515         } catch (SerializationError serializationError) {
516             throw new RuntimeException(serializationError);
517         }
518     }
519 
520 
521     /**
522      * Lookup and page the versions for a given item.
523      *
524      * @param id the event id
525      * @return a list of versions
526      */
527     @GET
528     @Path("{id:[^/]+}/versions")
529     public Response listVersions(@PathParam("id") String id) throws ItemNotFound, AccessDenied {
530         try (final Tx tx = beginTx()) {
531             Versioned item = api().detail(id, Versioned.class);
532             Response response = streamingPage(() -> getQuery().setStream(true)
533                     .page(item.getAllPriorVersions(), Version.class));
534             tx.success();
535             return response;
536         }
537     }
538 
539     /**
540      * Delete the access point with the given ID, on the given description,
541      * belonging to the given parent item.
542      *
543      * @param id   the parent item's ID
544      * @param did  the description's ID
545      * @param apid the access point's ID
546      */
547     @DELETE
548     @Path("{id:[^/]+}/descriptions/{did:[^/]+}/access-points/{apid:[^/]+}")
549     public void deleteAccessPoint(@PathParam("id") String id,
550             @PathParam("did") String did, @PathParam("apid") String apid)
551             throws AccessDenied, PermissionDenied, ValidationError,
552             DeserializationError, ItemNotFound {
553         try (final Tx tx = beginTx()) {
554             api().deleteDependent(id, apid, getLogMessage());
555             tx.success();
556         } catch (SerializationError serializationError) {
557             throw new RuntimeException(serializationError);
558         }
559     }
560 
561     @GET
562     @Path("{id:[^/]+}/dc")
563     @Produces(MediaType.TEXT_XML)
564     public Document exportDc(
565             @PathParam("id") String id,
566             @QueryParam("lang") String langCode)
567             throws AccessDenied, ItemNotFound, IOException {
568         try (final Tx tx = beginTx()) {
569             Described item = api().detail(id, Described.class);
570             DublinCoreExporter exporter = new DublinCore11Exporter(api());
571             Document doc = exporter.export(item, langCode);
572             tx.success();
573             return doc;
574         }
575     }
576 
577     // Helpers
578 
579     private Response buildResponse(Described item, Entity data, Response.Status status)
580             throws SerializationError {
581         return Response.status(status).location(getItemUri(item))
582                 .entity((getSerializer().entityToJson(data))
583                         .getBytes(Charsets.UTF_8)).build();
584     }
585 
586     private static class IdSet {
587         final List<String> ids;
588         final List<Long> gids;
589 
590         IdSet(List<String> ids, List<Long> gids) {
591             this.ids = ids;
592             this.gids = gids;
593         }
594     }
595 
596     private IdSet parseGraphIds(String json) throws IOException, DeserializationError {
597         try {
598             TypeReference<List<Object>> typeRef = new TypeReference<List<Object>>() {
599             };
600             List<Object> jsonValues = jsonMapper.readValue(json, typeRef);
601             List<String> ids = Lists.newArrayList();
602             List<Long> gids = Lists.newArrayList();
603 
604             for (Object js : jsonValues) {
605                 if (js instanceof Integer) {
606                     gids.add(Long.valueOf((Integer) js));
607                 } else if (js instanceof Long) {
608                     gids.add((Long) js);
609                 } else if (js instanceof String) {
610                     ids.add((String) js);
611                 }
612             }
613             return new IdSet(ids, gids);
614         } catch (JsonMappingException e) {
615             throw new DeserializationError(e.getMessage());
616         }
617     }
618 }