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.utils;
21  
22  import com.google.common.collect.ArrayListMultimap;
23  import com.google.common.collect.ListMultimap;
24  import com.google.common.collect.Lists;
25  import eu.ehri.project.persistence.NestableData;
26  
27  import java.util.List;
28  import java.util.function.BiFunction;
29  
30  /**
31   * Helpers for working with the {@link NestableData} format.
32   */
33  public class DataUtils {
34  
35      public static class BundlePathError extends NullPointerException {
36          BundlePathError(String message) {
37              super(message);
38          }
39      }
40  
41      static class BundleIndexError extends IndexOutOfBoundsException {
42          BundleIndexError(String message) {
43              super(message);
44          }
45      }
46  
47      /**
48       * XPath-like method for getting the value of a nested relation's attribute.
49       * <p>
50       * <pre>
51       * {@code
52       * String lang = DataUtils.get(item, "describes[0]/languageCode"));
53       * }
54       * </pre>
55       *
56       * @param item the item
57       * @param path a path string
58       * @param <T>  the type to fetch
59       * @return a property of type T
60       */
61      public static <T, N extends NestableData<N>> T get(N item, String path) {
62          return fetchAttribute(item, NestableDataPath.fromString(path),
63                  (subjectNode, subjectPath) -> subjectNode.getDataValue(subjectPath
64                          .getTerminus()));
65      }
66  
67      /**
68       * XPath-like method for getting a item at a given path.
69       * <p>
70       * <pre>
71       * {@code
72       * Bundle description = DataUtils.getItem(item, "describes[0]"));
73       * }
74       * </pre>
75       *
76       * @param item the item
77       * @param path a path string
78       * @return a item found at the given path
79       */
80      public static <N extends NestableData<N>> N getItem(N item, String path) {
81          return fetchNode(item, NestableDataPath.fromString(path));
82      }
83  
84      /**
85       * XPath-like method for deleting the value of a nested relation's
86       * attribute.
87       * <p>
88       * <pre>
89       * {@code
90       * Bundle newBundle = DataUtils.delete(item, "describes[0]/languageCode"));
91       * }
92       * </pre>
93       *
94       * @param item the item
95       * @param path a path string
96       * @return the item with the item at the given path deleted
97       */
98      public static <N extends NestableData<N>> N delete(N item, String path) {
99          return mutateAttribute(item, NestableDataPath.fromString(path),
100                 (subject, p) -> subject.removeDataValue(p.getTerminus()));
101     }
102 
103 
104     /**
105      * XPath-like method for deleting a node from a nested tree, i.e:
106      * <p>
107      * <pre>
108      * {@code
109      * Bundle newBundle = DataUtils.deleteItem(item, "describes[0]"));
110      * }
111      * </pre>
112      *
113      * @param item the item
114      * @param path a path string
115      * @return the item with the item at the given path deleted
116      */
117     public static <N extends NestableData<N>> N deleteItem(N item, String path) {
118         return deleteNode(item, NestableDataPath.fromString(path));
119     }
120 
121     /**
122      * Xpath-like method for creating a new item by updating a nested relation
123      * of an existing item.
124      * <p>
125      * <pre>
126      * {@code
127      * Bundle newBundle = DataUtils.set(oldBundle, "hasDate[0]/startDate", "1923-10-10");
128      * }
129      * </pre>
130      *
131      * @param item  the item
132      * @param path  a path string
133      * @param value the value being set
134      * @param <T>   the type of property being set
135      * @return the item with the property at the given path set
136      */
137     public static <T, N extends NestableData<N>> N set(N item, String path, final T value) {
138         return mutateAttribute(item, NestableDataPath.fromString(path),
139                 (subject, p) -> subject.withDataValue(p.getTerminus(), value));
140     }
141 
142     /**
143      * Xpath-like method for creating a new item by updating a nested relation
144      * of an existing item.
145      * <p>
146      * <pre>
147      * {@code
148      * Bundle dateBundle = ...
149      * Bundle newItem = DataUtils.setItem(oldBundle, "hasDate[0]", dateBundle);
150      * }
151      * </pre>
152      * <p>
153      * Use an index of -1 to create a relationship set or append to
154      * an existing one.
155      *
156      * @param item    the item
157      * @param path    a path string
158      * @param newItem the new item to set at the path
159      * @return a item with the given item set at the given path
160      */
161     public static <N extends NestableData<N>> N setItem(N item, String path, N newItem) {
162         return setNode(item, NestableDataPath.fromString(path), newItem);
163     }
164 
165     /**
166      * Xpath-like method to fetch a set of nested relations.
167      * <p>
168      * <pre>
169      * {@code
170      * List<Bundle> dates = DataUtils.getRelations(item, "hasDate");
171      * }
172      * </pre>
173      *
174      * @param item the item
175      * @param path a path string
176      * @return a list of bundles at the given relationship path
177      */
178     public static <N extends NestableData<N>> List<N> getRelations(N item, String path) {
179         return fetchAttribute(item, NestableDataPath.fromString(path),
180                 (subjectNode, subjectPath) -> Lists.newArrayList(subjectNode.getRelations(subjectPath
181                         .getTerminus())));
182     }
183 
184     // Private implementation helpers.
185 
186     private static <T, N extends NestableData<N>> T fetchAttribute(N bundle, NestableDataPath path,
187             BiFunction<N, NestableDataPath, T> op) {
188         if (path.isEmpty()) {
189             return op.apply(bundle, path);
190         } else {
191             PathSection section = path.current();
192             if (!bundle.hasRelations(section.getPath()))
193                 throw new BundlePathError(String.format(
194                         "Relation path '%s' not found", section.getPath()));
195             try {
196                 List<N> relations = bundle.getRelations(section.getPath());
197                 return fetchAttribute(relations.get(section.getIndex()),
198                         path.next(), op);
199             } catch (IndexOutOfBoundsException e) {
200                 throw new BundleIndexError(String.format(
201                         "Relation index '%s[%s]' not found", path.next(),
202                         section.getIndex()));
203             }
204         }
205     }
206 
207     private static <N extends NestableData<N>> N fetchNode(N bundle, NestableDataPath path) {
208         if (path.getTerminus() == null) {
209             throw new IllegalArgumentException(
210                     "Last component of path must be a valid subtree address.");
211         }
212         if (path.isEmpty()) {
213             return bundle;
214         } else {
215             PathSection section = path.current();
216             if (!bundle.hasRelations(section.getPath()))
217                 throw new BundlePathError(String.format(
218                         "Relation path '%s' not found", section.getPath()));
219             List<N> relations = bundle.getRelations(section.getPath());
220             try {
221                 N next = relations.get(section.getIndex());
222                 return fetchNode(next, path.next());
223             } catch (IndexOutOfBoundsException e) {
224                 throw new BundleIndexError(String.format(
225                         "Relation index '%s[%s]' not found: %s", section.getPath(),
226                         section.getIndex(), relations));
227             }
228         }
229     }
230 
231     private static <N extends NestableData<N>> N mutateAttribute(N bundle, NestableDataPath path,
232             BiFunction<N, NestableDataPath, N> op) {
233         // Case one: the main path is empty, so we *only* run
234         // the op on the top-level node.
235         if (path.isEmpty()) {
236             return op.apply(bundle, path);
237         } else {
238             // Case two:
239             PathSection section = path.current();
240             if (!bundle.hasRelations(section.getPath()))
241                 throw new BundlePathError(String.format(
242                         "Relation path '%s' not found", section.getPath()));
243             ListMultimap<String, N> allRelations = ArrayListMultimap
244                     .create(bundle.getRelations());
245             try {
246                 List<N> relations = Lists.newArrayList(allRelations
247                         .removeAll(section.getPath()));
248                 N subject = relations.get(section.getIndex());
249                 relations.set(section.getIndex(),
250                         mutateAttribute(subject, path.next(), op));
251                 allRelations.putAll(section.getPath(), relations);
252                 return bundle.replaceRelations(allRelations);
253             } catch (IndexOutOfBoundsException e) {
254                 throw new BundleIndexError(String.format(
255                         "Relation index '%s[%s]' not found", section.getPath(),
256                         section.getIndex()));
257             }
258         }
259     }
260 
261     private static <N extends NestableData<N>> N setNode(N bundle, NestableDataPath path, N newNode) {
262         if (path.getTerminus() == null) {
263             throw new IllegalArgumentException(
264                     "Last component of path must be a valid subtree address.");
265         }
266         if (path.isEmpty())
267             throw new IllegalArgumentException("Path must refer to a nested node.");
268 
269         PathSection section = path.current();
270         NestableDataPath next = path.next();
271 
272         if (section.getIndex() != -1 && !bundle.hasRelations(section.getPath())) {
273             throw new BundlePathError(String.format(
274                     "Relation path '%s' not found", section.getPath()));
275         }
276         ListMultimap<String, N> allRelations = ArrayListMultimap
277                 .create(bundle.getRelations());
278         try {
279             List<N> relations = Lists.newArrayList(allRelations
280                     .removeAll(section.getPath()));
281             if (next.isEmpty()) {
282                 // If the index is negative, add to the end...
283                 if (section.getIndex() == -1) {
284                     relations.add(newNode);
285                 } else {
286                     relations.set(section.getIndex(), newNode);
287                 }
288             } else {
289                 N subject = relations.get(section.getIndex());
290                 relations.set(section.getIndex(),
291                         setNode(subject, next, newNode));
292             }
293             allRelations.putAll(section.getPath(), relations);
294             return bundle.replaceRelations(allRelations);
295         } catch (IndexOutOfBoundsException e) {
296             throw new BundleIndexError(String.format(
297                     "Relation index '%s[%s]' not found", next.current().getPath(),
298                     next.current().getIndex()));
299         }
300     }
301 
302     private static <N extends NestableData<N>> N deleteNode(N bundle, NestableDataPath path) {
303         if (path.getTerminus() == null) {
304             throw new IllegalArgumentException(
305                     "Last component of path must be a valid subtree address.");
306         }
307         if (path.isEmpty()) {
308             throw new IllegalArgumentException("Path must refer to a nested node.");
309         }
310         PathSection section = path.current();
311         NestableDataPath next = path.next();
312 
313         if (!bundle.hasRelations(section.getPath()))
314             throw new BundlePathError(String.format(
315                     "Relation path '%s' not found", section.getPath()));
316         ListMultimap<String, N> allRelations = ArrayListMultimap
317                 .create(bundle.getRelations());
318         try {
319             List<N> relations = Lists.newArrayList(allRelations
320                     .removeAll(section.getPath()));
321             if (next.isEmpty()) {
322                 relations.remove(section.getIndex());
323             } else {
324                 N subject = relations.get(section.getIndex());
325                 relations.set(section.getIndex(),
326                         deleteNode(subject, next));
327             }
328             if (!relations.isEmpty())
329                 allRelations.putAll(section.getPath(), relations);
330             return bundle.replaceRelations(allRelations);
331         } catch (IndexOutOfBoundsException e) {
332             throw new BundleIndexError(String.format(
333                     "Relation index '%s[%s]' not found", section.getPath(),
334                     section.getIndex()));
335         }
336     }
337 }