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.utils.fixtures.impl;
21  
22  import com.google.common.collect.ArrayListMultimap;
23  import com.google.common.collect.ImmutableMap;
24  import com.google.common.collect.Lists;
25  import com.google.common.collect.Maps;
26  import com.google.common.collect.Multimap;
27  import com.tinkerpop.blueprints.Direction;
28  import com.tinkerpop.blueprints.Vertex;
29  import com.tinkerpop.frames.FramedGraph;
30  import eu.ehri.project.core.GraphManager;
31  import eu.ehri.project.core.GraphManagerFactory;
32  import eu.ehri.project.exceptions.DeserializationError;
33  import eu.ehri.project.exceptions.ValidationError;
34  import eu.ehri.project.models.EntityClass;
35  import eu.ehri.project.models.base.Entity;
36  import eu.ehri.project.persistence.Bundle;
37  import eu.ehri.project.persistence.BundleManager;
38  import eu.ehri.project.persistence.Mutation;
39  import eu.ehri.project.utils.GraphInitializer;
40  import eu.ehri.project.utils.fixtures.FixtureLoader;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  import org.yaml.snakeyaml.Yaml;
44  
45  import java.io.IOException;
46  import java.io.InputStream;
47  import java.nio.file.Files;
48  import java.nio.file.Path;
49  import java.nio.file.Paths;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Map.Entry;
53  
54  /**
55   * Load data from YAML fixtures.
56   * <p>
57   * The YAML fixture format is almost identical to the plain bundle format, but
58   * has some extensions to a) allow for creating non-dependent relationships, and
59   * b) allow for single relations to be more naturally expressed. For example,
60   * while, in the bundle format the relations for a given relation type is always
61   * a list (even if there is typically only one), the YAML format allows using a
62   * single item and it will be loaded as if it were a list containing just one
63   * item, i.e, instead of writing
64   * <p>
65   * <pre>
66   *     <code>
67   * relationships: heldBy: - some-repo
68   *     </code>
69   * </pre>
70   * <p>
71   * we can just write:
72   * <p>
73   * <pre>
74   *     <code>
75   * relationships: heldBy: some-repo
76   *     </code>
77   * </pre>
78   */
79  public class YamlFixtureLoader implements FixtureLoader {
80  
81      private static final boolean DEFAULT_INIT = true;
82      private static final String GENERATE_ID_PLACEHOLDER = "?";
83      private static final String DEFAULT_FIXTURE_FILE = "testdata.yaml";
84  
85      private static final Logger logger = LoggerFactory.getLogger(YamlFixtureLoader.class);
86  
87      private final FramedGraph<?> graph;
88      private final GraphManager manager;
89      private final BundleManager dao;
90      private final boolean initialize;
91      private final GraphInitializer initializer;
92  
93      /**
94       * Constructor
95       *
96       * @param graph      The graph
97       * @param initialize Whether or not to initialize the graph
98       */
99      public YamlFixtureLoader(FramedGraph<?> graph, boolean initialize) {
100         this.graph = graph;
101         this.initialize = initialize;
102         manager = GraphManagerFactory.getInstance(graph);
103         dao = new BundleManager(graph);
104         initializer = new GraphInitializer(graph);
105     }
106 
107     /**
108      * Constructor.
109      *
110      * @param graph The graph
111      */
112     public YamlFixtureLoader(FramedGraph<?> graph) {
113         this(graph, DEFAULT_INIT);
114     }
115 
116     /**
117      * Perform graph initialization (creating the event log structure and
118      * default nodes) prior to importing fixtures.
119      *
120      * @param initialize Whether or not to initialize the graph: default
121      *                   {@value YamlFixtureLoader#DEFAULT_INIT}
122      */
123     public YamlFixtureLoader setInitializing(boolean initialize) {
124         return new YamlFixtureLoader(graph, initialize);
125     }
126 
127     /**
128      * Load default fixtures.
129      */
130     private void loadFixtures() {
131         try (InputStream ios = this.getClass().getClassLoader()
132                 .getResourceAsStream(DEFAULT_FIXTURE_FILE)) {
133             loadTestData(ios);
134         } catch (IOException e) {
135             throw new RuntimeException(e);
136         }
137     }
138 
139     /**
140      * Load fixtures from a resource or file path.
141      *
142      * @param resourceNameOrPath Either a classloader-accessible
143      *                           resource, or a local file path.
144      */
145     public void loadTestData(String resourceNameOrPath) {
146         Path path = Paths.get(resourceNameOrPath);
147         try (InputStream stream = Files.isRegularFile(path)
148                 ? Files.newInputStream(path)
149                 : this.getClass().getClassLoader()
150                 .getResourceAsStream(resourceNameOrPath)) {
151             loadTestData(stream);
152         } catch (IOException e) {
153             throw new RuntimeException(e);
154         }
155     }
156 
157     /**
158      * Load test data from an input stream.
159      *
160      * @param stream A input stream of valid YAML data.
161      */
162     public void loadTestData(InputStream stream) {
163         // Initialize the DB
164         try {
165             if (initialize) {
166                 initializer.initialize();
167             }
168             loadFixtureFileStream(stream);
169         } catch (Exception e) {
170             throw new RuntimeException(e);
171         }
172     }
173 
174     @SuppressWarnings("unchecked")
175     private void loadFixtureFileStream(InputStream yamlStream) {
176         Yaml yaml = new Yaml();
177         try {
178             Map<Vertex, Multimap<String, String>> links = Maps.newHashMap();
179             for (Object data : yaml.loadAll(yamlStream)) {
180                 for (Object node : (List<?>) data) {
181                     if (node instanceof Map) {
182                         logger.trace("Importing node: {}", node);
183                         importNode(links, (Map<String, Object>) node);
184                     }
185                 }
186             }
187 
188             // Finally, go through and wire up all the non-dependent
189             // relationships
190             logger.trace("Linking data...");
191             for (Entry<Vertex, Multimap<String, String>> entry : links.entrySet()) {
192                 logger.trace("Setting links for: {}", entry.getKey());
193                 Vertex src = entry.getKey();
194                 Multimap<String, String> rels = entry.getValue();
195                 for (String relname : rels.keySet()) {
196                     for (String target : rels.get(relname)) {
197                         Vertex dst = manager.getVertex(target);
198                         addRelationship(src, dst, relname);
199                     }
200                 }
201             }
202         } catch (Exception e) {
203             throw new RuntimeException("Error loading YAML fixture", e);
204         }
205     }
206 
207     private void addRelationship(Vertex src, Vertex dst, String relname) {
208         boolean found = false;
209         for (Vertex v : src.getVertices(Direction.OUT, relname)) {
210             if (v.equals(dst)) {
211                 found = true;
212                 break;
213             }
214         }
215         if (!found) {
216             logger.trace(String.format(" - %s -[%s]-> %s", src, dst, relname));
217             graph.addEdge(null, src, dst, relname);
218         }
219     }
220 
221     private void importNode(Map<Vertex, Multimap<String, String>> links,
222             Map<String, Object> node) throws DeserializationError, ValidationError {
223         EntityClass isa = EntityClass.withName((String) node
224                 .get(Bundle.TYPE_KEY));
225 
226         String id = (String) node.get(Bundle.ID_KEY);
227 
228         @SuppressWarnings("unchecked")
229         Map<String, Object> nodeData = (Map<String, Object>) node
230                 .get(Bundle.DATA_KEY);
231         if (nodeData == null) {
232             nodeData = Maps.newHashMap();
233         }
234         @SuppressWarnings("unchecked")
235         Map<String, Object> nodeRels = (Map<String, Object>) node
236                 .get(Bundle.REL_KEY);
237 
238         // Since our data is written as a subgraph, we can use the
239         // bundle converter to load it.
240         Bundle entityBundle = createBundle(id, isa, nodeData,
241                 getDependentRelations(nodeRels));
242         logger.trace("Creating node with id: {}", id);
243         Mutation<Entity> frame = dao.createOrUpdate(entityBundle, Entity.class);
244 
245         Multimap<String, String> linkRels = getLinkedRelations(nodeRels);
246         if (!linkRels.isEmpty()) {
247             links.put(frame.getNode().asVertex(), linkRels);
248         }
249     }
250 
251     private Bundle createBundle(String id, EntityClass type,
252             Map<String, Object> nodeData,
253             Multimap<String, Map<?, ?>> dependentRelations) throws DeserializationError {
254         Map<String, Object> data = ImmutableMap.of(
255                 Bundle.ID_KEY, id,
256                 Bundle.TYPE_KEY, type.getName(),
257                 Bundle.DATA_KEY, nodeData,
258                 Bundle.REL_KEY, dependentRelations.asMap()
259         );
260         Bundle b = Bundle.fromData(data);
261 
262         // If the given id is a placeholder, generate it according to type rules
263         if (id.trim().contentEquals(GENERATE_ID_PLACEHOLDER)) {
264             String newId = type.getIdGen().generateId(Lists.<String>newArrayList(), b);
265             b = b.withId(newId);
266         }
267         return b;
268     }
269 
270     private Multimap<String, String> getLinkedRelations(Map<String, Object> data) {
271         Multimap<String, String> rels = ArrayListMultimap.create();
272         if (data != null) {
273             for (Entry<String, Object> entry : data.entrySet()) {
274                 String relName = entry.getKey();
275                 Object relValue = entry.getValue();
276                 if (relValue instanceof List) {
277                     for (Object relation : (List<?>) relValue) {
278                         if (relation instanceof String) {
279                             rels.put(relName, (String) relation);
280                         }
281                     }
282                 } else if (relValue instanceof String) {
283                     rels.put(relName, (String) relValue);
284                 }
285             }
286         }
287         return rels;
288     }
289 
290     private Multimap<String, Map<?, ?>> getDependentRelations(Map<String, Object> data) {
291         Multimap<String, Map<?, ?>> rels = ArrayListMultimap.create();
292         if (data != null) {
293             for (Entry<String, Object> entry : data.entrySet()) {
294                 String relName = entry.getKey();
295                 Object relValue = entry.getValue();
296                 if (relValue instanceof List) {
297                     for (Object relation : (List<?>) relValue) {
298                         if (relation instanceof Map) {
299                             rels.put(relName, (Map<?, ?>) relation);
300                         }
301                     }
302                 } else if (relValue instanceof Map) {
303                     rels.put(relName, (Map<?, ?>) relValue);
304                 }
305             }
306         }
307         return rels;
308     }
309 
310     public void loadTestData() {
311         loadFixtures();
312     }
313 }