/* * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Nicolas Chapurlat <nchapurlat@nuxeo.com> */ package org.nuxeo.ecm.core.api.validation; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.nuxeo.ecm.core.api.DataModel; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.model.DocumentPart; import org.nuxeo.ecm.core.api.model.Property; import org.nuxeo.ecm.core.api.model.impl.ArrayProperty; import org.nuxeo.ecm.core.api.validation.ConstraintViolation.PathNode; import org.nuxeo.ecm.core.schema.DocumentType; import org.nuxeo.ecm.core.schema.SchemaManager; import org.nuxeo.ecm.core.schema.types.ComplexType; import org.nuxeo.ecm.core.schema.types.Field; import org.nuxeo.ecm.core.schema.types.ListType; import org.nuxeo.ecm.core.schema.types.Schema; import org.nuxeo.ecm.core.schema.types.Type; import org.nuxeo.ecm.core.schema.types.constraints.Constraint; import org.nuxeo.ecm.core.schema.types.constraints.NotNullConstraint; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.ComponentContext; import org.nuxeo.runtime.model.ComponentInstance; import org.nuxeo.runtime.model.DefaultComponent; public class DocumentValidationServiceImpl extends DefaultComponent implements DocumentValidationService { private SchemaManager schemaManager; protected SchemaManager getSchemaManager() { if (schemaManager == null) { schemaManager = Framework.getService(SchemaManager.class); } return schemaManager; } private Map<String, Boolean> validationActivations = new HashMap<String, Boolean>(); @Override public void activate(ComponentContext context) { super.activate(context); } @Override public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (extensionPoint.equals("activations")) { DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution; validationActivations.put(dvd.getContext(), dvd.isActivated()); } } @Override public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (extensionPoint.equals("activations")) { DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution; validationActivations.remove(dvd.getContext()); } } @Override public boolean isActivated(String context, Map<String, Serializable> contextMap) { if (contextMap != null) { Forcing flag = (Forcing) contextMap.get(DocumentValidationService.CTX_MAP_KEY); if (flag != null) { switch (flag) { case TURN_ON: return true; case TURN_OFF: return false; case USUAL: break; } } } Boolean activated = validationActivations.get(context); if (activated == null) { return false; } else { return activated; } } @Override public DocumentValidationReport validate(DocumentModel document) { return validate(document, false); } @Override public DocumentValidationReport validate(DocumentModel document, boolean dirtyOnly) { List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>(); DocumentType docType = document.getDocumentType(); if (dirtyOnly) { for (DataModel dataModel : document.getDataModels().values()) { Schema schemaDef = getSchemaManager().getSchema(dataModel.getSchema()); for (String fieldName : dataModel.getDirtyFields()) { Field field = schemaDef.getField(fieldName); Property property = document.getProperty(field.getName().getPrefixedName()); List<PathNode> path = Arrays.asList(new PathNode(property.getField())); violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, true, true)); } } } else { for (Schema schema : docType.getSchemas()) { for (Field field : schema.getFields()) { Property property = document.getProperty(field.getName().getPrefixedName()); List<PathNode> path = Arrays.asList(new PathNode(property.getField())); violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, false, true)); } } } return new DocumentValidationReport(violations); } @Override public DocumentValidationReport validate(Field field, Object value) { return validate(field, value, true); } @Override public DocumentValidationReport validate(Field field, Object value, boolean validateSubProperties) { Schema schema = field.getDeclaringType().getSchema(); return new DocumentValidationReport(validate(schema, field, value, validateSubProperties)); } @Override public DocumentValidationReport validate(Property property) { return validate(property, true); } @Override public DocumentValidationReport validate(Property property, boolean validateSubProperties) { List<PathNode> path = new ArrayList<>(); Property inspect = property; while (inspect != null && !(inspect instanceof DocumentPart)) { path.add(0, new PathNode(inspect.getField())); inspect = inspect.getParent(); } return new DocumentValidationReport(validateAnyTypeProperty(property.getSchema(), path, property, false, validateSubProperties)); } @Override public DocumentValidationReport validate(String xpath, Object value) { return validate(xpath, value, true); } @Override public DocumentValidationReport validate(String xpath, Object value, boolean validateSubProperties) throws IllegalArgumentException { SchemaManager tm = Framework.getService(SchemaManager.class); List<String> splittedXpath = Arrays.asList(xpath.split("\\/")); List<PathNode> path = new ArrayList<>(); Field field = null; String fieldXpath = null; // rebuild the field path for (String xpathToken : splittedXpath) { // manage the list item case if (field != null && field.getType().isListType()) { // get the list field type Field itemField = ((ListType) field.getType()).getField(); if (xpathToken.matches("\\d+")) { // if the current token is an index, append the token and append an indexed PathNode to the path fieldXpath += "/" + xpathToken; field = itemField; int index = Integer.parseInt(xpathToken); path.add(new PathNode(field, index)); } else if (xpathToken.equals(itemField.getName().getLocalName())) { // if the token is equals to the item's field name // ignore it on the xpath but append the item's field to the path node field = itemField; path.add(new PathNode(field)); } else { // otherwise, the token in an item's element // append the token and append the item's field and the item's element's field to the path node fieldXpath += "/" + xpathToken; field = itemField; path.add(new PathNode(field)); field = tm.getField(fieldXpath); if (field == null) { throw new IllegalArgumentException("Invalid xpath " + fieldXpath); } path.add(new PathNode(field)); } } else { // in any case, if it's the first item, the token is the path if (fieldXpath == null) { fieldXpath = xpathToken; } else { // otherwise, append the token to the existing path fieldXpath += "/" + xpathToken; } // get the field field = tm.getField(fieldXpath); // check it exists if (field == null) { throw new IllegalArgumentException("Invalid xpath " + fieldXpath); } // append the pathnode path.add(new PathNode(field)); } } Schema schema = field.getDeclaringType().getSchema(); return new DocumentValidationReport(validateAnyTypeField(schema, path, field, value, validateSubProperties)); } // /////////////////// // UTILITY OPERATIONS protected List<ConstraintViolation> validate(Schema schema, Field field, Object value, boolean validateSubProperties) { List<PathNode> path = Arrays.asList(new PathNode(field)); return validateAnyTypeField(schema, path, field, value, validateSubProperties); } // //////////////////////////// // Exploration based on Fields /** * @since 7.1 */ @SuppressWarnings("rawtypes") private List<ConstraintViolation> validateAnyTypeField(Schema schema, List<PathNode> path, Field field, Object value, boolean validateSubProperties) { if (field.getType().isSimpleType()) { return validateSimpleTypeField(schema, path, field, value); } else if (field.getType().isComplexType()) { List<ConstraintViolation> res = new ArrayList<>(); if (!field.isNillable() && (value == null || (value instanceof Map && ((Map) value).isEmpty()))) { addNotNullViolation(res, schema, path); } if (validateSubProperties) { List<ConstraintViolation> subs = validateComplexTypeField(schema, path, field, value); if (subs != null) { res.addAll(subs); } } return res; } else if (field.getType().isListType()) { // maybe validate the list type here if (validateSubProperties) { return validateListTypeField(schema, path, field, value); } } // unrecognized type : ignored return Collections.emptyList(); } /** * This method should be the only one to create {@link ConstraintViolation}. * * @since 7.1 */ private List<ConstraintViolation> validateSimpleTypeField(Schema schema, List<PathNode> path, Field field, Object value) { Type type = field.getType(); assert type.isSimpleType() || type.isListType(); // list type to manage ArrayProperty List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>(); Set<Constraint> constraints = null; if (type.isListType()) { // ArrayProperty constraints = ((ListType) type).getFieldType().getConstraints(); } else { constraints = field.getConstraints(); } for (Constraint constraint : constraints) { if (!constraint.validate(value)) { ConstraintViolation violation = new ConstraintViolation(schema, path, constraint, value); violations.add(violation); } } return violations; } /** * Validates sub fields for given complex field. * * @since 7.1 */ @SuppressWarnings("unchecked") private List<ConstraintViolation> validateComplexTypeField(Schema schema, List<PathNode> path, Field field, Object value) { assert field.getType().isComplexType(); List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>(); ComplexType complexType = (ComplexType) field.getType(); // this code does not support other type than Map as value if (value != null && !(value instanceof Map)) { return violations; } Map<String, Object> map = (Map<String, Object>) value; for (Field child : complexType.getFields()) { Object item = map.get(child.getName().getLocalName()); List<PathNode> subPath = new ArrayList<PathNode>(path); subPath.add(new PathNode(child)); violations.addAll(validateAnyTypeField(schema, subPath, child, item, true)); } return violations; } /** * Validates sub fields for given list field. * * @since 7.1 */ private List<ConstraintViolation> validateListTypeField(Schema schema, List<PathNode> path, Field field, Object value) { assert field.getType().isListType(); List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>(); Collection<?> castedValue = null; if (value instanceof List) { castedValue = (Collection<?>) value; } else if (value instanceof Object[]) { castedValue = Arrays.asList((Object[]) value); } if (castedValue != null) { ListType listType = (ListType) field.getType(); Field listField = listType.getField(); int index = 0; for (Object item : castedValue) { List<PathNode> subPath = new ArrayList<PathNode>(path); subPath.add(new PathNode(listField, index)); violations.addAll(validateAnyTypeField(schema, subPath, listField, item, true)); index++; } return violations; } return violations; } // ////////////////////////////// // Exploration based on Property /** * @since 7.1 */ private List<ConstraintViolation> validateAnyTypeProperty(Schema schema, List<PathNode> path, Property prop, boolean dirtyOnly, boolean validateSubProperties) { Field field = prop.getField(); if (!dirtyOnly || prop.isDirty()) { if (field.getType().isSimpleType()) { return validateSimpleTypeProperty(schema, path, prop, dirtyOnly); } else if (field.getType().isComplexType()) { // ignore for now the case when the complex property is null with a null contraints because it's // currently impossible if (validateSubProperties) { return validateComplexTypeProperty(schema, path, prop, dirtyOnly); } } else if (field.getType().isListType()) { if (validateSubProperties) { return validateListTypeProperty(schema, path, prop, dirtyOnly); } } } // unrecognized type : ignored return Collections.emptyList(); } /** * @since 7.1 */ private List<ConstraintViolation> validateSimpleTypeProperty(Schema schema, List<PathNode> path, Property prop, boolean dirtyOnly) { Field field = prop.getField(); assert field.getType().isSimpleType() || prop.isScalar(); List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>(); Serializable value = prop.getValue(); if (prop.isPhantom() || value == null) { if (!field.isNillable()) { addNotNullViolation(violations, schema, path); } } else { violations.addAll(validateSimpleTypeField(schema, path, field, value)); } return violations; } /** * @since 7.1 */ private List<ConstraintViolation> validateComplexTypeProperty(Schema schema, List<PathNode> path, Property prop, boolean dirtyOnly) { Field field = prop.getField(); assert field.getType().isComplexType(); List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>(); boolean allChildrenPhantom = true; for (Property child : prop.getChildren()) { if (!child.isPhantom()) { allChildrenPhantom = false; break; } } Object value = prop.getValue(); if (prop.isPhantom() || value == null || allChildrenPhantom) { if (!field.isNillable()) { addNotNullViolation(violations, schema, path); } } else { // this code does not support other type than Map as value if (value instanceof Map) { @SuppressWarnings("unchecked") Map<String, Object> castedValue = (Map<String, Object>) value; if (value == null || castedValue.isEmpty()) { if (!field.isNillable()) { addNotNullViolation(violations, schema, path); } } else { for (Property child : prop.getChildren()) { List<PathNode> subPath = new ArrayList<PathNode>(path); subPath.add(new PathNode(child.getField())); violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly, true)); } } } } return violations; } /** * @since 7.1 */ private List<ConstraintViolation> validateListTypeProperty(Schema schema, List<PathNode> path, Property prop, boolean dirtyOnly) { Field field = prop.getField(); assert field.getType().isListType(); List<ConstraintViolation> violations = new ArrayList<ConstraintViolation>(); Serializable value = prop.getValue(); if (prop.isPhantom() || value == null) { if (!field.isNillable()) { addNotNullViolation(violations, schema, path); } } else { Collection<?> castedValue = null; if (value instanceof Collection) { castedValue = (Collection<?>) value; } else if (value instanceof Object[]) { castedValue = Arrays.asList((Object[]) value); } if (castedValue != null) { int index = 0; if (prop instanceof ArrayProperty) { ArrayProperty arrayProp = (ArrayProperty) prop; // that's an ArrayProperty : there will not be child properties for (Object itemValue : castedValue) { if (!dirtyOnly || arrayProp.isDirty(index)) { List<PathNode> subPath = new ArrayList<PathNode>(path); subPath.add(new PathNode(field, index)); violations.addAll(validateSimpleTypeField(schema, subPath, field, itemValue)); } index++; } } else { for (Property child : prop.getChildren()) { List<PathNode> subPath = new ArrayList<PathNode>(path); subPath.add(new PathNode(child.getField(), index)); violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly, true)); index++; } } } } return violations; } // ////// // Utils private void addNotNullViolation(List<ConstraintViolation> violations, Schema schema, List<PathNode> fieldPath) { NotNullConstraint constraint = NotNullConstraint.get(); ConstraintViolation violation = new ConstraintViolation(schema, fieldPath, constraint, null); violations.add(violation); } }