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.ImmutableListMultimap;
24  import com.google.common.collect.ImmutableMap;
25  import com.google.common.collect.LinkedHashMultiset;
26  import com.google.common.collect.Lists;
27  import com.google.common.collect.Maps;
28  import com.google.common.collect.Multimap;
29  import com.google.common.collect.Sets;
30  import com.tinkerpop.blueprints.CloseableIterable;
31  import com.tinkerpop.blueprints.Direction;
32  import eu.ehri.project.exceptions.DeserializationError;
33  import eu.ehri.project.exceptions.SerializationError;
34  import eu.ehri.project.models.EntityClass;
35  import eu.ehri.project.models.idgen.IdGenerator;
36  import eu.ehri.project.models.utils.ClassUtils;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import java.io.InputStream;
41  import java.io.OutputStream;
42  import java.util.Collection;
43  import java.util.Collections;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Objects;
47  import java.util.Optional;
48  import java.util.Set;
49  import java.util.function.BiPredicate;
50  import java.util.function.Function;
51  import java.util.function.Predicate;
52  
53  import static com.google.common.base.Preconditions.checkNotNull;
54  
55  /**
56   * Class that represents a graph entity and subtree relations
57   * prior to being materialised as vertices and edges.
58   * <p>
59   * Note: unlike a vertex, a bundle can contain null values
60   * in its data map, though these values will not be externally
61   * visible. Null values <i>are</i> however used in merge operations,
62   * where the secondary bundle's null data values will indicate that
63   * the key/value should be removed from the primary bundle's data.
64   */
65  public final class Bundle implements NestableData<Bundle> {
66  
67      private static final Logger logger = LoggerFactory.getLogger(Bundle.class);
68  
69      private final boolean temp;
70      private final String id;
71      private final EntityClass type;
72      private final Map<String, Object> data;
73      private final ImmutableMap<String, Object> meta;
74      private final ImmutableListMultimap<String, Bundle> relations;
75  
76      /**
77       * Serialization constant definitions
78       */
79      public static final String ID_KEY = "id";
80      public static final String REL_KEY = "relationships";
81      public static final String DATA_KEY = "data";
82      public static final String TYPE_KEY = "type";
83      public static final String META_KEY = "meta";
84  
85      /**
86       * Properties that are "managed", i.e. automatically set
87       * date/time strings or cache values should begin with a
88       * prefix and are ignored Bundle equality calculations.
89       */
90      static final String MANAGED_PREFIX = "_";
91  
92      public static class Builder {
93          private String id;
94          private final EntityClass type;
95          final Multimap<String, Bundle> relations = ArrayListMultimap.create();
96          final Map<String, Object> data = Maps.newHashMap();
97          final Map<String, Object> meta = Maps.newHashMap();
98  
99          public Builder setId(String id) {
100             this.id = id;
101             return this;
102         }
103 
104         private Builder(EntityClass cls) {
105             type = cls;
106         }
107 
108         public static Builder withClass(EntityClass cls) {
109             return new Builder(cls);
110         }
111 
112         public static Builder from(Bundle bundle) {
113             return withClass(bundle.getType())
114                     .setId(bundle.getId())
115                     .addMetaData(bundle.getMetaData())
116                     .addData(bundle.getData())
117                     .addRelations(bundle.getRelations());
118         }
119 
120         public Builder addRelations(Multimap<String, Bundle> r) {
121             relations.putAll(r);
122             return this;
123         }
124 
125         public Builder addRelation(String relation, Bundle bundle) {
126             relations.put(relation, bundle);
127             return this;
128         }
129 
130         public Builder addData(Map<String, Object> d) {
131             data.putAll(d);
132             return this;
133         }
134 
135         public Builder addDataValue(String key, Object value) {
136             data.put(key, value);
137             return this;
138         }
139 
140         public Builder addDataMultiValue(String key, Object value) {
141             if (!data.containsKey(key)) {
142                 data.put(key, value);
143             } else {
144                 Object current = data.get(key);
145                 if (current instanceof List) {
146                     ((List)current).add(value);
147                     data.put(key, current);
148                 } else {
149                     data.put(key, Lists.newArrayList(current, value));
150                 }
151             }
152             return this;
153         }
154 
155         public Builder addMetaData(Map<String, Object> d) {
156             meta.putAll(d);
157             return this;
158         }
159 
160         public Builder addMetaDataValue(String key, Object value) {
161             meta.put(key, value);
162             return this;
163         }
164 
165         public Bundle build() {
166             return of(id, type, data, relations, meta);
167         }
168     }
169 
170     /**
171      * Constructor.
172      *
173      * @param id        The bundle's id
174      * @param type      The bundle's type class
175      * @param data      An initial map of data
176      * @param relations An initial set of relations
177      * @param meta      An initial map of metadata
178      * @param temp      A marker to indicate the ID has been generated
179      */
180     private Bundle(String id, EntityClass type, Map<String, Object> data,
181             Multimap<String, Bundle> relations, Map<String, Object> meta, boolean temp) {
182         this.id = id;
183         this.type = type;
184         this.data = filterData(data);
185         this.meta = ImmutableMap.copyOf(meta);
186         this.relations = ImmutableListMultimap.copyOf(relations);
187         this.temp = temp;
188     }
189 
190     /**
191      * Factory for bundle without existing id.
192      *
193      * @param id        The bundle's id
194      * @param type      The bundle's type class
195      * @param data      An initial map of data
196      * @param relations An initial set of relations
197      */
198     public static Bundle of(String id, EntityClass type, Map<String, Object> data,
199             Multimap<String, Bundle> relations) {
200         return of(id, type, data, relations, Maps.<String, Object>newHashMap());
201     }
202 
203     /**
204      * Factory.
205      *
206      * @param id        The bundle's id
207      * @param type      The bundle's type class
208      * @param data      An initial map of data
209      * @param relations An initial set of relations
210      * @param meta      An initial map of metadata
211      */
212     public static Bundle of(String id, EntityClass type, Map<String, Object> data,
213             Multimap<String, Bundle> relations, Map<String, Object> meta) {
214         return new Bundle(id, type, data, relations, meta, false);
215     }
216 
217     /**
218      * Factory for bundle without existing id.
219      *
220      * @param type      The bundle's type class
221      * @param data      An initial map of data
222      * @param relations An initial set of relations
223      */
224     public static Bundle of(EntityClass type, Map<String, Object> data,
225             Multimap<String, Bundle> relations) {
226         return of(null, type, data, relations);
227     }
228 
229     /**
230      * Constructor for just a type.
231      *
232      * @param type The bundle's type class
233      */
234     public static Bundle of(EntityClass type) {
235         return of(null, type, Maps.<String, Object>newHashMap(), ArrayListMultimap
236                 .<String, Bundle>create(), Maps.<String, Object>newHashMap());
237     }
238 
239     /**
240      * Constructor for bundle without existing id or relations.
241      *
242      * @param type The bundle's type class
243      * @param data An initial map of data
244      */
245     public static Bundle of(EntityClass type, Map<String, Object> data) {
246         return of(null, type, data, ArrayListMultimap.<String, Bundle>create(),
247                 Maps.<String, Object>newHashMap());
248     }
249 
250     /**
251      * Get the id of the bundle's graph vertex (or null if it does not yet
252      * exist).
253      *
254      * @return The bundle's id
255      */
256     public String getId() {
257         return id;
258     }
259 
260     /**
261      * Get a bundle with the given id.
262      *
263      * @param id The bundle's id
264      */
265     public Bundle withId(String id) {
266         checkNotNull(id);
267         return new Bundle(id, type, data, relations, meta, temp);
268     }
269 
270     /**
271      * Get the type of entity this bundle represents as per the target class's
272      * entity type key.
273      *
274      * @return The bundle's type
275      */
276     public EntityClass getType() {
277         return type;
278     }
279 
280     /**
281      * Get a data value by its key.
282      *
283      * @return The data value, or null if there is no data for this key
284      * @throws ClassCastException if the fetched data does not match the
285      *                            type requested
286      */
287     @SuppressWarnings("unchecked")
288     @Override
289     public <T> T getDataValue(String key) throws ClassCastException {
290         checkNotNull(key);
291         return (T) data.get(key);
292     }
293 
294     /**
295      * Set a value in the bundle's data. If value is null,
296      * this Bundle is returned.
297      *
298      * @param key   The data key
299      * @param value The data value
300      * @return A new bundle with value as key
301      */
302     @Override
303     public Bundle withDataValue(String key, Object value) {
304         Map<String, Object> newData = Maps.newHashMap(data);
305         newData.put(key, value);
306         return withData(newData);
307     }
308 
309     /**
310      * Set a value in the bundle's meta data.
311      *
312      * @param key   The metadata key
313      * @param value The metadata value
314      * @return A new bundle
315      */
316     public Bundle withMetaDataValue(String key, Object value) {
317         if (value == null) {
318             return this;
319         } else {
320             Map<String, Object> newData = Maps.newHashMap(meta);
321             newData.put(key, value);
322             return withMetaData(newData);
323         }
324     }
325 
326     /**
327      * Remove a value in the bundle's data.
328      *
329      * @param key The data key to remove
330      * @return A new bundle
331      */
332     @Override
333     public Bundle removeDataValue(String key) {
334         Map<String, Object> newData = Maps.newHashMap(data);
335         newData.remove(key);
336         return withData(newData);
337     }
338 
339     /**
340      * Get the bundle data.
341      *
342      * @return The full data map
343      */
344     public Map<String, Object> getData() {
345         return ImmutableMap.copyOf(Maps.filterValues(data, Objects::nonNull));
346     }
347 
348     /**
349      * Get the bundle metadata
350      *
351      * @return The full metadata map
352      */
353     public Map<String, Object> getMetaData() {
354         return meta;
355     }
356 
357 
358     /**
359      * Check if this bundle has associated metadata.
360      *
361      * @return Whether metadata is present
362      */
363     public boolean hasMetaData() {
364         return !meta.isEmpty();
365     }
366 
367     /**
368      * Set the entire data map for this bundle.
369      *
370      * @param data The full data map to set
371      * @return The new bundle
372      */
373     public Bundle withData(Map<String, Object> data) {
374         return new Bundle(id, type, data, relations, meta, temp);
375     }
376 
377     /**
378      * Set the entire meta data map for this bundle.
379      *
380      * @param meta The full metadata map to set
381      * @return The new bundle
382      */
383     public Bundle withMetaData(Map<String, Object> meta) {
384         return new Bundle(id, type, data, relations, meta, temp);
385     }
386 
387     /**
388      * Get the bundle's relation bundles.
389      *
390      * @return The full set of relations
391      */
392     @Override
393     public Multimap<String, Bundle> getRelations() {
394         return relations;
395     }
396 
397     /**
398      * Get only the bundle's relations which have a dependent
399      * relationship.
400      *
401      * @return A multimap of dependent relations.
402      */
403     public Multimap<String, Bundle> getDependentRelations() {
404         Multimap<String, Bundle> dependentRelations = ArrayListMultimap.create();
405         Map<String, Direction> dependents = ClassUtils
406                 .getDependentRelations(type.getJavaClass());
407         for (String relation : relations.keySet()) {
408             if (dependents.containsKey(relation)) {
409                 for (Bundle child : relations.get(relation)) {
410                     dependentRelations.put(relation, child);
411                 }
412             }
413         }
414         return dependentRelations;
415     }
416 
417     /**
418      * Set entire set of relations.
419      *
420      * @param relations A full set of relations
421      * @return The new bundle
422      */
423     @Override
424     public Bundle replaceRelations(Multimap<String, Bundle> relations) {
425         return new Bundle(id, type, data, relations, meta, temp);
426     }
427 
428     /**
429      * Add a map of addition relationships.
430      *
431      * @param others Additional relationship map
432      * @return The new bundle
433      */
434     @Override
435     public Bundle withRelations(Multimap<String, Bundle> others) {
436         Multimap<String, Bundle> tmp = ArrayListMultimap
437                 .create(relations);
438         tmp.putAll(others);
439         return new Bundle(id, type, data, tmp, meta, temp);
440     }
441 
442     /**
443      * Get a set of relations.
444      *
445      * @param relation A relationship key
446      * @return A given set of relations
447      */
448     @Override
449     public List<Bundle> getRelations(String relation) {
450         return relations.get(relation);
451     }
452 
453     /**
454      * Set bundles for a particular relation.
455      *
456      * @param relation A relationship key
457      * @param others   A set of relations for the given key
458      * @return A new bundle
459      */
460     @Override
461     public Bundle withRelations(String relation, List<Bundle> others) {
462         Multimap<String, Bundle> tmp = ArrayListMultimap
463                 .create(relations);
464         tmp.putAll(relation, others);
465         return new Bundle(id, type, data, tmp, meta, temp);
466     }
467 
468     /**
469      * Add a bundle for a particular relation.
470      *
471      * @param relation A relationship key
472      * @param other    A related bundle
473      * @return A new bundle
474      */
475     @Override
476     public Bundle withRelation(String relation, Bundle other) {
477         Multimap<String, Bundle> tmp = ArrayListMultimap
478                 .create(relations);
479         tmp.put(relation, other);
480         return new Bundle(id, type, data, tmp, meta, temp);
481     }
482 
483     /**
484      * Check if this bundle contains the given relation set.
485      *
486      * @param relation A relationship key
487      * @return Whether this bundle has relations for the given key
488      */
489     public boolean hasRelations(String relation) {
490         return relations.containsKey(relation);
491     }
492 
493     /**
494      * Remove a single relation.
495      *
496      * @param relation A relationship key
497      * @param item     The item to remove
498      * @return A new bundle
499      */
500     public Bundle removeRelation(String relation, Bundle item) {
501         Multimap<String, Bundle> tmp = ArrayListMultimap.create(relations);
502         tmp.remove(relation, item);
503         return new Bundle(id, type, data, tmp, meta, temp);
504     }
505 
506     /**
507      * Merge this bundle's data with that of another. Relation data is merged when
508      * corresponding related items exist in the tree, but new related items are not
509      * added.
510      *
511      * @param otherBundle Another bundle
512      * @return A bundle with data merged
513      */
514     public Bundle mergeDataWith(final Bundle otherBundle) {
515         Map<String, Object> mergeData = Maps.newHashMap(getData());
516 
517         // This merges the data maps so that keys with null values in the
518         // second bundle are removed from the current one's data
519         logger.trace("Merging data: {}", otherBundle.data);
520         for (Map.Entry<String, Object> entry : otherBundle.data.entrySet()) {
521             if (entry.getValue() != null) {
522                 mergeData.put(entry.getKey(), entry.getValue());
523             } else {
524                 logger.trace("Unset key in merge: {}", entry.getKey());
525                 mergeData.remove(entry.getKey());
526             }
527         }
528         final Builder builder = Builder.withClass(getType()).setId(getId()).addMetaData(meta)
529                 .addData(mergeData);
530 
531         // This is a slightly gnarly algorithm as written.
532         // We want to merge two relationship trees
533         for (Map.Entry<String, Collection<Bundle>> entry : otherBundle.getRelations().asMap().entrySet()) {
534             String relName = entry.getKey();
535             if (relations.containsKey(relName)) {
536                 List<Bundle> relations = getRelations(relName);
537                 Collection<Bundle> otherRelations = entry.getValue();
538                 Set<Bundle> updated = Sets.newHashSet();
539                 for (final Bundle otherRel : otherRelations) {
540                     Optional<Bundle> toUpdate = relations.stream().filter(
541                             bundle -> bundle.getId() != null && bundle.getId().equals(otherRel.getId()))
542                             .findFirst();
543                     if (toUpdate.isPresent()) {
544                         Bundle up = toUpdate.get();
545                         updated.add(up);
546                         builder.addRelation(relName, up.mergeDataWith(otherRel));
547                     } else {
548                         logger.warn("Ignoring nested bundle in PATCH update: {}", otherRel);
549                     }
550                 }
551                 for (Bundle bundle : relations) {
552                     if (!updated.contains(bundle)) {
553                         builder.addRelation(relName, bundle);
554                     }
555                 }
556             }
557         }
558 
559         for (Map.Entry<String, Bundle> entry : relations.entries()) {
560             if (!otherBundle.hasRelations(entry.getKey())) {
561                 builder.addRelation(entry.getKey(), entry.getValue());
562             }
563         }
564 
565         return builder.build();
566     }
567 
568     /**
569      * Filter relations, removing items that *match* the given
570      * filter function.
571      *
572      * @param filter a predicate taking a string relation name
573      *               and a bundle item
574      * @return a bundle with relations matching the given predicate function removed.
575      */
576     public Bundle filterRelations(BiPredicate<String, Bundle> filter) {
577         final Multimap<String, Bundle> newRels = ArrayListMultimap.create();
578         for (Map.Entry<String, Bundle> rel : relations.entries()) {
579             if (!filter.test(rel.getKey(), rel.getValue())) {
580                 newRels.put(rel.getKey(), rel.getValue()
581                         .filterRelations(filter));
582             }
583         }
584         return replaceRelations(newRels);
585     }
586 
587     /**
588      * Run a function transforming all items in the bundle, including the top level,
589      * returning a new bundle.
590      *
591      * @param f A (pure) function transforming the bundle
592      * @return A new bundle
593      */
594     public Bundle map(Function<Bundle, Bundle> f) {
595         Bundle me = f.apply(this);
596         final Multimap<String, Bundle> newRels = ArrayListMultimap.create();
597         for (Map.Entry<String, Bundle> rel : me.getRelations().entries()) {
598             newRels.put(rel.getKey(), rel.getValue().map(f));
599         }
600         return me.replaceRelations(newRels);
601     }
602 
603     /**
604      * Test if a predicate function holds true for any item in the
605      * bundle, including the top level.
606      *
607      * @param f A predicate function
608      * @return If the predicate tested true
609      */
610     public boolean forAny(Predicate<Bundle> f) {
611         return find(f).isPresent();
612     }
613 
614     public Optional<Bundle> find(Predicate<Bundle> f) {
615         if (f.test(this)) {
616             return Optional.of(this);
617         }
618         for (Map.Entry<String, Bundle> rel : relations.entries()) {
619             Optional<Bundle> findRel = rel.getValue().find(f);
620             if (findRel.isPresent()) {
621                 return findRel;
622             }
623         }
624         return Optional.empty();
625     }
626 
627     /**
628      * Get the target class.
629      *
630      * @return The bundle's type class
631      */
632     public Class<?> getBundleJavaClass() {
633         return type.getJavaClass();
634     }
635 
636     /**
637      * Return a list of names for mandatory properties, as represented in the
638      * graph.
639      *
640      * @return A list of property keys for the bundle's type
641      */
642     public Collection<String> getPropertyKeys() {
643         return ClassUtils.getPropertyKeys(type.getJavaClass());
644     }
645 
646     /**
647      * Return a list of property keys which must be unique.
648      *
649      * @return A list of unique property keys for the bundle's type
650      */
651     public Collection<String> getUniquePropertyKeys() {
652         return ClassUtils.getUniquePropertyKeys(type.getJavaClass());
653     }
654 
655     /**
656      * Create a bundle from raw data.
657      *
658      * @param data A raw data object
659      * @return A bundle
660      */
661     public static Bundle fromData(Object data) throws DeserializationError {
662         return DataConverter.dataToBundle(data);
663     }
664 
665     /**
666      * Serialize a bundle to raw data.
667      *
668      * @return A raw data object
669      */
670     public Map<String, Object> toData() {
671         return DataConverter.bundleToData(this);
672     }
673 
674     /**
675      * Create a bundle from a (JSON) string.
676      *
677      * @param json A JSON representation
678      * @return A bundle
679      */
680     public static Bundle fromString(String json) throws DeserializationError {
681         return DataConverter.jsonToBundle(json);
682     }
683 
684     /**
685      * Create a bundle from a stream containing JSON data..
686      *
687      * @param stream A JSON stream
688      * @return A bundle
689      */
690     public static Bundle fromStream(InputStream stream) throws DeserializationError {
691         return DataConverter.streamToBundle(stream);
692     }
693 
694     /**
695      * Write a bundle to a JSON stream.
696      *
697      * @param bundle the bundle
698      * @param stream the output stream
699      */
700     public static void toStream(Bundle bundle, OutputStream stream) throws SerializationError {
701         DataConverter.bundleToStream(bundle, stream);
702     }
703 
704     public static CloseableIterable<Bundle> bundleStream(InputStream inputStream) throws DeserializationError {
705         return DataConverter.bundleStream(inputStream);
706     }
707 
708     @Override
709     public String toString() {
710         return "<" + getType() + ": '" + (id == null ? "?" : id) + "'> (" + getData() + " + Rels: " + relations + ")";
711     }
712 
713     /**
714      * Serialize a bundle to a JSON string.
715      *
716      * @return json string
717      */
718     public String toJson() {
719         try {
720             return DataConverter.bundleToJson(this);
721         } catch (SerializationError e) {
722             return "Invalid Bundle: " + e.getMessage();
723         }
724     }
725 
726     /**
727      * Check if this bundle as a generated ID.
728      *
729      * @return True if the ID has been synthesised.
730      */
731     public boolean hasGeneratedId() {
732         return temp;
733     }
734 
735     /**
736      * The depth of this bundle tree, i.e. the number
737      * of levels of relationships beneath this one.
738      *
739      * @return the number of levels
740      */
741     public int depth() {
742         int depth = 0;
743         for (Bundle rel : relations.values()) {
744             depth = Math.max(depth, 1 + rel.depth());
745         }
746         return depth;
747     }
748 
749     /**
750      * Return a bundle consisting of only dependent relations.
751      *
752      * @return a new bundle with non-dependent relations removed
753      */
754     public Bundle dependentsOnly() {
755         Map<String, Direction> dependents = ClassUtils
756                 .getDependentRelations(type.getJavaClass());
757         Multimap<String, Bundle> tmp = ArrayListMultimap.create();
758         for (String relation : relations.keySet()) {
759             if (dependents.containsKey(relation)) {
760                 for (Bundle bundle : relations.get(relation)) {
761                     tmp.put(relation, bundle.dependentsOnly());
762                 }
763             }
764         }
765         return new Bundle(id, type, data, tmp, meta, temp);
766     }
767 
768     /**
769      * Generate missing IDs for the subtree.
770      *
771      * @param scopes A set of parent scopes.
772      * @return A new bundle
773      */
774     public Bundle generateIds(Collection<String> scopes) {
775         boolean isTemp = id == null;
776         IdGenerator idGen = getType().getIdGen();
777         String newId = isTemp ? idGen.generateId(scopes, this) : id;
778         Multimap<String, Bundle> idRels = ArrayListMultimap.create();
779         List<String> nextScopes = Lists.newArrayList(scopes);
780         nextScopes.add(idGen.getIdBase(this));
781         for (Map.Entry<String, Bundle> entry : relations.entries()) {
782             idRels.put(entry.getKey(), entry.getValue().generateIds(nextScopes));
783         }
784         return new Bundle(newId, type, data, idRels, meta, isTemp);
785     }
786 
787     @Override
788     public boolean equals(Object o) {
789         if (this == o) return true;
790         if (o == null || getClass() != o.getClass()) return false;
791 
792         Bundle bundle = (Bundle) o;
793 
794         return type == bundle.type
795                 && unmanagedData(data).equals(unmanagedData(bundle.data))
796                 && unorderedRelations(relations).equals(unorderedRelations(bundle.relations));
797     }
798 
799     @Override
800     public int hashCode() {
801         int result = type.hashCode();
802         result = 31 * result + unmanagedData(data).hashCode();
803         result = 31 * result + unorderedRelations(relations).hashCode();
804         return result;
805     }
806 
807     /**
808      * Return a JSON-Patch representation of the difference between
809      * this bundle and another. Metadata is ignored.
810      *
811      * @param target the target bundle
812      * @return a JSON-Patch, as a string
813      */
814     public String diff(Bundle target) {
815         return DataConverter.diffBundles(
816                 withMetaData(Collections.emptyMap()),
817                 target.withMetaData(Collections.emptyMap()));
818     }
819 
820     // Helpers...
821 
822     /**
823      * Return a copy of the given data map with enums converted to string values.
824      */
825     private Map<String, Object> filterData(Map<String, Object> data) {
826         Map<String, Object> filtered = Maps.newHashMap();
827         for (Map.Entry<? extends String, Object> entry : data.entrySet()) {
828             Object value = entry.getValue();
829             if (value instanceof Enum<?>) {
830                 value = ((Enum) value).name();
831             }
832             filtered.put(entry.getKey(), value);
833         }
834         return filtered;
835     }
836 
837     /**
838      * Return a set of data with 'managed' items (prefixed by a particular
839      * key) removed.
840      */
841     private Map<String, Object> unmanagedData(Map<String, Object> in) {
842         Map<String, Object> filtered = Maps.newHashMap();
843         for (Map.Entry<? extends String, Object> entry : in.entrySet()) {
844             if (!entry.getKey().startsWith(MANAGED_PREFIX)
845                     && entry.getValue() != null) {
846                 filtered.put(entry.getKey(), entry.getValue());
847             }
848         }
849         return filtered;
850     }
851 
852     /**
853      * Convert the ordered relationship set into an unordered one for comparison.
854      */
855     private Map<String, LinkedHashMultiset<Bundle>> unorderedRelations(Multimap<String, Bundle> rels) {
856         Map<String, LinkedHashMultiset<Bundle>> map = Maps.newHashMap();
857         for (Map.Entry<String, Collection<Bundle>> entry : rels.asMap().entrySet()) {
858             map.put(entry.getKey(), LinkedHashMultiset.create(entry.getValue()));
859         }
860         return map;
861     }
862 }