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