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}