001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
018 */
019
020package org.nuxeo.ecm.core.api.validation;
021
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032import org.nuxeo.ecm.core.api.DataModel;
033import org.nuxeo.ecm.core.api.DocumentModel;
034import org.nuxeo.ecm.core.api.model.DocumentPart;
035import org.nuxeo.ecm.core.api.model.Property;
036import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
037import org.nuxeo.ecm.core.api.validation.ConstraintViolation.PathNode;
038import org.nuxeo.ecm.core.schema.DocumentType;
039import org.nuxeo.ecm.core.schema.SchemaManager;
040import org.nuxeo.ecm.core.schema.types.ComplexType;
041import org.nuxeo.ecm.core.schema.types.Field;
042import org.nuxeo.ecm.core.schema.types.ListType;
043import org.nuxeo.ecm.core.schema.types.Schema;
044import org.nuxeo.ecm.core.schema.types.Type;
045import org.nuxeo.ecm.core.schema.types.constraints.Constraint;
046import org.nuxeo.ecm.core.schema.types.constraints.NotNullConstraint;
047import org.nuxeo.runtime.api.Framework;
048import org.nuxeo.runtime.model.ComponentContext;
049import org.nuxeo.runtime.model.ComponentInstance;
050import org.nuxeo.runtime.model.DefaultComponent;
051
052public class DocumentValidationServiceImpl extends DefaultComponent implements DocumentValidationService {
053
054    private SchemaManager schemaManager;
055
056    protected SchemaManager getSchemaManager() {
057        if (schemaManager == null) {
058            schemaManager = Framework.getService(SchemaManager.class);
059        }
060        return schemaManager;
061    }
062
063    private Map<String, Boolean> validationActivations = new HashMap<String, Boolean>();
064
065    @Override
066    public void activate(ComponentContext context) {
067        super.activate(context);
068    }
069
070    @Override
071    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
072        if (extensionPoint.equals("activations")) {
073            DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution;
074            validationActivations.put(dvd.getContext(), dvd.isActivated());
075        }
076    }
077
078    @Override
079    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
080        if (extensionPoint.equals("activations")) {
081            DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution;
082            validationActivations.remove(dvd.getContext());
083        }
084    }
085
086    @Override
087    public boolean isActivated(String context, Map<String, Serializable> contextMap) {
088        if (contextMap != null) {
089            Forcing flag = (Forcing) contextMap.get(DocumentValidationService.CTX_MAP_KEY);
090            if (flag != null) {
091                switch (flag) {
092                case TURN_ON:
093                    return true;
094                case TURN_OFF:
095                    return false;
096                case USUAL:
097                    break;
098                }
099            }
100        }
101        Boolean activated = validationActivations.get(context);
102        if (activated == null) {
103            return false;
104        } else {
105            return activated;
106        }
107    }
108
109    @Override
110    public DocumentValidationReport validate(DocumentModel document) {
111        return validate(document, false);
112    }
113
114    @Override
115    public DocumentValidationReport validate(DocumentModel document, boolean dirtyOnly) {
116        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
117        DocumentType docType = document.getDocumentType();
118        if (dirtyOnly) {
119            for (DataModel dataModel : document.getDataModels().values()) {
120                Schema schemaDef = getSchemaManager().getSchema(dataModel.getSchema());
121                for (String fieldName : dataModel.getDirtyFields()) {
122                    Field field = schemaDef.getField(fieldName);
123                    Property property = document.getProperty(field.getName().getPrefixedName());
124                    List<PathNode> path = Arrays.asList(new PathNode(property.getField()));
125                    violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, true, true));
126                }
127            }
128        } else {
129            for (Schema schema : docType.getSchemas()) {
130                for (Field field : schema.getFields()) {
131                    Property property = document.getProperty(field.getName().getPrefixedName());
132                    List<PathNode> path = Arrays.asList(new PathNode(property.getField()));
133                    violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, false, true));
134                }
135            }
136        }
137        return new DocumentValidationReport(violations);
138    }
139
140    @Override
141    public DocumentValidationReport validate(Field field, Object value) {
142        return validate(field, value, true);
143    }
144
145    @Override
146    public DocumentValidationReport validate(Field field, Object value, boolean validateSubProperties) {
147        Schema schema = field.getDeclaringType().getSchema();
148        return new DocumentValidationReport(validate(schema, field, value, validateSubProperties));
149    }
150
151    @Override
152    public DocumentValidationReport validate(Property property) {
153        return validate(property, true);
154    }
155
156    @Override
157    public DocumentValidationReport validate(Property property, boolean validateSubProperties) {
158        List<PathNode> path = new ArrayList<>();
159        Property inspect = property;
160        while (inspect != null && !(inspect instanceof DocumentPart)) {
161            path.add(0, new PathNode(inspect.getField()));
162            inspect = inspect.getParent();
163        }
164        return new DocumentValidationReport(validateAnyTypeProperty(property.getSchema(), path, property, false,
165                validateSubProperties));
166    }
167
168    @Override
169    public DocumentValidationReport validate(String xpath, Object value) {
170        return validate(xpath, value, true);
171    }
172
173    @Override
174    public DocumentValidationReport validate(String xpath, Object value, boolean validateSubProperties)
175            throws IllegalArgumentException {
176        SchemaManager tm = Framework.getService(SchemaManager.class);
177        List<String> splittedXpath = Arrays.asList(xpath.split("\\/"));
178        List<PathNode> path = new ArrayList<>();
179        Field field = null;
180        String fieldXpath = null;
181        // rebuild the field path
182        for (String xpathToken : splittedXpath) {
183            // manage the list item case
184            if (field != null && field.getType().isListType()) {
185                // get the list field type
186                Field itemField = ((ListType) field.getType()).getField();
187                if (xpathToken.matches("\\d+")) {
188                    // if the current token is an index, append the token and append an indexed PathNode to the path
189                    fieldXpath += "/" + xpathToken;
190                    field = itemField;
191                    int index = Integer.parseInt(xpathToken);
192                    path.add(new PathNode(field, index));
193                } else if (xpathToken.equals(itemField.getName().getLocalName())) {
194                    // if the token is equals to the item's field name
195                    // ignore it on the xpath but append the item's field to the path node
196                    field = itemField;
197                    path.add(new PathNode(field));
198                } else {
199                    // otherwise, the token in an item's element
200                    // append the token and append the item's field and the item's element's field to the path node
201                    fieldXpath += "/" + xpathToken;
202                    field = itemField;
203                    path.add(new PathNode(field));
204                    field = tm.getField(fieldXpath);
205                    if (field == null) {
206                        throw new IllegalArgumentException("Invalid xpath " + fieldXpath);
207                    }
208                    path.add(new PathNode(field));
209                }
210            } else {
211                // in any case, if it's the first item, the token is the path
212                if (fieldXpath == null) {
213                    fieldXpath = xpathToken;
214                } else {
215                    // otherwise, append the token to the existing path
216                    fieldXpath += "/" + xpathToken;
217                }
218                // get the field
219                field = tm.getField(fieldXpath);
220                // check it exists
221                if (field == null) {
222                    throw new IllegalArgumentException("Invalid xpath " + fieldXpath);
223                }
224                // append the pathnode
225                path.add(new PathNode(field));
226            }
227        }
228        Schema schema = field.getDeclaringType().getSchema();
229        return new DocumentValidationReport(validateAnyTypeField(schema, path, field, value, validateSubProperties));
230    }
231
232    // ///////////////////
233    // UTILITY OPERATIONS
234
235    protected List<ConstraintViolation> validate(Schema schema, Field field, Object value, boolean validateSubProperties) {
236        List<PathNode> path = Arrays.asList(new PathNode(field));
237        return validateAnyTypeField(schema, path, field, value, validateSubProperties);
238    }
239
240    // ////////////////////////////
241    // Exploration based on Fields
242
243    /**
244     * @since 7.1
245     */
246    @SuppressWarnings("rawtypes")
247    private List<ConstraintViolation> validateAnyTypeField(Schema schema, List<PathNode> path, Field field,
248            Object value, boolean validateSubProperties) {
249        if (field.getType().isSimpleType()) {
250            return validateSimpleTypeField(schema, path, field, value);
251        } else if (field.getType().isComplexType()) {
252            List<ConstraintViolation> res = new ArrayList<>();
253            if (!field.isNillable() && (value == null || (value instanceof Map && ((Map) value).isEmpty()))) {
254                addNotNullViolation(res, schema, path);
255            }
256            if (validateSubProperties) {
257                List<ConstraintViolation> subs = validateComplexTypeField(schema, path, field, value);
258                if (subs != null) {
259                    res.addAll(subs);
260                }
261            }
262            return res;
263        } else if (field.getType().isListType()) {
264            // maybe validate the list type here
265            if (validateSubProperties) {
266                return validateListTypeField(schema, path, field, value);
267            }
268        }
269        // unrecognized type : ignored
270        return Collections.emptyList();
271    }
272
273    /**
274     * This method should be the only one to create {@link ConstraintViolation}.
275     *
276     * @since 7.1
277     */
278    private List<ConstraintViolation> validateSimpleTypeField(Schema schema, List<PathNode> path, Field field,
279            Object value) {
280        Type type = field.getType();
281        assert type.isSimpleType() || type.isListType(); // list type to manage ArrayProperty
282        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
283        Set<Constraint> constraints = null;
284        if (type.isListType()) { // ArrayProperty
285            constraints = ((ListType) type).getFieldType().getConstraints();
286        } else {
287            constraints = field.getConstraints();
288        }
289        for (Constraint constraint : constraints) {
290            if (!constraint.validate(value)) {
291                ConstraintViolation violation = new ConstraintViolation(schema, path, constraint, value);
292                violations.add(violation);
293            }
294        }
295        return violations;
296    }
297
298    /**
299     * Validates sub fields for given complex field.
300     *
301     * @since 7.1
302     */
303    @SuppressWarnings("unchecked")
304    private List<ConstraintViolation> validateComplexTypeField(Schema schema, List<PathNode> path, Field field,
305            Object value) {
306        assert field.getType().isComplexType();
307        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
308        ComplexType complexType = (ComplexType) field.getType();
309        // this code does not support other type than Map as value
310        if (value != null && !(value instanceof Map)) {
311            return violations;
312        }
313        Map<String, Object> map = (Map<String, Object>) value;
314        for (Field child : complexType.getFields()) {
315            Object item = map.get(child.getName().getLocalName());
316            List<PathNode> subPath = new ArrayList<PathNode>(path);
317            subPath.add(new PathNode(child));
318            violations.addAll(validateAnyTypeField(schema, subPath, child, item, true));
319        }
320        return violations;
321    }
322
323    /**
324     * Validates sub fields for given list field.
325     *
326     * @since 7.1
327     */
328    private List<ConstraintViolation> validateListTypeField(Schema schema, List<PathNode> path, Field field,
329            Object value) {
330        assert field.getType().isListType();
331        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
332        Collection<?> castedValue = null;
333        if (value instanceof List) {
334            castedValue = (Collection<?>) value;
335        } else if (value instanceof Object[]) {
336            castedValue = Arrays.asList((Object[]) value);
337        }
338        if (castedValue != null) {
339            ListType listType = (ListType) field.getType();
340            Field listField = listType.getField();
341            int index = 0;
342            for (Object item : castedValue) {
343                List<PathNode> subPath = new ArrayList<PathNode>(path);
344                subPath.add(new PathNode(listField, index));
345                violations.addAll(validateAnyTypeField(schema, subPath, listField, item, true));
346                index++;
347            }
348            return violations;
349        }
350        return violations;
351    }
352
353    // //////////////////////////////
354    // Exploration based on Property
355
356    /**
357     * @since 7.1
358     */
359    private List<ConstraintViolation> validateAnyTypeProperty(Schema schema, List<PathNode> path, Property prop,
360            boolean dirtyOnly, boolean validateSubProperties) {
361        Field field = prop.getField();
362        if (!dirtyOnly || prop.isDirty()) {
363            if (field.getType().isSimpleType()) {
364                return validateSimpleTypeProperty(schema, path, prop, dirtyOnly);
365            } else if (field.getType().isComplexType()) {
366                // ignore for now the case when the complex property is null with a null contraints because it's
367                // currently impossible
368                if (validateSubProperties) {
369                    return validateComplexTypeProperty(schema, path, prop, dirtyOnly);
370                }
371            } else if (field.getType().isListType()) {
372                if (validateSubProperties) {
373                    return validateListTypeProperty(schema, path, prop, dirtyOnly);
374                }
375            }
376        }
377        // unrecognized type : ignored
378        return Collections.emptyList();
379    }
380
381    /**
382     * @since 7.1
383     */
384    private List<ConstraintViolation> validateSimpleTypeProperty(Schema schema, List<PathNode> path, Property prop,
385            boolean dirtyOnly) {
386        Field field = prop.getField();
387        assert field.getType().isSimpleType() || prop.isScalar();
388        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
389        Serializable value = prop.getValue();
390        if (prop.isPhantom() || value == null) {
391            if (!field.isNillable()) {
392                addNotNullViolation(violations, schema, path);
393            }
394        } else {
395            violations.addAll(validateSimpleTypeField(schema, path, field, value));
396        }
397        return violations;
398    }
399
400    /**
401     * @since 7.1
402     */
403    private List<ConstraintViolation> validateComplexTypeProperty(Schema schema, List<PathNode> path, Property prop,
404            boolean dirtyOnly) {
405        Field field = prop.getField();
406        assert field.getType().isComplexType();
407        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
408        boolean allChildrenPhantom = true;
409        for (Property child : prop.getChildren()) {
410            if (!child.isPhantom()) {
411                allChildrenPhantom = false;
412                break;
413            }
414        }
415        Object value = prop.getValue();
416        if (prop.isPhantom() || value == null || allChildrenPhantom) {
417            if (!field.isNillable()) {
418                addNotNullViolation(violations, schema, path);
419            }
420        } else {
421            // this code does not support other type than Map as value
422            if (value instanceof Map) {
423                @SuppressWarnings("unchecked")
424                Map<String, Object> castedValue = (Map<String, Object>) value;
425                if (value == null || castedValue.isEmpty()) {
426                    if (!field.isNillable()) {
427                        addNotNullViolation(violations, schema, path);
428                    }
429                } else {
430                    for (Property child : prop.getChildren()) {
431                        List<PathNode> subPath = new ArrayList<PathNode>(path);
432                        subPath.add(new PathNode(child.getField()));
433                        violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly, true));
434                    }
435                }
436            }
437        }
438        return violations;
439    }
440
441    /**
442     * @since 7.1
443     */
444    private List<ConstraintViolation> validateListTypeProperty(Schema schema, List<PathNode> path, Property prop,
445            boolean dirtyOnly) {
446        Field field = prop.getField();
447        assert field.getType().isListType();
448        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
449        Serializable value = prop.getValue();
450        if (prop.isPhantom() || value == null) {
451            if (!field.isNillable()) {
452                addNotNullViolation(violations, schema, path);
453            }
454        } else {
455            Collection<?> castedValue = null;
456            if (value instanceof Collection) {
457                castedValue = (Collection<?>) value;
458            } else if (value instanceof Object[]) {
459                castedValue = Arrays.asList((Object[]) value);
460            }
461            if (castedValue != null) {
462                int index = 0;
463                if (prop instanceof ArrayProperty) {
464                    ArrayProperty arrayProp = (ArrayProperty) prop;
465                    // that's an ArrayProperty : there will not be child properties
466                    for (Object itemValue : castedValue) {
467                        if (!dirtyOnly || arrayProp.isDirty(index)) {
468                            List<PathNode> subPath = new ArrayList<PathNode>(path);
469                            subPath.add(new PathNode(field, index));
470                            violations.addAll(validateSimpleTypeField(schema, subPath, field, itemValue));
471                        }
472                        index++;
473                    }
474                } else {
475                    for (Property child : prop.getChildren()) {
476                        List<PathNode> subPath = new ArrayList<PathNode>(path);
477                        subPath.add(new PathNode(child.getField(), index));
478                        violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly, true));
479                        index++;
480                    }
481                }
482            }
483        }
484        return violations;
485    }
486
487    // //////
488    // Utils
489
490    private void addNotNullViolation(List<ConstraintViolation> violations, Schema schema, List<PathNode> fieldPath) {
491        NotNullConstraint constraint = NotNullConstraint.get();
492        ConstraintViolation violation = new ConstraintViolation(schema, fieldPath, constraint, null);
493        violations.add(violation);
494    }
495
496}