001/* 002 * (C) Copyright 2014-2018 Nuxeo (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 <[email protected]> 018 */ 019 020package org.nuxeo.ecm.core.api.validation; 021 022import static java.util.Collections.singletonList; 023 024import java.io.Serializable; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Objects; 033import java.util.Set; 034 035import org.nuxeo.ecm.core.api.DataModel; 036import org.nuxeo.ecm.core.api.DocumentModel; 037import org.nuxeo.ecm.core.api.model.DocumentPart; 038import org.nuxeo.ecm.core.api.model.Property; 039import org.nuxeo.ecm.core.api.model.impl.ArrayProperty; 040import org.nuxeo.ecm.core.api.validation.ConstraintViolation.PathNode; 041import org.nuxeo.ecm.core.schema.DocumentType; 042import org.nuxeo.ecm.core.schema.SchemaManager; 043import org.nuxeo.ecm.core.schema.types.ComplexType; 044import org.nuxeo.ecm.core.schema.types.Field; 045import org.nuxeo.ecm.core.schema.types.ListType; 046import org.nuxeo.ecm.core.schema.types.Schema; 047import org.nuxeo.ecm.core.schema.types.Type; 048import org.nuxeo.ecm.core.schema.types.constraints.Constraint; 049import org.nuxeo.ecm.core.schema.types.constraints.NotNullConstraint; 050import org.nuxeo.runtime.api.Framework; 051import org.nuxeo.runtime.model.ComponentContext; 052import org.nuxeo.runtime.model.ComponentInstance; 053import org.nuxeo.runtime.model.DefaultComponent; 054 055public class DocumentValidationServiceImpl extends DefaultComponent implements DocumentValidationService { 056 057 private SchemaManager schemaManager; 058 059 protected SchemaManager getSchemaManager() { 060 if (schemaManager == null) { 061 schemaManager = Framework.getService(SchemaManager.class); 062 } 063 return schemaManager; 064 } 065 066 private Map<String, Boolean> validationActivations = new HashMap<>(); 067 068 @Override 069 public void activate(ComponentContext context) { 070 super.activate(context); 071 } 072 073 @Override 074 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 075 if (extensionPoint.equals("activations")) { 076 DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution; 077 validationActivations.put(dvd.getContext(), dvd.isActivated()); 078 } 079 } 080 081 @Override 082 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 083 if (extensionPoint.equals("activations")) { 084 DocumentValidationDescriptor dvd = (DocumentValidationDescriptor) contribution; 085 validationActivations.remove(dvd.getContext()); 086 } 087 } 088 089 @Override 090 public boolean isActivated(String context, Map<String, Serializable> contextMap) { 091 if (contextMap != null) { 092 Forcing flag = (Forcing) contextMap.get(DocumentValidationService.CTX_MAP_KEY); 093 if (flag != null) { 094 switch (flag) { 095 case TURN_ON: 096 return true; 097 case TURN_OFF: 098 return false; 099 case USUAL: 100 break; 101 } 102 } 103 } 104 Boolean activated = validationActivations.get(context); 105 if (activated == null) { 106 return false; 107 } else { 108 return activated; 109 } 110 } 111 112 @Override 113 public DocumentValidationReport validate(DocumentModel document) { 114 return validate(document, false); 115 } 116 117 @Override 118 public DocumentValidationReport validate(DocumentModel document, boolean dirtyOnly) { 119 List<ConstraintViolation> violations = new ArrayList<>(); 120 DocumentType docType = document.getDocumentType(); 121 if (dirtyOnly) { 122 for (DataModel dataModel : document.getDataModels().values()) { 123 Schema schemaDef = getSchemaManager().getSchema(dataModel.getSchema()); 124 for (String fieldName : dataModel.getDirtyFields()) { 125 Field field = schemaDef.getField(fieldName); 126 Property property = document.getProperty(field.getName().getPrefixedName()); 127 List<PathNode> path = singletonList(new PathNode(property.getField())); 128 violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, true, true)); 129 } 130 } 131 } else { 132 for (Schema schema : docType.getSchemas()) { 133 for (Field field : schema.getFields()) { 134 Property property = document.getProperty(field.getName().getPrefixedName()); 135 List<PathNode> path = singletonList(new PathNode(property.getField())); 136 violations.addAll(validateAnyTypeProperty(property.getSchema(), path, property, false, true)); 137 } 138 } 139 } 140 return new DocumentValidationReport(violations); 141 } 142 143 @Override 144 public DocumentValidationReport validate(Field field, Object value) { 145 return validate(field, value, true); 146 } 147 148 @Override 149 public DocumentValidationReport validate(Field field, Object value, boolean validateSubProperties) { 150 Schema schema = field.getDeclaringType().getSchema(); 151 return new DocumentValidationReport(validate(schema, field, value, validateSubProperties)); 152 } 153 154 @Override 155 public DocumentValidationReport validate(Property property) { 156 return validate(property, true); 157 } 158 159 @Override 160 public DocumentValidationReport validate(Property property, boolean validateSubProperties) { 161 List<PathNode> path = new ArrayList<>(); 162 Property inspect = property; 163 while (inspect != null && !(inspect instanceof DocumentPart)) { 164 path.add(0, new PathNode(inspect.getField())); 165 inspect = inspect.getParent(); 166 } 167 return new DocumentValidationReport( 168 validateAnyTypeProperty(property.getSchema(), path, property, false, validateSubProperties)); 169 } 170 171 @Override 172 public DocumentValidationReport validate(String xpath, Object value) { 173 return validate(xpath, value, true); 174 } 175 176 @Override 177 public DocumentValidationReport validate(String xpath, Object value, boolean validateSubProperties) 178 throws IllegalArgumentException { 179 SchemaManager tm = Framework.getService(SchemaManager.class); 180 String[] splittedXpath = xpath.split("/"); 181 List<PathNode> path = new ArrayList<>(); 182 Field field = null; 183 StringBuilder fieldXpath = new StringBuilder(xpath.length()); 184 // rebuild the field path 185 for (String xpathToken : splittedXpath) { 186 // manage the list item case 187 if (field != null && field.getType().isListType()) { 188 // get the list field type 189 Field itemField = ((ListType) field.getType()).getField(); 190 if (xpathToken.matches("\\d+")) { 191 // if the current token is an index, append the token and append an indexed PathNode to the path 192 fieldXpath.append('/').append(xpathToken); 193 field = itemField; 194 int index = Integer.parseInt(xpathToken); 195 path.add(new PathNode(field, index)); 196 } else if (xpathToken.equals(itemField.getName().getLocalName())) { 197 // if the token is equals to the item's field name 198 // ignore it on the xpath but append the item's field to the path node 199 field = itemField; 200 path.add(new PathNode(field)); 201 } else { 202 // otherwise, the token in an item's element 203 // append the token and append the item's field and the item's element's field to the path node 204 fieldXpath.append('/').append(xpathToken); 205 field = itemField; 206 path.add(new PathNode(field)); 207 field = tm.getField(fieldXpath.toString()); 208 if (field == null) { 209 throw new IllegalArgumentException("Invalid xpath " + fieldXpath); 210 } 211 path.add(new PathNode(field)); 212 } 213 } else { 214 if (fieldXpath.length() != 0) { 215 fieldXpath.append('/'); 216 } 217 fieldXpath.append(xpathToken); 218 // get the field 219 field = tm.getField(fieldXpath.toString()); 220 // check it exists 221 if (field == null) { 222 throw new IllegalArgumentException("Invalid xpath " + fieldXpath); 223 } 224 // append the pathnode 225 path.add(new PathNode(field)); 226 } 227 } 228 Schema schema = field.getDeclaringType().getSchema(); // NOSONAR 229 return new DocumentValidationReport(validateAnyTypeField(schema, path, field, value, validateSubProperties)); 230 } 231 232 // /////////////////// 233 // UTILITY OPERATIONS 234 235 protected List<ConstraintViolation> validate(Schema schema, Field field, Object value, 236 boolean validateSubProperties) { 237 List<PathNode> path = singletonList(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<>(); 284 Set<Constraint> constraints; 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<>(); 309 ComplexType complexType = (ComplexType) field.getType(); 310 // this code does not support other type than Map as value 311 if (!(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<>(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<>(); 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<>(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<>(); 397 Serializable value = prop.getValue(); 398 Object defaultValue = field.getDefaultValue(); 399 // check nullity constraint only if field doesn't have a default value (phantom case) 400 if (prop.isPhantom() && defaultValue == null || value == null) { 401 if (!field.isNillable()) { 402 addNotNullViolation(violations, schema, path); 403 } 404 } else { 405 violations.addAll(validateSimpleTypeField(schema, path, field, value)); 406 } 407 return violations; 408 } 409 410 /** 411 * @since 7.1 412 */ 413 private List<ConstraintViolation> validateComplexTypeProperty(Schema schema, List<PathNode> path, Property prop, 414 boolean dirtyOnly) { 415 Field field = prop.getField(); 416 assert field.getType().isComplexType(); 417 List<ConstraintViolation> violations = new ArrayList<>(); 418 boolean allChildrenPhantom = true; 419 for (Property child : prop.getChildren()) { 420 if (!child.isPhantom()) { 421 allChildrenPhantom = false; 422 break; 423 } 424 } 425 Object value = prop.getValue(); 426 if (prop.isPhantom() || value == null || allChildrenPhantom) { 427 if (!field.isNillable()) { 428 addNotNullViolation(violations, schema, path); 429 } 430 } else { 431 // this code does not support other type than Map as value 432 if (value instanceof Map) { 433 @SuppressWarnings("unchecked") 434 Map<String, Object> castedValue = (Map<String, Object>) value; 435 if (castedValue.isEmpty() || castedValue.values().stream().allMatch(Objects::isNull)) { 436 if (!field.isNillable()) { 437 addNotNullViolation(violations, schema, path); 438 } 439 } else { 440 for (Property child : prop.getChildren()) { 441 List<PathNode> subPath = new ArrayList<>(path); 442 subPath.add(new PathNode(child.getField())); 443 violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly, true)); 444 } 445 } 446 } 447 } 448 return violations; 449 } 450 451 /** 452 * @since 7.1 453 */ 454 private List<ConstraintViolation> validateListTypeProperty(Schema schema, List<PathNode> path, Property prop, 455 boolean dirtyOnly) { 456 Field field = prop.getField(); 457 assert field.getType().isListType(); 458 List<ConstraintViolation> violations = new ArrayList<>(); 459 Serializable value = prop.getValue(); 460 if (prop.isPhantom() || value == null) { 461 if (!field.isNillable()) { 462 addNotNullViolation(violations, schema, path); 463 } 464 } else { 465 Collection<?> castedValue = null; 466 if (value instanceof Collection) { 467 castedValue = (Collection<?>) value; 468 } else if (value instanceof Object[]) { 469 castedValue = Arrays.asList((Object[]) value); 470 } 471 if (castedValue != null) { 472 int index = 0; 473 if (prop instanceof ArrayProperty) { 474 if (!field.isNillable() && castedValue.isEmpty()) { 475 addNotNullViolation(violations, schema, path); 476 } 477 ArrayProperty arrayProp = (ArrayProperty) prop; 478 // that's an ArrayProperty : there will not be child properties 479 for (Object itemValue : castedValue) { 480 if (!dirtyOnly || arrayProp.isDirty(index)) { 481 List<PathNode> subPath = new ArrayList<>(path); 482 subPath.add(new PathNode(field, index)); 483 violations.addAll(validateSimpleTypeField(schema, subPath, field, itemValue)); 484 } 485 index++; 486 } 487 } else { 488 Collection<Property> children = prop.getChildren(); 489 if (!field.isNillable() && children.isEmpty()) { 490 addNotNullViolation(violations, schema, path); 491 } 492 for (Property child : children) { 493 List<PathNode> subPath = new ArrayList<>(path); 494 subPath.add(new PathNode(child.getField(), index)); 495 violations.addAll(validateAnyTypeProperty(schema, subPath, child, dirtyOnly, true)); 496 index++; 497 } 498 } 499 } 500 } 501 return violations; 502 } 503 504 // ////// 505 // Utils 506 507 private void addNotNullViolation(List<ConstraintViolation> violations, Schema schema, List<PathNode> fieldPath) { 508 NotNullConstraint constraint = NotNullConstraint.get(); 509 ConstraintViolation violation = new ConstraintViolation(schema, fieldPath, constraint, null); 510 violations.add(violation); 511 } 512 513}