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.Objects;
031import java.util.Set;
032
033import org.nuxeo.ecm.core.api.DataModel;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.model.DocumentPart;
036import org.nuxeo.ecm.core.api.model.Property;
037import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
038import org.nuxeo.ecm.core.api.validation.ConstraintViolation.PathNode;
039import org.nuxeo.ecm.core.schema.DocumentType;
040import org.nuxeo.ecm.core.schema.SchemaManager;
041import org.nuxeo.ecm.core.schema.types.ComplexType;
042import org.nuxeo.ecm.core.schema.types.Field;
043import org.nuxeo.ecm.core.schema.types.ListType;
044import org.nuxeo.ecm.core.schema.types.Schema;
045import org.nuxeo.ecm.core.schema.types.Type;
046import org.nuxeo.ecm.core.schema.types.constraints.Constraint;
047import org.nuxeo.ecm.core.schema.types.constraints.NotNullConstraint;
048import org.nuxeo.runtime.api.Framework;
049import org.nuxeo.runtime.model.ComponentContext;
050import org.nuxeo.runtime.model.ComponentInstance;
051import org.nuxeo.runtime.model.DefaultComponent;
052
053public class DocumentValidationServiceImpl extends DefaultComponent implements DocumentValidationService {
054
055    private SchemaManager schemaManager;
056
057    protected SchemaManager getSchemaManager() {
058        if (schemaManager == null) {
059            schemaManager = Framework.getService(SchemaManager.class);
060        }
061        return schemaManager;
062    }
063
064    private Map<String, Boolean> validationActivations = new HashMap<String, Boolean>();
065
066    @Override
067    public void activate(ComponentContext context) {
068        super.activate(context);
069    }
070
071    @Override
072    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
073        if (extensionPoint.equals("activations")) {
074            DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution;
075            validationActivations.put(dvd.getContext(), dvd.isActivated());
076        }
077    }
078
079    @Override
080    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
081        if (extensionPoint.equals("activations")) {
082            DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution;
083            validationActivations.remove(dvd.getContext());
084        }
085    }
086
087    @Override
088    public boolean isActivated(String context, Map<String, Serializable> contextMap) {
089        if (contextMap != null) {
090            Forcing flag = (Forcing) contextMap.get(DocumentValidationService.CTX_MAP_KEY);
091            if (flag != null) {
092                switch (flag) {
093                case TURN_ON:
094                    return true;
095                case TURN_OFF:
096                    return false;
097                case USUAL:
098                    break;
099                }
100            }
101        }
102        Boolean activated = validationActivations.get(context);
103        if (activated == null) {
104            return false;
105        } else {
106            return activated;
107        }
108    }
109
110    @Override
111    public DocumentValidationReport validate(DocumentModel document) {
112        return validate(document, false);
113    }
114
115    @Override
116    public DocumentValidationReport validate(DocumentModel document, boolean dirtyOnly) {
117        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
118        DocumentType docType = document.getDocumentType();
119        if (dirtyOnly) {
120            for (DataModel dataModel : document.getDataModels().values()) {
121                Schema schemaDef = getSchemaManager().getSchema(dataModel.getSchema());
122                for (String fieldName : dataModel.getDirtyFields()) {
123                    Field field = schemaDef.getField(fieldName);
124                    Property property = document.getProperty(field.getName().getPrefixedName());
125                    List<PathNode> path = Arrays.asList(new PathNode(property.getField()));
126                    violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, true, true));
127                }
128            }
129        } else {
130            for (Schema schema : docType.getSchemas()) {
131                for (Field field : schema.getFields()) {
132                    Property property = document.getProperty(field.getName().getPrefixedName());
133                    List<PathNode> path = Arrays.asList(new PathNode(property.getField()));
134                    violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, false, true));
135                }
136            }
137        }
138        return new DocumentValidationReport(violations);
139    }
140
141    @Override
142    public DocumentValidationReport validate(Field field, Object value) {
143        return validate(field, value, true);
144    }
145
146    @Override
147    public DocumentValidationReport validate(Field field, Object value, boolean validateSubProperties) {
148        Schema schema = field.getDeclaringType().getSchema();
149        return new DocumentValidationReport(validate(schema, field, value, validateSubProperties));
150    }
151
152    @Override
153    public DocumentValidationReport validate(Property property) {
154        return validate(property, true);
155    }
156
157    @Override
158    public DocumentValidationReport validate(Property property, boolean validateSubProperties) {
159        List<PathNode> path = new ArrayList<>();
160        Property inspect = property;
161        while (inspect != null && !(inspect instanceof DocumentPart)) {
162            path.add(0, new PathNode(inspect.getField()));
163            inspect = inspect.getParent();
164        }
165        return new DocumentValidationReport(validateAnyTypeProperty(property.getSchema(), path, property, false,
166                validateSubProperties));
167    }
168
169    @Override
170    public DocumentValidationReport validate(String xpath, Object value) {
171        return validate(xpath, value, true);
172    }
173
174    @Override
175    public DocumentValidationReport validate(String xpath, Object value, boolean validateSubProperties)
176            throws IllegalArgumentException {
177        SchemaManager tm = Framework.getService(SchemaManager.class);
178        List<String> splittedXpath = Arrays.asList(xpath.split("\\/"));
179        List<PathNode> path = new ArrayList<>();
180        Field field = null;
181        String fieldXpath = null;
182        // rebuild the field path
183        for (String xpathToken : splittedXpath) {
184            // manage the list item case
185            if (field != null && field.getType().isListType()) {
186                // get the list field type
187                Field itemField = ((ListType) field.getType()).getField();
188                if (xpathToken.matches("\\d+")) {
189                    // if the current token is an index, append the token and append an indexed PathNode to the path
190                    fieldXpath += "/" + xpathToken;
191                    field = itemField;
192                    int index = Integer.parseInt(xpathToken);
193                    path.add(new PathNode(field, index));
194                } else if (xpathToken.equals(itemField.getName().getLocalName())) {
195                    // if the token is equals to the item's field name
196                    // ignore it on the xpath but append the item's field to the path node
197                    field = itemField;
198                    path.add(new PathNode(field));
199                } else {
200                    // otherwise, the token in an item's element
201                    // append the token and append the item's field and the item's element's field to the path node
202                    fieldXpath += "/" + xpathToken;
203                    field = itemField;
204                    path.add(new PathNode(field));
205                    field = tm.getField(fieldXpath);
206                    if (field == null) {
207                        throw new IllegalArgumentException("Invalid xpath " + fieldXpath);
208                    }
209                    path.add(new PathNode(field));
210                }
211            } else {
212                // in any case, if it's the first item, the token is the path
213                if (fieldXpath == null) {
214                    fieldXpath = xpathToken;
215                } else {
216                    // otherwise, append the token to the existing path
217                    fieldXpath += "/" + xpathToken;
218                }
219                // get the field
220                field = tm.getField(fieldXpath);
221                // check it exists
222                if (field == null) {
223                    throw new IllegalArgumentException("Invalid xpath " + fieldXpath);
224                }
225                // append the pathnode
226                path.add(new PathNode(field));
227            }
228        }
229        Schema schema = field.getDeclaringType().getSchema();
230        return new DocumentValidationReport(validateAnyTypeField(schema, path, field, value, validateSubProperties));
231    }
232
233    // ///////////////////
234    // UTILITY OPERATIONS
235
236    protected List<ConstraintViolation> validate(Schema schema, Field field, Object value, boolean validateSubProperties) {
237        List<PathNode> path = Arrays.asList(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<ConstraintViolation> 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<ConstraintViolation> 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<ConstraintViolation> 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 ConstraintViolation}.
276     *
277     * @since 7.1
278     */
279    private List<ConstraintViolation> 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<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
284        Set<Constraint> constraints = null;
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<ConstraintViolation> validateComplexTypeField(Schema schema, List<PathNode> path, Field field,
306            Object value) {
307        assert field.getType().isComplexType();
308        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
309        ComplexType complexType = (ComplexType) field.getType();
310        // this code does not support other type than Map as value
311        if (value != null && !(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<PathNode>(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<ConstraintViolation> validateListTypeField(Schema schema, List<PathNode> path, Field field,
330            Object value) {
331        assert field.getType().isListType();
332        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
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<PathNode>(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<ConstraintViolation> 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<ConstraintViolation> 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<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
397        Serializable value = prop.getValue();
398        if (prop.isPhantom() || value == null) {
399            if (!field.isNillable()) {
400                addNotNullViolation(violations, schema, path);
401            }
402        } else {
403            violations.addAll(validateSimpleTypeField(schema, path, field, value));
404        }
405        return violations;
406    }
407
408    /**
409     * @since 7.1
410     */
411    private List<ConstraintViolation> validateComplexTypeProperty(Schema schema, List<PathNode> path, Property prop,
412            boolean dirtyOnly) {
413        Field field = prop.getField();
414        assert field.getType().isComplexType();
415        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
416        boolean allChildrenPhantom = true;
417        for (Property child : prop.getChildren()) {
418            if (!child.isPhantom()) {
419                allChildrenPhantom = false;
420                break;
421            }
422        }
423        Object value = prop.getValue();
424        if (prop.isPhantom() || value == null || allChildrenPhantom) {
425            if (!field.isNillable()) {
426                addNotNullViolation(violations, schema, path);
427            }
428        } else {
429            // this code does not support other type than Map as value
430            if (value instanceof Map) {
431                @SuppressWarnings("unchecked")
432                Map<String, Object> castedValue = (Map<String, Object>) value;
433                if (castedValue.isEmpty() || castedValue.values().stream().allMatch(Objects::isNull)) {
434                    if (!field.isNillable()) {
435                        addNotNullViolation(violations, schema, path);
436                    }
437                } else {
438                    for (Property child : prop.getChildren()) {
439                        List<PathNode> subPath = new ArrayList<PathNode>(path);
440                        subPath.add(new PathNode(child.getField()));
441                        violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly, true));
442                    }
443                }
444            }
445        }
446        return violations;
447    }
448
449    /**
450     * @since 7.1
451     */
452    private List<ConstraintViolation> validateListTypeProperty(Schema schema, List<PathNode> path, Property prop,
453            boolean dirtyOnly) {
454        Field field = prop.getField();
455        assert field.getType().isListType();
456        List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>();
457        Serializable value = prop.getValue();
458        if (prop.isPhantom() || value == null) {
459            if (!field.isNillable()) {
460                addNotNullViolation(violations, schema, path);
461            }
462        } else {
463            Collection<?> castedValue = null;
464            if (value instanceof Collection) {
465                castedValue = (Collection<?>) value;
466            } else if (value instanceof Object[]) {
467                castedValue = Arrays.asList((Object[]) value);
468            }
469            if (castedValue != null) {
470                int index = 0;
471                if (prop instanceof ArrayProperty) {
472                    if (!field.isNillable() && castedValue.isEmpty()) {
473                        addNotNullViolation(violations, schema, path);
474                    }
475                    ArrayProperty arrayProp = (ArrayProperty) prop;
476                    // that's an ArrayProperty : there will not be child properties
477                    for (Object itemValue : castedValue) {
478                        if (!dirtyOnly || arrayProp.isDirty(index)) {
479                            List<PathNode> subPath = new ArrayList<PathNode>(path);
480                            subPath.add(new PathNode(field, index));
481                            violations.addAll(validateSimpleTypeField(schema, subPath, field, itemValue));
482                        }
483                        index++;
484                    }
485                } else {
486                    Collection<Property> children = prop.getChildren();
487                    if (!field.isNillable() && children.isEmpty()) {
488                        addNotNullViolation(violations, schema, path);
489                    }
490                    for (Property child : children) {
491                        List<PathNode> subPath = new ArrayList<PathNode>(path);
492                        subPath.add(new PathNode(child.getField(), index));
493                        violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly, true));
494                        index++;
495                    }
496                }
497            }
498        }
499        return violations;
500    }
501
502    // //////
503    // Utils
504
505    private void addNotNullViolation(List<ConstraintViolation> violations, Schema schema, List<PathNode> fieldPath) {
506        NotNullConstraint constraint = NotNullConstraint.get();
507        ConstraintViolation violation = new ConstraintViolation(schema, fieldPath, constraint, null);
508        violations.add(violation);
509    }
510
511}