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.ListMultimap;
23  import com.google.common.collect.Lists;
24  import com.tinkerpop.blueprints.CloseableIterable;
25  import com.tinkerpop.blueprints.Vertex;
26  import eu.ehri.project.core.GraphManager;
27  import eu.ehri.project.exceptions.ValidationError;
28  import eu.ehri.project.models.annotations.EntityType;
29  import eu.ehri.project.models.utils.ClassUtils;
30  
31  import java.text.MessageFormat;
32  import java.util.Collection;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Optional;
36  import java.util.Set;
37  
38  /**
39   * Class responsible for validating bundles.
40   */
41  public final class BundleValidator {
42  
43      private final GraphManager manager;
44      private final List<String> scopes;
45  
46      public BundleValidator(GraphManager manager, Collection<String> scopes) {
47          this.manager = manager;
48          this.scopes = Lists.newArrayList(Optional.ofNullable(scopes)
49                  .orElse(Lists.newArrayList()));
50      }
51  
52      /**
53       * Validate the data in the bundle, according to the target class, and
54       * ensure it is fit for creating in the graph.
55       *
56       * @return A new bundle with generated IDs.
57       */
58      Bundle validateForCreate(Bundle bundle) throws ValidationError {
59          validateData(bundle);
60          Bundle withIds = bundle.generateIds(scopes);
61          ErrorSet createErrors = validateTreeForCreate(withIds);
62          if (!createErrors.isEmpty()) {
63              throw new ValidationError(bundle, createErrors);
64          }
65          return withIds;
66      }
67  
68      /**
69       * Validate the data in the bundle, according to the target class, and
70       * ensure it is fit for updating in the graph.
71       *
72       * @return A new bundle with generated IDs.
73       */
74      Bundle validateForUpdate(Bundle bundle) throws ValidationError {
75          validateData(bundle);
76          Bundle withIds = bundle.generateIds(scopes);
77          ErrorSet updateErrors = validateTreeForUpdate(withIds);
78          if (!updateErrors.isEmpty()) {
79              throw new ValidationError(bundle, updateErrors);
80          }
81          return withIds;
82      }
83  
84      /**
85       * Validate bundle fields. Mandatory fields must be present and non-empty
86       * and entity type annotations must be present in the bundle's entity type class.
87       *
88       * @param bundle a Bundle to validate
89       * @throws ValidationError if any errors were found during validation of the bundle
90       */
91      private void validateData(Bundle bundle) throws ValidationError {
92          ErrorSet es = validateTreeData(bundle);
93          if (!es.isEmpty()) {
94              throw new ValidationError(bundle, es);
95          }
96      }
97  
98      private enum ValidationType {
99          data, create, update
100     }
101 
102     /**
103      * Validate the data in the bundle, according to the target class, and
104      * ensure it is fit for updating in the graph.
105      *
106      * @param bundle the Bundle to validate
107      * @return errors an ErrorSet that is empty when no errors were found,
108      * or containing found errors
109      */
110     private ErrorSet validateTreeForUpdate(Bundle bundle) {
111         ErrorSet.Builder builder = new ErrorSet.Builder();
112         if (bundle.getId() == null)
113             builder.addError(Bundle.ID_KEY, Messages
114                     .getString("BundleValidator.missingIdForUpdate")); //$NON-NLS-1$
115         checkUniquenessOnUpdate(bundle, builder);
116         checkChildren(bundle, builder, ValidationType.update);
117         return builder.build();
118     }
119 
120     /**
121      * Validate the data in the bundle, according to the target class, and
122      * ensure it is fit for creating in the graph.
123      *
124      * @param bundle the Bundle to validate
125      * @return errors an ErrorSet that is empty when no errors were found,
126      * or containing found errors
127      */
128     private ErrorSet validateTreeForCreate(Bundle bundle) {
129         ErrorSet.Builder builder = new ErrorSet.Builder();
130         checkIntegrity(bundle, builder);
131         checkUniqueness(bundle, builder);
132         checkChildren(bundle, builder, ValidationType.create);
133         return builder.build();
134     }
135 
136     /**
137      * Validate bundle fields. Mandatory fields must be present and non-empty
138      * and entity type annotations must be present in the bundle's entity type class.
139      *
140      * @return errors an ErrorSet that may contain errors for missing/empty mandatory fields
141      * and missing entity types
142      */
143     private ErrorSet validateTreeData(Bundle bundle) {
144         ErrorSet.Builder builder = new ErrorSet.Builder();
145         checkFields(bundle, builder);
146         checkEntityType(bundle, builder);
147         checkChildren(bundle, builder, ValidationType.data);
148         return builder.build();
149     }
150 
151     private void checkChildren(Bundle bundle,
152             ErrorSet.Builder builder, ValidationType type) {
153         for (Map.Entry<String, Bundle> entry : bundle.getDependentRelations().entries()) {
154             switch (type) {
155                 case data:
156                     builder.addRelation(entry.getKey(), validateTreeData(entry.getValue()));
157                     break;
158                 case create:
159                     builder.addRelation(entry.getKey(), validateTreeForCreate(entry.getValue()));
160                     break;
161                 case update:
162                     builder.addRelation(entry.getKey(), validateTreeForUpdate(entry.getValue()));
163                     break;
164             }
165         }
166     }
167 
168     /**
169      * Check a bundle's mandatory fields are present and not empty. Add errors to the builder's ErrorSet.
170      */
171     private static void checkFields(Bundle bundle, ErrorSet.Builder builder) {
172         for (String key : ClassUtils.getMandatoryPropertyKeys(bundle.getBundleJavaClass())) {
173             checkField(bundle, builder, key);
174         }
175         Map<String, Set<String>> enumPropertyKeys = ClassUtils.getEnumPropertyKeys(bundle.getBundleJavaClass());
176         for (Map.Entry<String, Set<String>> entry : enumPropertyKeys.entrySet()) {
177             checkValueInRange(bundle, builder, entry.getKey(), entry.getValue());
178         }
179     }
180 
181     private static void checkValueInRange(Bundle bundle, ErrorSet.Builder builder, String key, Collection<String> values) {
182         Object dataValue = bundle.getDataValue(key);
183         if (dataValue != null) {
184             if (!values.contains(dataValue.toString())) {
185                 builder.addError(key,
186                         MessageFormat.format(Messages.getString("BundleValidator.invalidFieldValue"), values, dataValue));
187             }
188         }
189     }
190 
191     /**
192      * Check the data holds a given field and that the field is not empty.
193      *
194      * @param name The field name
195      */
196     private static void checkField(Bundle bundle, ErrorSet.Builder builder, String name) {
197         if (!bundle.getData().containsKey(name)) {
198             builder.addError(name, Messages.getString("BundleValidator.missingField"));
199         } else {
200             Object value = bundle.getData().get(name);
201             if (value == null) {
202                 builder.addError(name, Messages.getString("BundleValidator.emptyField"));
203             } else if (value instanceof String) {
204                 if (((String) value).trim().isEmpty()) {
205                     builder.addError(name, Messages.getString("BundleValidator.emptyField"));
206                 }
207             }
208         }
209     }
210 
211     /**
212      * Check that the entity type annotation is present in the bundle's class.
213      */
214     private static void checkEntityType(Bundle bundle, ErrorSet.Builder builder) {
215         EntityType annotation = bundle.getBundleJavaClass().getAnnotation(EntityType.class);
216         if (annotation == null) {
217             builder.addError(Bundle.TYPE_KEY, MessageFormat.format(Messages
218                             .getString("BundleValidator.missingTypeAnnotation"), //$NON-NLS-1$
219                     bundle.getBundleJavaClass().getName()));
220         }
221     }
222 
223     /**
224      * Check uniqueness constraints for a bundle's fields. ID must be present and not existing in
225      * the graph.
226      */
227     private void checkIntegrity(Bundle bundle, ErrorSet.Builder builder) {
228         if (bundle.getId() == null) {
229             builder.addError("id", MessageFormat.format(Messages.getString("BundleValidator.missingIdForCreate"),
230                     bundle.getId()));
231         }
232         if (manager.exists(bundle.getId())) {
233             ListMultimap<String, String> idErrors = bundle
234                     .getType().getIdGen().handleIdCollision(scopes, bundle);
235             for (Map.Entry<String, String> entry : idErrors.entries()) {
236                 builder.addError(entry.getKey(), entry.getValue());
237             }
238         }
239     }
240 
241     /**
242      * Check uniqueness constraints for a bundle's fields.
243      */
244     private void checkUniqueness(Bundle bundle, ErrorSet.Builder builder) {
245         for (String ukey : bundle.getUniquePropertyKeys()) {
246             Object uval = bundle.getDataValue(ukey);
247             if (uval != null) {
248                 try (CloseableIterable<Vertex> vertices = manager.getVertices(ukey, uval, bundle.getType())) {
249                     if (vertices.iterator().hasNext()) {
250                         builder.addError(ukey, MessageFormat.format(Messages
251                                 .getString("BundleValidator.uniquenessError"), uval));
252                     }
253                 }
254             }
255         }
256     }
257 
258     /**
259      * Check uniqueness constraints for a bundle's fields: add errors for node references whose referent is
260      * not already in the graph.
261      */
262     private void checkUniquenessOnUpdate(Bundle bundle, ErrorSet.Builder builder) {
263         for (String ukey : bundle.getUniquePropertyKeys()) {
264             Object uval = bundle.getDataValue(ukey);
265             if (uval != null) {
266                 try (CloseableIterable<Vertex> vertices = manager.getVertices(ukey, uval, bundle.getType())) {
267                     if (vertices.iterator().hasNext()) {
268                         Vertex v = vertices.iterator().next();
269                         // If it's the same vertex, we don't have a problem...
270                         if (!manager.getId(v).equals(bundle.getId())) {
271                             builder.addError(ukey, MessageFormat.format(Messages
272                                     .getString("BundleValidator.uniquenessError"), uval));
273 
274                         }
275                     }
276                 }
277             }
278         }
279     }
280 }