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.fasterxml.jackson.core.JsonFactory;
23  import com.fasterxml.jackson.core.JsonParser;
24  import com.fasterxml.jackson.core.JsonProcessingException;
25  import com.fasterxml.jackson.core.JsonToken;
26  import com.fasterxml.jackson.databind.JsonNode;
27  import com.fasterxml.jackson.databind.ObjectMapper;
28  import com.fasterxml.jackson.databind.ObjectWriter;
29  import com.fasterxml.jackson.databind.module.SimpleModule;
30  import com.flipkart.zjsonpatch.JsonDiff;
31  import com.google.common.base.Charsets;
32  import com.google.common.base.Preconditions;
33  import com.google.common.collect.ArrayListMultimap;
34  import com.google.common.collect.Lists;
35  import com.google.common.collect.Maps;
36  import com.google.common.collect.Multimap;
37  import com.google.common.collect.Ordering;
38  import com.tinkerpop.blueprints.CloseableIterable;
39  import eu.ehri.project.exceptions.DeserializationError;
40  import eu.ehri.project.exceptions.SerializationError;
41  import eu.ehri.project.models.EntityClass;
42  
43  import java.io.IOException;
44  import java.io.InputStream;
45  import java.io.InputStreamReader;
46  import java.io.OutputStream;
47  import java.util.Collection;
48  import java.util.Collections;
49  import java.util.Iterator;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Map.Entry;
53  
54  class DataConverter {
55  
56      private static final JsonFactory factory = new JsonFactory();
57      private static final ObjectMapper mapper = new ObjectMapper(factory);
58      private static final ObjectWriter writer = mapper.writerWithDefaultPrettyPrinter();
59  
60      static {
61          SimpleModule bundleModule = new SimpleModule();
62          bundleModule.addDeserializer(Bundle.class, new BundleDeserializer());
63          mapper.registerModule(bundleModule);
64      }
65  
66      /**
67       * Convert an error set to a generic data structure.
68       *
69       * @param errorSet an ErrorSet instance
70       * @return a map containing the error set data
71       */
72      public static Map<String, Object> errorSetToData(ErrorSet errorSet) {
73          Map<String, Object> data = Maps.newHashMap();
74          data.put(ErrorSet.ERROR_KEY, errorSet.getErrors().asMap());
75          Map<String, List<Map<String, Object>>> relations = Maps.newLinkedHashMap();
76          Multimap<String, ErrorSet> crelations = errorSet.getRelations();
77          for (String key : crelations.keySet()) {
78              List<Map<String, Object>> rels = Lists.newArrayList();
79              for (ErrorSet subbundle : crelations.get(key)) {
80                  rels.add(errorSetToData(subbundle));
81              }
82              relations.put(key, rels);
83          }
84          data.put(ErrorSet.REL_KEY, relations);
85          return data;
86      }
87  
88      /**
89       * Convert an error set to JSON.
90       *
91       * @param errorSet an ErrorSet instance
92       * @return a JSON string representing the error set
93       */
94      public static String errorSetToJson(ErrorSet errorSet) throws SerializationError {
95          try {
96              Map<String, Object> data = errorSetToData(errorSet);
97              return writer.writeValueAsString(data);
98          } catch (JsonProcessingException e) {
99              throw new SerializationError("Error writing errorSet to JSON", e);
100         }
101     }
102 
103     /**
104      * Convert a bundle to a generic data structure.
105      *
106      * @param bundle the bundle
107      * @return a data map
108      */
109     public static Map<String, Object> bundleToData(Bundle bundle) {
110         Map<String, Object> data = Maps.newLinkedHashMap();
111         data.put(Bundle.ID_KEY, bundle.getId());
112         data.put(Bundle.TYPE_KEY, bundle.getType().getName());
113         data.put(Bundle.DATA_KEY, bundle.getData());
114         if (bundle.hasMetaData()) {
115             data.put(Bundle.META_KEY, bundle.getMetaData());
116         }
117         Map<String, List<Map<String, Object>>> relations = Maps.newLinkedHashMap();
118         Multimap<String, Bundle> crelations = bundle.getRelations();
119         List<String> sortedKeys = Ordering.natural().sortedCopy(crelations.keySet());
120         for (String key : sortedKeys) {
121             List<Map<String, Object>> rels = Lists.newArrayList();
122             for (Bundle subbundle : crelations.get(key)) {
123                 rels.add(bundleToData(subbundle));
124             }
125             relations.put(key, rels);
126         }
127         data.put(Bundle.REL_KEY, relations);
128         return data;
129     }
130 
131     /**
132      * Convert a bundle to JSON.
133      *
134      * @param bundle the bundle
135      * @return a JSON string representing the bundle
136      */
137     public static String bundleToJson(Bundle bundle) throws SerializationError {
138         try {
139             Map<String, Object> data = bundleToData(bundle);
140             return writer.writeValueAsString(data);
141         } catch (JsonProcessingException e) {
142             throw new SerializationError("Error writing bundle to JSON", e);
143         }
144     }
145 
146     /**
147      * Return a JSON-Patch representation of the difference between
148      * two bundles. Metadata is included.
149      *
150      * @param source the source bundle
151      * @param target the target bundle
152      * @return a JSON-Patch, as a string
153      */
154     public static String diffBundles(Bundle source, Bundle target) {
155         try {
156             JsonNode diff = JsonDiff.asJson(
157                     mapper.valueToTree(source), mapper.valueToTree(target));
158             return writer.writeValueAsString(diff);
159         } catch (JsonProcessingException e) {
160             throw new RuntimeException(e);
161         }
162     }
163 
164     /**
165      * Convert some JSON into an EntityBundle.
166      *
167      * @param inputStream an input stream containing JSON representing the bundle
168      * @return the bundle
169      *                              a valid bundle
170      */
171     public static Bundle streamToBundle(InputStream inputStream) throws DeserializationError {
172         try {
173             return mapper.readValue(inputStream, Bundle.class);
174         } catch (IOException e) {
175             e.printStackTrace();
176             throw new DeserializationError("Error decoding JSON", e);
177         }
178     }
179 
180     /**
181      * Write a bundle to a JSON stream.
182      *
183      * @param bundle       the bundle
184      * @param outputStream the stream
185      */
186     public static void bundleToStream(Bundle bundle, OutputStream outputStream) throws SerializationError {
187         try {
188             mapper.writeValue(outputStream, bundle);
189         } catch (IOException e) {
190             e.printStackTrace();
191             throw new SerializationError("Error encoding JSON", e);
192         }
193     }
194 
195     /**
196      * Parse an input stream containing a JSON array of bundle objects into
197      * an iterable of bundles.
198      *
199      * @param inputStream a JSON input stream
200      * @return an iterable of bundle objects
201      *                              a valid bundle
202      */
203     public static CloseableIterable<Bundle> bundleStream(InputStream inputStream) throws DeserializationError {
204         Preconditions.checkNotNull(inputStream);
205         try {
206             final JsonParser parser = factory
207                     .createParser(new InputStreamReader(inputStream, Charsets.UTF_8));
208             JsonToken jsonToken = parser.nextValue();
209             if (!parser.isExpectedStartArrayToken()) {
210                 throw new DeserializationError("Stream should be an array of objects, was: " + jsonToken);
211             }
212             final Iterator<Bundle> iterator = parser.nextValue() == JsonToken.END_ARRAY
213                     ? Collections.<Bundle>emptyIterator()
214                     : parser.readValuesAs(Bundle.class);
215             return new CloseableIterable<Bundle>() {
216                 @Override
217                 public void close() {
218                     try {
219                         parser.close();
220                     } catch (IOException e) {
221                         throw new RuntimeException(e);
222                     }
223                 }
224 
225                 @Override
226                 public Iterator<Bundle> iterator() {
227                     return iterator;
228                 }
229             };
230         } catch (IOException e) {
231             throw new DeserializationError("Error reading JSON", e);
232         }
233     }
234 
235     /**
236      * Convert some JSON into an EntityBundle.
237      *
238      * @param json a JSON string representing the bundle
239      * @return the bundle
240      *                              a valid bundle
241      */
242     public static Bundle jsonToBundle(String json) throws DeserializationError {
243         try {
244             return mapper.readValue(json, Bundle.class);
245         } catch (Exception e) {
246             e.printStackTrace();
247             throw new DeserializationError("Error decoding JSON", e);
248         }
249     }
250 
251     /**
252      * Convert generic data into a bundle.
253      * <p>
254      * Prize to whomever can remove all the unchecked warnings. I don't really
255      * know how else to do this otherwise.
256      * <p>
257      * NB: We also strip out all NULL property values at this stage.
258      *
259      * @param rawData an map object
260      * @return a bundle
261      *                              a valid bundle
262      */
263     public static Bundle dataToBundle(Object rawData)
264             throws DeserializationError {
265 
266         // Check what we've been given is actually a Map...
267         if (!(rawData instanceof Map<?, ?>))
268             throw new DeserializationError("Bundle data must be a map value.");
269 
270         Map<?, ?> data = (Map<?, ?>) rawData;
271         String id = (String) data.get(Bundle.ID_KEY);
272         EntityClass type = getType(data);
273 
274         // Guava's immutable collections don't allow null values.
275         // Since Neo4j doesn't either it's safest to trip these out
276         // at the deserialization stage. I can't think of a use-case
277         // where we'd need them.
278         Map<String, Object> properties = getSanitisedProperties(data);
279         return Bundle.of(id, type, properties, getRelationships(data));
280     }
281 
282     /**
283      * Extract relationships from the bundle data.
284      *
285      * @param data a plain map
286      * @return a multi-map of string -> bundle list
287      *                              valid relationships
288      */
289     private static Multimap<String, Bundle> getRelationships(Map<?, ?> data)
290             throws DeserializationError {
291         Multimap<String, Bundle> relationBundles = ArrayListMultimap
292                 .create();
293 
294         // It's okay to pass in a null value for relationships.
295         Object relations = data.get(Bundle.REL_KEY);
296         if (relations == null)
297             return relationBundles;
298 
299         if (relations instanceof Map) {
300             for (Entry<?, ?> entry : ((Map<?, ?>) relations).entrySet()) {
301                 if (entry.getValue() instanceof List<?>) {
302                     for (Object item : (List<?>) entry.getValue()) {
303                         relationBundles.put((String) entry.getKey(),
304                                 dataToBundle(item));
305                     }
306                 }
307             }
308         } else {
309             throw new DeserializationError(
310                     "Relationships value should be a map type");
311         }
312         return relationBundles;
313     }
314 
315     private static Map<String, Object> getSanitisedProperties(Map<?, ?> data)
316             throws DeserializationError {
317         Object props = data.get(Bundle.DATA_KEY);
318         if (props != null) {
319             if (props instanceof Map) {
320                 return sanitiseProperties((Map<?, ?>) props);
321             } else {
322                 throw new DeserializationError(
323                         "Data value not a map type! " + props.getClass().getSimpleName());
324             }
325         } else {
326             return Maps.newHashMap();
327         }
328     }
329 
330     private static EntityClass getType(Map<?, ?> data)
331             throws DeserializationError {
332         try {
333             return EntityClass.withName((String) data.get(Bundle.TYPE_KEY));
334         } catch (IllegalArgumentException e) {
335             throw new DeserializationError("Bad or unknown type key: "
336                     + data.get(Bundle.TYPE_KEY));
337         }
338     }
339 
340     private static Map<String, Object> sanitiseProperties(Map<?, ?> data) {
341         Map<String, Object> cleaned = Maps.newHashMap();
342         for (Entry<?, ?> entry : data.entrySet()) {
343             Object value = entry.getValue();
344             // Allow any null value, as long as it's not an empty array
345             if (!isEmptySequence(value)) {
346                 cleaned.put((String) entry.getKey(), entry.getValue());
347             }
348         }
349         return cleaned;
350     }
351 
352     /**
353      * Ensure a value isn't an empty array or list, which will
354      * cause Neo4j to barf.
355      *
356      * @param value A unknown object
357      * @return If the object is a sequence type, and is empty
358      */
359     static boolean isEmptySequence(Object value) {
360         if (value == null) {
361             return false;
362         } else if (value instanceof Object[]) {
363             return ((Object[]) value).length == 0;
364         } else if (value instanceof Collection<?>) {
365             return ((Collection) value).isEmpty();
366         } else if (value instanceof Iterable<?>) {
367             return !((Iterable) value).iterator().hasNext();
368         }
369         return false;
370     }
371 }