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