001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
016 */
017
018package org.nuxeo.ecm.core.api.validation;
019
020import java.io.Serializable;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029
030import org.nuxeo.ecm.core.api.DataModel;
031import org.nuxeo.ecm.core.api.DocumentModel;
032import org.nuxeo.ecm.core.api.model.Property;
033import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
034import org.nuxeo.ecm.core.api.validation.ConstraintViolation.PathNode;
035import org.nuxeo.ecm.core.schema.DocumentType;
036import org.nuxeo.ecm.core.schema.SchemaManager;
037import org.nuxeo.ecm.core.schema.types.ComplexType;
038import org.nuxeo.ecm.core.schema.types.Field;
039import org.nuxeo.ecm.core.schema.types.ListType;
040import org.nuxeo.ecm.core.schema.types.Schema;
041import org.nuxeo.ecm.core.schema.types.Type;
042import org.nuxeo.ecm.core.schema.types.constraints.Constraint;
043import org.nuxeo.ecm.core.schema.types.constraints.NotNullConstraint;
044import org.nuxeo.runtime.api.Framework;
045import org.nuxeo.runtime.model.ComponentContext;
046import org.nuxeo.runtime.model.ComponentInstance;
047import org.nuxeo.runtime.model.DefaultComponent;
048
049public class DocumentValidationServiceImpl extends DefaultComponent implements DocumentValidationService {
050
051    private SchemaManager schemaManager;
052
053    protected SchemaManager getSchemaManager() {
054        if (schemaManager == null) {
055            schemaManager = Framework.getService(SchemaManager.class);
056        }
057        return schemaManager;
058    }
059
060    private Map<String, Boolean> validationActivations = new HashMap<String, Boolean>();
061
062    @Override
063    public void activate(ComponentContext context) {
064        super.activate(context);
065    }
066
067    @Override
068    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
069        if (extensionPoint.equals("activations")) {
070            DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution;
071            validationActivations.put(dvd.getContext(), dvd.isActivated());
072        }
073    }
074
075    @Override
076    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
077        if (extensionPoint.equals("activations")) {
078            DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution;
079            validationActivations.remove(dvd.getContext());
080        }
081    }
082
083    @Override
084    public boolean isActivated(String context, Map<String, Serializable> contextMap) {
085        if (contextMap != null) {
086            Forcing flag = (Forcing) contextMap.get(DocumentValidationService.CTX_MAP_KEY);
087            if (flag != null) {
088                switch (flag) {
089                case TURN_ON:
090                    return true;
091                case TURN_OFF:
092                    return false;
093                case USUAL:
094                    break;
095                }
096            }
097        }
098        Boolean activated = validationActivations.get(context);
099        if (activated == null) {
100            return false;
101        } else {
102            return activated;
103        }
104    }
105
106    @Override
107    public DocumentValidationReport validate(DocumentModel document) {
108        return validate(document, false);
109    }
110
111    @Override
112    public DocumentValidationReport validate(DocumentModel document, boolean dirtyOnly) {
113        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
114        DocumentType docType = document.getDocumentType();
115        if (dirtyOnly) {
116            for (DataModel dataModel : document.getDataModels().values()) {
117                Schema schemaDef = getSchemaManager().getSchema(dataModel.getSchema());
118                for (String fieldName : dataModel.getDirtyFields()) {
119                    Field field = schemaDef.getField(fieldName);
120                    Property property = document.getProperty(field.getName().getPrefixedName());
121                    List<PathNode> path = Arrays.asList(new PathNode(property.getField()));
122                    violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, true));
123                }
124            }
125        } else {
126            for (Schema schema : docType.getSchemas()) {
127                for (Field field : schema.getFields()) {
128                    Property property = document.getProperty(field.getName().getPrefixedName());
129                    List<PathNode> path = Arrays.asList(new PathNode(property.getField()));
130                    violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, false));
131                }
132            }
133        }
134        return new DocumentValidationReport(violations);
135    }
136
137    @Override
138    public DocumentValidationReport validate(Field field, Object value) {
139        Schema schema = field.getDeclaringType().getSchema();
140        return new DocumentValidationReport(validate(schema, field, value, true));
141    }
142
143    @Override
144    public DocumentValidationReport validate(Field field, Object value, boolean validateSubProperties) {
145        Schema schema = field.getDeclaringType().getSchema();
146        return new DocumentValidationReport(validate(schema, field, value, validateSubProperties));
147    }
148
149    @Override
150    public DocumentValidationReport validate(Property property) {
151        List<PathNode> path = Arrays.asList(new PathNode(property.getField()));
152        return new DocumentValidationReport(validateAnyTypeProperty(property.getSchema(), path, property, false));
153    }
154
155    @Override
156    public DocumentValidationReport validate(String xpath, Object value) throws IllegalArgumentException {
157        SchemaManager tm = Framework.getService(SchemaManager.class);
158        Field field = tm.getField(xpath);
159        if (field == null) {
160            throw new IllegalArgumentException("Invalid xpath " + xpath);
161        }
162        return new DocumentValidationReport(validate(field.getDeclaringType().getSchema(), field, value, true));
163    }
164
165    // ///////////////////
166    // UTILITY OPERATIONS
167
168    protected List<ConstraintViolation> validate(Schema schema, Field field, Object value, boolean validateSubProperties) {
169        List<PathNode> path = Arrays.asList(new PathNode(field));
170        return validateAnyTypeField(schema, path, field, value, validateSubProperties);
171    }
172
173    // ////////////////////////////
174    // Exploration based on Fields
175
176    /**
177     * @since 7.1
178     */
179    @SuppressWarnings("rawtypes")
180    private List<ConstraintViolation> validateAnyTypeField(Schema schema, List<PathNode> path, Field field,
181            Object value, boolean validateSubProperties) {
182        if (field.getType().isSimpleType()) {
183            return validateSimpleTypeField(schema, path, field, value);
184        } else if (field.getType().isComplexType()) {
185            List<ConstraintViolation> res = new ArrayList<>();
186            if (!field.isNillable() && (value == null || (value instanceof Map && ((Map) value).isEmpty()))) {
187                addNotNullViolation(res, schema, path);
188            }
189            if (validateSubProperties) {
190                List<ConstraintViolation> subs = validateComplexTypeField(schema, path, field, value);
191                if (subs != null) {
192                    res.addAll(subs);
193                }
194            }
195            return res;
196        } else if (field.getType().isListType()) {
197            // maybe validate the list type here
198            if (validateSubProperties) {
199                return validateListTypeField(schema, path, field, value);
200            }
201        }
202        // unrecognized type : ignored
203        return Collections.emptyList();
204    }
205
206    /**
207     * This method should be the only one to create {@link ConstraintViolation}.
208     *
209     * @since 7.1
210     */
211    private List<ConstraintViolation> validateSimpleTypeField(Schema schema, List<PathNode> path, Field field,
212            Object value) {
213        Type type = field.getType();
214        assert type.isSimpleType() || type.isListType(); // list type to manage ArrayProperty
215        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
216        Set<Constraint> constraints = null;
217        if (type.isListType()) { // ArrayProperty
218            constraints = ((ListType) type).getFieldType().getConstraints();
219        } else {
220            constraints = field.getConstraints();
221        }
222        for (Constraint constraint : constraints) {
223            if (!constraint.validate(value)) {
224                ConstraintViolation violation = new ConstraintViolation(schema, path, constraint, value);
225                violations.add(violation);
226            }
227        }
228        return violations;
229    }
230
231    /**
232     * Validates sub fields for given complex field.
233     *
234     * @since 7.1
235     */
236    @SuppressWarnings("unchecked")
237    private List<ConstraintViolation> validateComplexTypeField(Schema schema, List<PathNode> path, Field field,
238            Object value) {
239        assert field.getType().isComplexType();
240        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
241        ComplexType complexType = (ComplexType) field.getType();
242        // this code does not support other type than Map as value
243        if (value != null && !(value instanceof Map)) {
244            return violations;
245        }
246        Map<String, Object> map = (Map<String, Object>) value;
247        for (Field child : complexType.getFields()) {
248            Object item = map.get(child.getName().getLocalName());
249            List<PathNode> subPath = new ArrayList<PathNode>(path);
250            subPath.add(new PathNode(child));
251            violations.addAll(validateAnyTypeField(schema, subPath, child, item, true));
252        }
253        return violations;
254    }
255
256    /**
257     * Validates sub fields for given list field.
258     *
259     * @since 7.1
260     */
261    private List<ConstraintViolation> validateListTypeField(Schema schema, List<PathNode> path, Field field,
262            Object value) {
263        assert field.getType().isListType();
264        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
265        Collection<?> castedValue = null;
266        if (value instanceof List) {
267            castedValue = (Collection<?>) value;
268        } else if (value instanceof Object[]) {
269            castedValue = Arrays.asList((Object[]) value);
270        }
271        if (castedValue != null) {
272            ListType listType = (ListType) field.getType();
273            Field listField = listType.getField();
274            int index = 0;
275            for (Object item : castedValue) {
276                List<PathNode> subPath = new ArrayList<PathNode>(path);
277                subPath.add(new PathNode(listField, index));
278                violations.addAll(validateAnyTypeField(schema, subPath, listField, item, true));
279                index++;
280            }
281            return violations;
282        }
283        return violations;
284    }
285
286    // //////////////////////////////
287    // Exploration based on Property
288
289    /**
290     * @since 7.1
291     */
292    private List<ConstraintViolation> validateAnyTypeProperty(Schema schema, List<PathNode> path, Property prop,
293            boolean dirtyOnly) {
294        Field field = prop.getField();
295        if (!dirtyOnly || prop.isDirty()) {
296            if (field.getType().isSimpleType()) {
297                return validateSimpleTypeProperty(schema, path, prop, dirtyOnly);
298            } else if (field.getType().isComplexType()) {
299                return validateComplexTypeProperty(schema, path, prop, dirtyOnly);
300            } else if (field.getType().isListType()) {
301                return validateListTypeProperty(schema, path, prop, dirtyOnly);
302            }
303        }
304        // unrecognized type : ignored
305        return Collections.emptyList();
306    }
307
308    /**
309     * @since 7.1
310     */
311    private List<ConstraintViolation> validateSimpleTypeProperty(Schema schema, List<PathNode> path, Property prop,
312            boolean dirtyOnly) {
313        Field field = prop.getField();
314        assert field.getType().isSimpleType() || prop.isScalar();
315        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
316        Serializable value = prop.getValue();
317        if (prop.isPhantom() || value == null) {
318            if (!field.isNillable()) {
319                addNotNullViolation(violations, schema, path);
320            }
321        } else {
322            violations.addAll(validateSimpleTypeField(schema, path, field, value));
323        }
324        return violations;
325    }
326
327    /**
328     * @since 7.1
329     */
330    private List<ConstraintViolation> validateComplexTypeProperty(Schema schema, List<PathNode> path, Property prop,
331            boolean dirtyOnly) {
332        Field field = prop.getField();
333        assert field.getType().isComplexType();
334        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
335        boolean allChildrenPhantom = true;
336        for (Property child : prop.getChildren()) {
337            if (!child.isPhantom()) {
338                allChildrenPhantom = false;
339                break;
340            }
341        }
342        Object value = prop.getValue();
343        if (prop.isPhantom() || value == null || allChildrenPhantom) {
344            if (!field.isNillable()) {
345                addNotNullViolation(violations, schema, path);
346            }
347        } else {
348            // this code does not support other type than Map as value
349            if (value instanceof Map) {
350                @SuppressWarnings("unchecked")
351                Map<String, Object> castedValue = (Map<String, Object>) value;
352                if (value == null || castedValue.isEmpty()) {
353                    if (!field.isNillable()) {
354                        addNotNullViolation(violations, schema, path);
355                    }
356                } else {
357                    for (Property child : prop.getChildren()) {
358                        List<PathNode> subPath = new ArrayList<PathNode>(path);
359                        subPath.add(new PathNode(child.getField()));
360                        violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly));
361                    }
362                }
363            }
364        }
365        return violations;
366    }
367
368    /**
369     * @since 7.1
370     */
371    private List<ConstraintViolation> validateListTypeProperty(Schema schema, List<PathNode> path, Property prop,
372            boolean dirtyOnly) {
373        Field field = prop.getField();
374        assert field.getType().isListType();
375        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
376        Serializable value = prop.getValue();
377        if (prop.isPhantom() || value == null) {
378            if (!field.isNillable()) {
379                addNotNullViolation(violations, schema, path);
380            }
381        } else {
382            Collection<?> castedValue = null;
383            if (value instanceof Collection) {
384                castedValue = (Collection<?>) value;
385            } else if (value instanceof Object[]) {
386                castedValue = Arrays.asList((Object[]) value);
387            }
388            if (castedValue != null) {
389                int index = 0;
390                if (prop instanceof ArrayProperty) {
391                    ArrayProperty arrayProp = (ArrayProperty) prop;
392                    // that's an ArrayProperty : there will not be child properties
393                    for (Object itemValue : castedValue) {
394                        if (!dirtyOnly || arrayProp.isDirty(index)) {
395                            List<PathNode> subPath = new ArrayList<PathNode>(path);
396                            subPath.add(new PathNode(field, index));
397                            violations.addAll(validateSimpleTypeField(schema, subPath, field, itemValue));
398                            index++;
399                        }
400                    }
401                } else {
402                    for (Property child : prop.getChildren()) {
403                        List<PathNode> subPath = new ArrayList<PathNode>(path);
404                        subPath.add(new PathNode(child.getField(), index));
405                        violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly));
406                        index++;
407                    }
408                }
409            }
410        }
411        return violations;
412    }
413
414    // //////
415    // Utils
416
417    private void addNotNullViolation(List<ConstraintViolation> violations, Schema schema, List<PathNode> fieldPath) {
418        NotNullConstraint constraint = NotNullConstraint.get();
419        ConstraintViolation violation = new ConstraintViolation(schema, fieldPath, constraint, null);
420        violations.add(violation);
421    }
422
423}