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.ArrayListMultimap;
23  import com.google.common.collect.Iterables;
24  import com.google.common.collect.ListMultimap;
25  import com.google.common.collect.Lists;
26  import com.google.common.collect.Maps;
27  import com.tinkerpop.blueprints.Vertex;
28  import com.tinkerpop.frames.FramedGraph;
29  import eu.ehri.project.exceptions.SerializationError;
30  import eu.ehri.project.models.EntityClass;
31  import eu.ehri.project.models.annotations.Dependent;
32  import eu.ehri.project.models.annotations.EntityType;
33  import eu.ehri.project.models.annotations.Fetch;
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.lang.reflect.Method;
40  import java.util.LinkedHashMap;
41  import java.util.List;
42  import java.util.Map;
43  
44  /**
45   * Class containing static methods to convert between FramedVertex instances,
46   * EntityBundles, and raw data.
47   */
48  public final class Serializer {
49  
50      private static final Logger logger = LoggerFactory.getLogger(Serializer.class);
51  
52      private static final int DEFAULT_CACHE_SIZE = 100;
53  
54      private static class LruCache<A, B> extends LinkedHashMap<A, B> {
55          private final int maxEntries;
56  
57          public LruCache(int maxEntries) {
58              super(maxEntries + 1, 1.0f, true);
59              this.maxEntries = maxEntries;
60          }
61  
62          @Override
63          protected boolean removeEldestEntry(Map.Entry<A, B> eldest) {
64              return super.size() > maxEntries;
65          }
66      }
67  
68      private final FramedGraph<?> graph;
69      private final int maxTraversals;
70      private final boolean dependentOnly;
71      private final boolean liteMode;
72      private final List<String> includeProps;
73      private final LruCache<String, Bundle> cache;
74  
75  
76      /**
77       * Basic constructor.
78       */
79      public Serializer(FramedGraph<?> graph) {
80          this(new Builder(graph));
81      }
82  
83      /**
84       * Builder for serializers with non-default options.
85       */
86      public static class Builder {
87          private final FramedGraph<?> graph;
88          private int maxTraversals = Fetch.DEFAULT_TRAVERSALS;
89          private boolean dependentOnly;
90          private boolean liteMode;
91          private List<String> includeProps = Lists.newArrayList();
92          private LruCache<String, Bundle> cache;
93  
94          public Builder(FramedGraph<?> graph) {
95              this.graph = graph;
96          }
97  
98          public Builder withDepth(int depth) {
99              this.maxTraversals = depth;
100             return this;
101         }
102 
103         public Builder dependentOnly() {
104             this.dependentOnly = true;
105             return this;
106         }
107 
108         public Builder dependentOnly(boolean dependentOnly) {
109             this.dependentOnly = dependentOnly;
110             return this;
111         }
112 
113         public Builder withLiteMode(boolean lite) {
114             this.liteMode = lite;
115             return this;
116         }
117 
118         public Builder withCache() {
119             return withCache(DEFAULT_CACHE_SIZE);
120         }
121 
122         public Builder withCache(int size) {
123             this.cache = new LruCache<>(size);
124             return this;
125         }
126 
127         public Builder withIncludedProperties(List<String> properties) {
128             this.includeProps = Lists.newArrayList(properties);
129             return this;
130         }
131 
132         public Serializer build() {
133             return new Serializer(this);
134         }
135 
136     }
137 
138     public Serializer(Builder builder) {
139         this(builder.graph, builder.dependentOnly,
140                 builder.maxTraversals, builder.liteMode, builder.includeProps, builder.cache);
141     }
142 
143     /**
144      * Constructor which allows specifying whether to serialize non-dependent relations
145      * and the level of traversal.
146      *
147      * @param graph         The framed graph
148      * @param dependentOnly Only serialize dependent nodes
149      * @param depth         Depth at which to stop recursion
150      * @param lite          Only serialize mandatory properties
151      * @param cache         Use a cache - use for single operations serializing many vertices
152      *                      with common attributes, and NOT for reusable serializers
153      */
154     private Serializer(FramedGraph<?> graph, boolean dependentOnly, int depth, boolean lite,
155             List<String> includeProps, LruCache<String, Bundle> cache) {
156         this.graph = graph;
157         this.dependentOnly = dependentOnly;
158         this.maxTraversals = depth;
159         this.liteMode = lite;
160         this.includeProps = includeProps;
161         this.cache = cache;
162     }
163 
164     /**
165      * Create a new serializer from this one, with extra included properties.
166      *
167      * @param includeProps a set of properties to include.
168      * @return a new serializer.
169      */
170     public Serializer withIncludedProperties(List<String> includeProps) {
171         return new Serializer(graph, dependentOnly, maxTraversals, liteMode,
172                 includeProps, cache);
173     }
174 
175     /**
176      * Create a new serializer from this one, with the given depth.
177      *
178      * @param depth the maximum depth of traversal
179      * @return a new serializer
180      */
181     public Serializer withDepth(int depth) {
182         return new Serializer(graph, dependentOnly, depth, liteMode,
183                 includeProps, cache);
184     }
185 
186     public Serializer withDependentOnly(boolean dependentOnly) {
187         return new Serializer(graph, dependentOnly, maxTraversals, liteMode,
188                 includeProps, cache);
189     }
190 
191     /**
192      * Get the list of included properties for this serializer.
193      *
194      * @return a list of property-name strings
195      */
196     public List<String> getIncludedProperties() {
197         return includeProps;
198     }
199 
200     /**
201      * Return a serializer that caches recently-serialized items.
202      *
203      * @return a new serializer
204      */
205     public Serializer withCache() {
206         return new Builder(graph)
207                 .withIncludedProperties(includeProps)
208                 .withLiteMode(liteMode)
209                 .dependentOnly(dependentOnly)
210                 .withDepth(maxTraversals)
211                 .withCache().build();
212     }
213 
214     /**
215      * Convert a vertex frame to a raw bundle of data.
216      *
217      * @param item The framed item
218      * @return A map of data
219      */
220     public <T extends Entity> Map<String, Object> entityToData(T item)
221             throws SerializationError {
222         return entityToBundle(item).toData();
223     }
224 
225     /**
226      * Convert a vertex to a raw bundle of data.
227      *
228      * @param item The item vertex
229      * @return A map of data
230      */
231     public Map<String, Object> vertexToData(Vertex item)
232             throws SerializationError {
233         return vertexToBundle(item).toData();
234     }
235 
236     /**
237      * Convert an Entity into a Bundle that includes its @Fetch'd
238      * relations.
239      *
240      * @param item The framed item
241      * @return A data bundle
242      */
243     public <T extends Entity> Bundle entityToBundle(T item)
244             throws SerializationError {
245         return vertexToBundle(item.asVertex(), 0, maxTraversals, false);
246     }
247 
248     /**
249      * Convert a Vertex into a Bundle that includes its @Fetch'd
250      * relations.
251      *
252      * @param item The item vertex
253      * @return A data bundle
254      */
255     public Bundle vertexToBundle(Vertex item)
256             throws SerializationError {
257         return vertexToBundle(item, 0, maxTraversals, false);
258     }
259 
260     /**
261      * Serialise a vertex frame to JSON.
262      *
263      * @param item The framed item
264      * @return A JSON string
265      */
266     public <T extends Entity> String entityToJson(T item)
267             throws SerializationError {
268         return DataConverter.bundleToJson(entityToBundle(item));
269     }
270 
271     /**
272      * Serialise a vertex to JSON.
273      *
274      * @param item The item vertex
275      * @return A JSON string
276      */
277     public String vertexToJson(Vertex item)
278             throws SerializationError {
279         return DataConverter.bundleToJson(vertexToBundle(item));
280     }
281 
282     /**
283      * Run a callback every time a node in a subtree is encountered,
284      * excepting the top-level node.
285      *
286      * @param item The item
287      * @param cb   A callback object
288      */
289     public <T extends Entity> void traverseSubtree(T item,
290             TraversalCallback cb) {
291         traverseSubtree(item, 0, cb);
292     }
293 
294     /**
295      * Convert a vertex into a Bundle that includes its @Fetch'd
296      * relations.
297      *
298      * @param item  The item vertex
299      * @param depth The maximum serialization depth
300      * @return A data bundle
301      */
302     private Bundle vertexToBundle(Vertex item, int depth, int maxDepth, boolean lite)
303             throws SerializationError {
304         try {
305             EntityClass type = EntityClass.withName(item
306                     .getProperty(EntityType.TYPE_KEY));
307             String id = item.getProperty(EntityType.ID_KEY);
308             logger.trace("Serializing {} ({}) at depth {}", id, type, depth);
309 
310             Class<? extends Entity> cls = type.getJavaClass();
311             Bundle.Builder builder = Bundle.Builder.withClass(type)
312                     .setId(id)
313                     .addData(getVertexData(item, type, lite))
314                     .addRelations(getRelationData(item,
315                             depth, maxDepth, lite, cls))
316                     .addMetaData(getVertexMeta(item, cls));
317             if (!lite) {
318                 builder.addMetaData(getVertexMeta(item, cls))
319                         .addMetaDataValue("gid", item.getId());
320             }
321             return builder.build();
322         } catch (IllegalArgumentException e) {
323             logger.error("Error serializing vertex with data: {}", getVertexData(item));
324             throw new SerializationError("Unable to serialize vertex: " + item, e);
325         }
326     }
327 
328     private Bundle fetch(Entity frame, int depth, int maxDepth, boolean isLite) throws SerializationError {
329         if (cache != null) {
330             String key = frame.getId() + depth + isLite;
331             if (cache.containsKey(key))
332                 return cache.get(key);
333             Bundle bundle = vertexToBundle(frame.asVertex(), depth, maxDepth, isLite);
334             cache.put(key, bundle);
335             return bundle;
336         }
337         return vertexToBundle(frame.asVertex(), depth, maxDepth, isLite);
338     }
339 
340     // TODO: Profiling shows that (unsurprisingly) this method is a
341     // performance hotspot. Rewrite it so that instead of using Frames
342     // method invocations to do the traversal, we use regular traversals
343     // whereever possible. Unfortunately the use of @JavaHandler Frame
344     // annotations will make this difficult.
345     private ListMultimap<String, Bundle> getRelationData(
346             Vertex item, int depth, int maxDepth, boolean lite, Class<?> cls) {
347         ListMultimap<String, Bundle> relations = ArrayListMultimap.create();
348         if (depth < maxDepth) {
349             Map<String, Method> fetchMethods = ClassUtils.getFetchMethods(cls);
350             logger.trace(" - Fetch methods: {}", fetchMethods);
351             for (Map.Entry<String, Method> entry : fetchMethods.entrySet()) {
352                 String relationName = entry.getKey();
353                 Method method = entry.getValue();
354 
355                 boolean isLite = liteMode || lite
356                         || shouldSerializeLite(method);
357 
358                 if (shouldTraverse(relationName, method, depth, isLite)) {
359                     int nextDepth = depth + 1;
360                     int nextMaxDepth = getNewMaxDepth(method, nextDepth, maxDepth);
361                     logger.trace("Fetching relation: {}, depth {}, {}",
362                             relationName, depth, method.getName());
363                     try {
364                         Object result = method.invoke(graph.frame(
365                                 item, cls));
366                         // The result of one of these fetchMethods should either
367                         // be a single Frame, or a Iterable<Frame>.
368                         if (result instanceof Iterable<?>) {
369                             for (Object d : (Iterable<?>) result) {
370                                 relations.put(relationName, fetch((Entity) d, nextDepth, nextMaxDepth, isLite));
371                             }
372                         } else {
373                             // This relationship could be NULL if, e.g. a
374                             // collection has no holder.
375                             if (result != null) {
376                                 relations.put(relationName, fetch((Entity) result, nextDepth, nextMaxDepth, isLite));
377                             }
378                         }
379                     } catch (Exception e) {
380                         e.printStackTrace();
381                         logger.error("Error serializing relationship for {} ({}): {}, depth {}, {}",
382                                 item, item.getProperty(EntityType.TYPE_KEY),
383                                 relationName, depth, method.getName());
384                         throw new RuntimeException(
385                                 "Unexpected error serializing Frame " + item, e);
386                     }
387                 }
388             }
389         }
390         return relations;
391     }
392 
393     private int getNewMaxDepth(Method fetchMethod, int currentDepth, int currentMaxDepth) {
394         Fetch fetchProps = fetchMethod.getAnnotation(Fetch.class);
395         int max = fetchProps.numLevels();
396         int newMax = max == -1
397                 ? currentMaxDepth
398                 : Math.min(currentDepth + max, currentMaxDepth);
399         logger.trace("Current depth {}, fetch levels: {}, current max: {}, new max: {}, {}", currentDepth, max,
400                 currentMaxDepth, newMax, fetchMethod.getName());
401         return newMax;
402     }
403 
404     private boolean shouldSerializeLite(Method method) {
405         Dependent dep = method.getAnnotation(Dependent.class);
406         Fetch fetch = method.getAnnotation(Fetch.class);
407         return dep == null && (fetch == null || !fetch.full());
408     }
409 
410     private boolean shouldTraverse(String relationName, Method method, int level, boolean lite) {
411         // In order to avoid @Fetching the whole graph we track the
412         // depth parameter and increase it for every traversal.
413         // However the @Fetch annotation can also specify a maximum
414         // level of traversal beyond which we don't serialize.
415         Fetch fetchProps = method.getAnnotation(Fetch.class);
416         Dependent dep = method.getAnnotation(Dependent.class);
417 
418         if (fetchProps == null) {
419             return false;
420         }
421 
422         if (dependentOnly && dep == null) {
423             logger.trace(
424                     "Terminating fetch dependent only is specified: {}, ifBelowLevel {}, limit {}, {}",
425                     relationName, level, fetchProps.ifBelowLevel());
426             return false;
427         }
428 
429         if (lite && fetchProps.whenNotLite()) {
430             logger.trace(
431                     "Terminating fetch because it specifies whenNotLite: {}, ifBelowLevel {}, limit {}, {}",
432                     relationName, level, fetchProps.ifBelowLevel());
433             return false;
434         }
435 
436         if (level >= fetchProps.ifBelowLevel()) {
437             logger.trace(
438                     "Terminating fetch because level exceeded ifBelowLevel on fetch clause: {}, ifBelowLevel {}, " +
439                             "limit {}, {}",
440                     relationName, level, fetchProps.ifBelowLevel());
441             return false;
442         }
443 
444         // If the fetch should only be serialized at a certain ifBelowLevel and
445         // we've exceeded that, don't serialize.
446         if (fetchProps.ifLevel() != -1 && level > fetchProps.ifLevel()) {
447             logger.trace(
448                     "Terminating fetch because ifLevel clause found on {}, ifBelowLevel {}, {}",
449                     relationName, level);
450             return false;
451         }
452         return true;
453     }
454 
455     /**
456      * Fetch a map of data from a vertex.
457      */
458     private Map<String, Object> getVertexData(Vertex item, EntityClass type, boolean lite) {
459         Map<String, Object> data = Maps.newHashMap();
460         Iterable<String> keys = lite
461                 ? getMandatoryOrSpecificProps(type)
462                 : item.getPropertyKeys();
463 
464         for (String key : keys) {
465             if (!(key.equals(EntityType.ID_KEY) || key
466                     .equals(EntityType.TYPE_KEY) || key.startsWith("_")))
467                 data.put(key, item.getProperty(key));
468         }
469         return data;
470     }
471 
472     /**
473      * Get a list of properties with are either given specifically
474      * in this serializer's includeProps attr, or are mandatory for
475      * the type.
476      *
477      * @param type An EntityClass
478      * @return A list of mandatory or included properties.
479      */
480     private List<String> getMandatoryOrSpecificProps(EntityClass type) {
481         return Lists.newArrayList(
482                 Iterables.concat(ClassUtils.getMandatoryPropertyKeys(type.getJavaClass()),
483                         includeProps));
484     }
485 
486     /**
487      * Fetch a map of metadata sourced from vertex properties.
488      * This is anything that begins with an underscore (but now
489      * two underscores)
490      */
491     private Map<String, Object> getVertexMeta(Vertex item, Class<?> cls) {
492         Map<String, Object> data = Maps.newHashMap();
493         for (String key : item.getPropertyKeys()) {
494             if (!key.startsWith("__") && key.startsWith("_")) {
495                 data.put(key.substring(1), item.getProperty(key));
496             }
497         }
498         Map<String, Method> metaMethods = ClassUtils.getMetaMethods(cls);
499         if (!metaMethods.isEmpty()) {
500             try {
501                 Object frame = graph.frame(item, cls);
502                 for (Map.Entry<String, Method> metaEntry : metaMethods.entrySet()) {
503                     Object value = metaEntry.getValue().invoke(frame);
504                     if (value != null) {
505                         data.put(metaEntry.getKey(), value);
506                     }
507                 }
508             } catch (Exception e) {
509                 throw new RuntimeException("Error fetching metadata", e);
510             }
511         }
512         return data;
513     }
514 
515     private Map<String, Object> getVertexData(Vertex item) {
516         Map<String, Object> data = Maps.newHashMap();
517         for (String key : item.getPropertyKeys()) {
518             data.put(key, item.getProperty(key));
519         }
520         return data;
521     }
522 
523     /**
524      * Run a callback every time a node in a subtree is encountered, excepting
525      * the top-level node.
526      */
527     private <T extends Entity> void traverseSubtree(T item, int depth,
528             TraversalCallback cb) {
529 
530         if (depth < maxTraversals) {
531             Class<?> cls = EntityClass
532                     .withName(item.<String>getProperty(EntityType.TYPE_KEY)).getJavaClass();
533             Map<String, Method> fetchMethods = ClassUtils.getFetchMethods(cls);
534             for (Map.Entry<String, Method> entry : fetchMethods.entrySet()) {
535 
536                 String relationName = entry.getKey();
537                 Method method = entry.getValue();
538                 if (shouldTraverse(relationName, method, depth, false)) {
539                     try {
540                         Object result = method.invoke(graph.frame(
541                                 item.asVertex(), cls));
542                         if (result instanceof Iterable<?>) {
543                             int rnum = 0;
544                             for (Object d : (Iterable<?>) result) {
545                                 cb.process((Entity) d, depth,
546                                         entry.getKey(), rnum);
547                                 traverseSubtree((Entity) d, depth + 1, cb);
548                                 rnum++;
549                             }
550                         } else {
551                             if (result != null) {
552                                 cb.process((Entity) result, depth,
553                                         entry.getKey(), 0);
554                                 traverseSubtree((Entity) result,
555                                         depth + 1, cb);
556                             }
557                         }
558                     } catch (Exception e) {
559                         e.printStackTrace();
560                         throw new RuntimeException(
561                                 "Unexpected error serializing Frame", e);
562                     }
563                 }
564             }
565         }
566     }
567 }