1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
46
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
78
79 public Serializer(FramedGraph<?> graph) {
80 this(new Builder(graph));
81 }
82
83
84
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
145
146
147
148
149
150
151
152
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
166
167
168
169
170 public Serializer withIncludedProperties(List<String> includeProps) {
171 return new Serializer(graph, dependentOnly, maxTraversals, liteMode,
172 includeProps, cache);
173 }
174
175
176
177
178
179
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
193
194
195
196 public List<String> getIncludedProperties() {
197 return includeProps;
198 }
199
200
201
202
203
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
216
217
218
219
220 public <T extends Entity> Map<String, Object> entityToData(T item)
221 throws SerializationError {
222 return entityToBundle(item).toData();
223 }
224
225
226
227
228
229
230
231 public Map<String, Object> vertexToData(Vertex item)
232 throws SerializationError {
233 return vertexToBundle(item).toData();
234 }
235
236
237
238
239
240
241
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
250
251
252
253
254
255 public Bundle vertexToBundle(Vertex item)
256 throws SerializationError {
257 return vertexToBundle(item, 0, maxTraversals, false);
258 }
259
260
261
262
263
264
265
266 public <T extends Entity> String entityToJson(T item)
267 throws SerializationError {
268 return DataConverter.bundleToJson(entityToBundle(item));
269 }
270
271
272
273
274
275
276
277 public String vertexToJson(Vertex item)
278 throws SerializationError {
279 return DataConverter.bundleToJson(vertexToBundle(item));
280 }
281
282
283
284
285
286
287
288
289 public <T extends Entity> void traverseSubtree(T item,
290 TraversalCallback cb) {
291 traverseSubtree(item, 0, cb);
292 }
293
294
295
296
297
298
299
300
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
341
342
343
344
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
367
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
374
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
412
413
414
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
445
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
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
474
475
476
477
478
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
488
489
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
525
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 }