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}