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