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.base;
21  
22  import com.google.common.collect.Lists;
23  import com.google.common.collect.Sets;
24  import eu.ehri.project.acl.AclManager;
25  import eu.ehri.project.api.Api;
26  import eu.ehri.project.api.EventsApi;
27  import eu.ehri.project.core.Tx;
28  import eu.ehri.project.definitions.EventTypes;
29  import eu.ehri.project.exceptions.DeserializationError;
30  import eu.ehri.project.exceptions.ItemNotFound;
31  import eu.ehri.project.exceptions.PermissionDenied;
32  import eu.ehri.project.exceptions.SerializationError;
33  import eu.ehri.project.exceptions.ValidationError;
34  import eu.ehri.project.exporters.xml.XmlExporter;
35  import eu.ehri.project.models.EntityClass;
36  import eu.ehri.project.models.base.Accessible;
37  import eu.ehri.project.models.base.Accessor;
38  import eu.ehri.project.models.base.Entity;
39  import eu.ehri.project.persistence.ActionManager;
40  import eu.ehri.project.persistence.Bundle;
41  import eu.ehri.project.persistence.Mutation;
42  import eu.ehri.project.persistence.Serializer;
43  import org.joda.time.DateTime;
44  import org.neo4j.graphdb.GraphDatabaseService;
45  
46  import javax.ws.rs.WebApplicationException;
47  import javax.ws.rs.core.Context;
48  import javax.ws.rs.core.Response;
49  import javax.ws.rs.core.StreamingOutput;
50  import javax.xml.transform.TransformerException;
51  import java.io.IOException;
52  import java.util.List;
53  import java.util.Set;
54  import java.util.zip.ZipEntry;
55  import java.util.zip.ZipOutputStream;
56  
57  
58  /**
59   * Handle CRUD operations on Accessible's by using the
60   * eu.ehri.project.views.Views class generic code. Resources for specific
61   * entities can extend this class.
62   *
63   * @param <E> the specific Accessible derived class
64   */
65  public class AbstractAccessibleResource<E extends Accessible> extends AbstractResource {
66  
67      public final static String ITEM_TYPE_PARAM = "type";
68      public final static String ITEM_ID_PARAM = "item";
69      public final static String EVENT_TYPE_PARAM = "et";
70      public final static String USER_PARAM = "user";
71      public final static String FROM_PARAM = "from";
72      public final static String TO_PARAM = "to";
73      public final static String SHOW_PARAM = "show"; // watched, follows
74      public final static String AGGREGATION_PARAM = "aggregation";
75  
76      protected final AclManager aclManager;
77      protected final ActionManager actionManager;
78      protected final Class<E> cls;
79  
80      /**
81       * Functor used to post-process items.
82       */
83      public interface Handler<E extends Accessible> {
84          void process(E frame) throws PermissionDenied;
85      }
86  
87      /**
88       * Implementation of a Handler that does nothing.
89       *
90       * @param <E> the specific Accessible derived class
91       */
92      public static class NoOpHandler<E extends Accessible> implements Handler<E> {
93          @Override
94          public void process(E frame) {
95          }
96      }
97  
98      private final Handler<E> noOpHandler = new NoOpHandler<>();
99  
100     /**
101      * Constructor
102      *
103      * @param database the injected neo4j database
104      * @param cls      the entity Java class
105      */
106     public AbstractAccessibleResource(
107             @Context GraphDatabaseService database, Class<E> cls) {
108         super(database);
109         this.cls = cls;
110         aclManager = new AclManager(graph);
111         actionManager = new ActionManager(graph);
112     }
113 
114     /**
115      * List all instances of the 'entity' accessible to the given user.
116      *
117      * @return a list of entities
118      */
119     public Response listItems() {
120         try (final Tx tx = beginTx()) {
121             Response response = streamingPage(() -> getQuery().page(cls));
122             tx.success();
123             return response;
124         }
125     }
126 
127     /**
128      * Create an instance of the 'entity' in the database
129      *
130      * @param entityBundle a bundle of item data
131      *                     'id' fields)
132      * @param accessorIds  a list of accessors who can initially view this item
133      * @param handler      a callback function that allows additional operations
134      *                     to be run on the created object after it is initialised
135      *                     but before the response is generated. This is useful for adding
136      *                     relationships to the new item.
137      * @param scopedApi    the Api instance to use to create the item. This allows callers
138      *                     to override the scope used.
139      * @param otherCls     the class of the created item.
140      * @param <T>          the generic type of class T
141      * @return the response of the create request, the 'location' will contain
142      * the url of the newly created instance.
143      */
144     public <T extends Accessible> Response createItem(
145             Bundle entityBundle,
146             List<String> accessorIds,
147             Handler<T> handler,
148             Api scopedApi,
149             Class<T> otherCls)
150             throws PermissionDenied, ValidationError, DeserializationError {
151         Accessor user = getRequesterUserProfile();
152         T entity = scopedApi.create(entityBundle, otherCls, getLogMessage());
153         if (!accessorIds.isEmpty()) {
154             api().acl().setAccessors(entity, getAccessors(accessorIds, user));
155         }
156 
157         // run post-creation callbacks
158         handler.process(entity);
159         return creationResponse(entity);
160     }
161 
162     /**
163      * Create an instance of the 'entity' in the database
164      *
165      * @param entityBundle a bundle of item data
166      *                     'id' fields)
167      * @param accessorIds  a list of accessors who can initially view this item
168      * @param handler      a callback function that allows additional operations
169      *                     to be run on the created object after it is initialised
170      *                     but before the response is generated. This is useful for adding
171      *                     relationships to the new item.
172      * @return the response of the create request, the 'location' will contain
173      * the url of the newly created instance.
174      */
175     public Response createItem(Bundle entityBundle, List<String> accessorIds, Handler<E> handler)
176             throws PermissionDenied, ValidationError, DeserializationError {
177         return createItem(entityBundle, accessorIds, handler, api(), cls);
178     }
179 
180     public Response createItem(Bundle entityBundle, List<String> accessorIds)
181             throws PermissionDenied, ValidationError, DeserializationError {
182         return createItem(entityBundle, accessorIds, noOpHandler);
183     }
184 
185     /**
186      * Retieve (get) an instance of the 'entity' in the database
187      *
188      * @param id the Entities identifier string
189      * @return the response of the request, which contains the json
190      * representation
191      */
192     public Response getItem(String id) throws ItemNotFound {
193         logger.debug("Fetched item: {}", id);
194         try (final Tx tx = beginTx()) {
195             E entity = api().detail(id, cls);
196             if (!manager.getEntityClass(entity).getJavaClass().equals(cls)) {
197                 throw new ItemNotFound(id);
198             }
199             Response response = single(entity);
200             tx.success();
201             return response;
202         }
203     }
204 
205     /**
206      * Update (change) an instance of the 'entity' in the database.
207      *
208      * @param entityBundle the bundle
209      * @return the response of the update request
210      */
211     public Response updateItem(Bundle entityBundle)
212             throws PermissionDenied, ValidationError, ItemNotFound, DeserializationError {
213         Mutation<E> update = api().update(entityBundle, cls, getLogMessage());
214         return single(update.getNode());
215     }
216 
217     /**
218      * Update (change) an instance of the 'entity' in the database
219      * <p>
220      * If the Patch header is true top-level bundle data will be merged
221      * instead of overwritten.
222      *
223      * @param id        the item's ID
224      * @param rawBundle the bundle
225      * @return the response of the update request
226      */
227     public Response updateItem(String id, Bundle rawBundle)
228             throws PermissionDenied, ValidationError,
229             DeserializationError, ItemNotFound {
230         try {
231             E entity = api().detail(id, cls);
232             if (isPatch()) {
233                 Serializer depSerializer = new Serializer.Builder(graph).dependentOnly().build();
234                 Bundle existing = depSerializer.entityToBundle(entity);
235                 return updateItem(existing.mergeDataWith(rawBundle));
236             } else {
237                 return updateItem(rawBundle.withId(entity.getId()));
238             }
239         } catch (SerializationError serializationError) {
240             throw new RuntimeException(serializationError);
241         }
242     }
243 
244     /**
245      * Delete (remove) an instance of the 'entity' in the database,
246      * running a handler callback beforehand.
247      *
248      * @param id         the item's ID
249      * @param preProcess a handler to run before deleting the item
250      */
251     protected void deleteItem(String id, Handler<E> preProcess)
252             throws PermissionDenied, ItemNotFound, ValidationError {
253         try {
254             Api api = api();
255             preProcess.process(api.detail(id, cls));
256             api.delete(id, getLogMessage());
257         } catch (SerializationError serializationError) {
258             throw new RuntimeException(serializationError);
259         }
260     }
261 
262     /**
263      * Delete (remove) an instance of the 'entity' in the database
264      *
265      * @param id the item's ID
266      */
267     protected void deleteItem(String id)
268             throws PermissionDenied, ItemNotFound, ValidationError {
269         deleteItem(id, noOpHandler);
270     }
271 
272     // Helpers
273 
274     protected <T extends Entity> Response exportItemsAsZip(XmlExporter<T> exporter, Iterable<T> items, String lang)
275             throws IOException {
276         return Response.ok((StreamingOutput) outputStream -> {
277             try (final Tx tx = beginTx();
278                  ZipOutputStream zos = new ZipOutputStream(outputStream)) {
279                 for (T item : items) {
280                     ZipEntry zipEntry = new ZipEntry(item.getId() + ".xml");
281                     zipEntry.setComment("Exported from the EHRI portal at " + (DateTime.now()));
282                     zos.putNextEntry(zipEntry);
283                     exporter.export(item, zos, lang);
284                     zos.closeEntry();
285                 }
286                 tx.success();
287             } catch (TransformerException e) {
288                 throw new WebApplicationException(e);
289             }
290         }).type("application/zip").build();
291     }
292 
293     /**
294      * Get an event query builder object.
295      *
296      * @return a new event query builder
297      */
298     protected EventsApi getEventsApi() {
299         List<EventTypes> eventTypes = Lists.transform(getStringListQueryParam(EVENT_TYPE_PARAM),
300                 EventTypes::valueOf);
301         List<EntityClass> entityClasses = Lists.transform(getStringListQueryParam(ITEM_TYPE_PARAM),
302                 EntityClass::withName);
303         List<EventsApi.ShowType> showTypes = Lists.transform(getStringListQueryParam(SHOW_PARAM),
304                 EventsApi.ShowType::valueOf);
305         List<String> fromStrings = getStringListQueryParam(FROM_PARAM);
306         List<String> toStrings = getStringListQueryParam(TO_PARAM);
307         List<String> users = getStringListQueryParam(USER_PARAM);
308         List<String> ids = getStringListQueryParam(ITEM_ID_PARAM);
309         return api()
310                 .events()
311                 .withRange(getIntQueryParam(OFFSET_PARAM, 0),
312                         getIntQueryParam(LIMIT_PARAM, DEFAULT_LIST_LIMIT))
313                 .withEventTypes(eventTypes.toArray(new EventTypes[eventTypes.size()]))
314                 .withEntityClasses(entityClasses.toArray(new EntityClass[entityClasses.size()]))
315                 .from(fromStrings.isEmpty() ? null : fromStrings.get(0))
316                 .to(toStrings.isEmpty() ? null : toStrings.get(0))
317                 .withUsers(users.toArray(new String[users.size()]))
318                 .withIds(ids.toArray(new String[ids.size()]))
319                 .withShowType(showTypes.toArray(new EventsApi.ShowType[showTypes.size()]));
320     }
321 
322     /**
323      * Get a set of accessor frames given a list of names.
324      *
325      * @param accessorIds a list of accessor IDs
326      * @param current     the current accessor
327      * @return a set a accessors
328      */
329     protected Set<Accessor> getAccessors(List<String> accessorIds, Accessor current) {
330 
331         Set<Accessor> accessors = Sets.newHashSet();
332         for (String id : accessorIds) {
333             try {
334                 Accessor av = manager.getEntity(id, Accessor.class);
335                 accessors.add(av);
336             } catch (ItemNotFound e) {
337                 logger.warn("Invalid accessor given: {}", id);
338             }
339         }
340         // The current user should always be among the accessors, so add
341         // them unless the list is empty.
342         if (!accessors.isEmpty()) {
343             accessors.add(current);
344         }
345         return accessors;
346     }
347 }