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.project.persistence;
21  
22  import com.google.common.collect.Lists;
23  import com.google.common.collect.Multimap;
24  import com.google.common.collect.Sets;
25  import com.tinkerpop.blueprints.Direction;
26  import com.tinkerpop.blueprints.Vertex;
27  import com.tinkerpop.frames.FramedGraph;
28  import eu.ehri.project.core.GraphManager;
29  import eu.ehri.project.core.GraphManagerFactory;
30  import eu.ehri.project.exceptions.IntegrityError;
31  import eu.ehri.project.exceptions.ItemNotFound;
32  import eu.ehri.project.exceptions.SerializationError;
33  import eu.ehri.project.exceptions.ValidationError;
34  import eu.ehri.project.models.base.Entity;
35  import eu.ehri.project.models.utils.ClassUtils;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  import java.util.Collection;
40  import java.util.HashSet;
41  import java.util.Map;
42  import java.util.Map.Entry;
43  import java.util.Set;
44  
45  
46  /**
47   * Class responsible for creating, updating and deleting Bundles.
48   */
49  public final class BundleManager {
50  
51      private static final Logger logger = LoggerFactory.getLogger(BundleManager.class);
52  
53      private final FramedGraph<?> graph;
54      private final GraphManager manager;
55      private final Serializer serializer;
56      private final BundleValidator validator;
57  
58      /**
59       * Constructor with a given scope.
60       *
61       * @param graph The graph
62       * @param scopeIds The ID set for the current scope.
63       */
64      public BundleManager(FramedGraph<?> graph, Collection<String> scopeIds) {
65          this.graph = graph;
66          manager = GraphManagerFactory.getInstance(graph);
67          serializer = new Serializer.Builder(graph).dependentOnly().build();
68          validator = new BundleValidator(manager, scopeIds);
69      }
70  
71      /**
72       * Constructor with system scope.
73       *
74       * @param graph The graph
75       */
76      public BundleManager(FramedGraph<?> graph) {
77          this(graph, Lists.<String>newArrayList());
78      }
79  
80      /**
81       * Entry-point for updating a bundle.
82       *
83       * @param bundle The bundle to create or update
84       * @param cls The frame class of the return type
85       * @return A framed vertex mutation
86       */
87      public <T extends Entity> Mutation<T> update(Bundle bundle, Class<T> cls)
88              throws ValidationError, ItemNotFound {
89          Bundle bundleWithIds = validator.validateForUpdate(bundle);
90          Mutation<Vertex> mutation = updateInner(bundleWithIds);
91          return new Mutation<>(graph.frame(mutation.getNode(), cls),
92                  mutation.getState(), mutation.getPrior());
93      }
94  
95      /**
96       * Entry-point for creating a bundle.
97       *
98       * @param bundle The bundle to create or update
99       * @param cls The frame class of the return type
100      * @return A framed vertex
101      */
102     public <T extends Entity> T create(Bundle bundle, Class<T> cls)
103             throws ValidationError {
104         Bundle bundleWithIds = validator.validateForCreate(bundle);
105         return graph.frame(createInner(bundleWithIds), cls);
106     }
107 
108     /**
109      * Entry point for creating or updating a bundle, depending on whether it has a supplied id.
110      *
111      * @param bundle The bundle to create or update
112      * @param cls The frame class of the return type
113      * @return A frame mutation
114      */
115     public <T extends Entity> Mutation<T> createOrUpdate(Bundle bundle, Class<T> cls)
116             throws ValidationError {
117         Bundle bundleWithIds = validator.validateForUpdate(bundle);
118         Mutation<Vertex> vertexMutation = createOrUpdateInner(bundleWithIds);
119         return new Mutation<>(graph.frame(vertexMutation.getNode(), cls), vertexMutation.getState(),
120                 vertexMutation.getPrior());
121     }
122 
123     /**
124      * Delete a bundle and dependent items, returning the total number of vertices deleted.
125      *
126      * @param bundle The bundle to delete
127      * @return The number of vertices deleted
128      */
129     public int delete(Bundle bundle) {
130         try {
131             return deleteCount(bundle, 0);
132         } catch (Exception e) {
133             throw new RuntimeException(e);
134         }
135     }
136 
137     public BundleManager withScopeIds(Collection<String> scopeIds) {
138         return new BundleManager(graph, scopeIds);
139     }
140 
141     // Helpers
142     private int deleteCount(Bundle bundle, int count) throws Exception {
143         Integer c = count;
144 
145         for (Bundle child : bundle.getDependentRelations().values()) {
146             c = deleteCount(child, c);
147         }
148         manager.deleteVertex(bundle.getId());
149         return c + 1;
150     }
151 
152     /**
153      * Insert or update an item depending on a) whether it has an ID, and b) whether it has an ID and already exists. If
154      * import mode is not enabled an error will be thrown.
155      *
156      * @param bundle The bundle to create or update
157      * @return A vertex mutation
158      * @throws RuntimeException when an item is said to exist, but could not be found
159      */
160     private Mutation<Vertex> createOrUpdateInner(Bundle bundle) {
161         try {
162             if (manager.exists(bundle.getId())) {
163                 return updateInner(bundle);
164             } else {
165                 return new Mutation<>(createInner(bundle), MutationState.CREATED);
166             }
167         } catch (ItemNotFound e) {
168             throw new RuntimeException(
169                     "Create or update failed because ItemNotFound was thrown even though exists() was true",
170                     e);
171         }
172     }
173     
174     /**
175      * Insert a bundle and save its dependent items.
176      *
177      * @param bundle a Bundle to insert in the graph
178      * @return the Vertex that was created from this Bundle
179      * @throws RuntimeException when an ID generation error was not handled by the IdGenerator class
180      */
181     private Vertex createInner(Bundle bundle) {
182         try {
183             Vertex node = manager.createVertex(bundle.getId(), bundle.getType(),
184                     bundle.getData());
185             createDependents(node, bundle.getBundleJavaClass(), bundle.getRelations());
186             return node;
187         } catch (IntegrityError e) {
188             // Mmmn, if we get here, it means that there's been an ID generation error
189             // which was not handled by an exception.. so throw a runtime error...
190             throw new RuntimeException(
191                     "Unexpected state: ID generation error not handled by IdGenerator class " + e.getMessage());
192         }
193     }
194 
195     /**
196      * Update a bundle and save its dependent items.
197      *
198      * @param bundle The bundle to update
199      * @return A vertex mutation
200      */
201     private Mutation<Vertex> updateInner(Bundle bundle) throws ItemNotFound {
202         Vertex node = manager.getVertex(bundle.getId());
203         try {
204             Bundle currentBundle = serializer.vertexToBundle(node);
205             Bundle newBundle = bundle.dependentsOnly();
206             if (!currentBundle.equals(newBundle)) {
207                 if (logger.isTraceEnabled()) {
208                     logger.trace("Bundles differ: {}", bundle.getId());
209                     logger.trace(currentBundle.diff(newBundle));
210                 }
211                 node = manager.updateVertex(bundle.getId(), bundle.getType(),
212                         bundle.getData());
213                 updateDependents(node, bundle.getBundleJavaClass(), bundle.getRelations());
214                 return new Mutation<>(node, MutationState.UPDATED, currentBundle);
215             } else {
216                 logger.debug("Not updating equivalent bundle: {}", bundle.getId());
217                 return new Mutation<>(node, MutationState.UNCHANGED);
218             }
219         } catch (SerializationError serializationError) {
220             throw new RuntimeException("Unexpected serialization error " +
221                     "checking bundle for equivalency", serializationError);
222         }
223     }
224 
225     /**
226      * Saves the dependent relations within a given bundle. Relations that are not dependent are ignored.
227      *
228      * @param master The master vertex
229      * @param cls The master vertex class
230      * @param relations A map of relations
231      */
232     private void createDependents(Vertex master,
233             Class<?> cls, Multimap<String, Bundle> relations) {
234         Map<String, Direction> dependents = ClassUtils
235                 .getDependentRelations(cls);
236         for (String relation : relations.keySet()) {
237             if (dependents.containsKey(relation)) {
238                 for (Bundle bundle : relations.get(relation)) {
239                     Vertex child = createInner(bundle);
240                     createChildRelationship(master, child, relation,
241                             dependents.get(relation));
242                 }
243             } else {
244                 logger.error("Nested data being ignored on creation because it is not a dependent relation: {}: {}", relation, relations.get(relation));
245             }
246         }
247     }
248 
249     /**
250      * Saves the dependent relations within a given bundle. Relations that are not dependent are ignored.
251      *
252      * @param master The master vertex
253      * @param cls The master vertex class
254      * @param relations A map of relations
255      */
256     private void updateDependents(Vertex master, Class<?> cls, Multimap<String, Bundle> relations) {
257 
258         // Get a list of dependent relationships for this class, and their
259         // directions.
260         Map<String, Direction> dependents = ClassUtils
261                 .getDependentRelations(cls);
262         // Build a list of the IDs of existing dependents we're going to be
263         // updating.
264         Set<String> updating = getUpdateSet(relations);
265         // Any that we're not going to update can have their subtrees deleted.
266         deleteMissingFromUpdateSet(master, dependents, updating);
267 
268         // Now go throw and create or update the new subtrees.
269         for (String relation : relations.keySet()) {
270             if (dependents.containsKey(relation)) {
271                 Direction direction = dependents.get(relation);
272 
273                 // FIXME: Assuming all dependents have the same direction of
274                 // relationship. This is *should* be safe, but could easily
275                 // break if the model ontology is altered without this
276                 // assumption in mind.
277                 Set<Vertex> currentRels = getCurrentRelationships(master,
278                         relation, direction);
279 
280                 for (Bundle bundle : relations.get(relation)) {
281                     Vertex child = createOrUpdateInner(bundle).getNode();
282                     // Create a relation if there isn't one already
283                     if (!currentRels.contains(child)) {
284                         createChildRelationship(master, child, relation,
285                                 direction);
286                     }
287                 }
288             } else {
289                 logger.warn("Nested data being ignored on update because " +
290                         "it is not a dependent relation: {}: {}",
291                         relation, relations.get(relation));
292             }
293         }
294     }
295 
296     private Set<String> getUpdateSet(Multimap<String, Bundle> relations) {
297         Set<String> updating = new HashSet<>();
298         for (String relation : relations.keySet()) {
299             for (Bundle child : relations.get(relation)) {
300                 updating.add(child.getId());
301             }
302         }
303         return updating;
304     }
305 
306     private void deleteMissingFromUpdateSet(Vertex master,
307             Map<String, Direction> dependents, Set<String> updating) {
308         for (Entry<String, Direction> relEntry : dependents.entrySet()) {
309             for (Vertex v : getCurrentRelationships(master,
310                     relEntry.getKey(), relEntry.getValue())) {
311                 if (!updating.contains(manager.getId(v))) {
312                     try {
313                         delete(serializer.entityToBundle(graph.frame(v,
314                                 manager.getEntityClass(v).getJavaClass())));
315                     } catch (SerializationError e) {
316                         throw new RuntimeException(e);
317                     }
318                 }
319             }
320         }
321     }
322 
323     /**
324      * Get the nodes that terminate a given relationship from a particular source node.
325      *
326      *
327      * @param src The source vertex
328      * @param label The relationship label
329      * @param direction The relationship direction
330      * @return A set of related nodes
331      */
332     private Set<Vertex> getCurrentRelationships(Vertex src,
333             String label, Direction direction) {
334         HashSet<Vertex> out = Sets.newHashSet();
335         for (Vertex end : src.getVertices(direction, label)) {
336             out.add(end);
337         }
338         return out;
339     }
340 
341     /**
342      * Create a relationship between a parent and child vertex.
343      *
344      * @param master The master vertex
345      * @param child The child vertex
346      * @param label The relationship label
347      * @param direction The direction of the relationship
348      */
349     private void createChildRelationship(Vertex master, Vertex child,
350             String label, Direction direction) {
351         if (direction == Direction.OUT) {
352             graph.addEdge(null, master, child, label);
353         } else {
354             graph.addEdge(null, child, master, label);
355         }
356     }
357 }