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