001/* 002 * Copyright 2018 Anyware Services 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 */ 016package org.ametys.runtime.model; 017 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025 026import org.apache.commons.lang3.StringUtils; 027import org.apache.commons.lang3.tuple.ImmutablePair; 028import org.apache.commons.lang3.tuple.Pair; 029import org.slf4j.Logger; 030import org.slf4j.LoggerFactory; 031 032import org.ametys.runtime.config.DisableCondition; 033import org.ametys.runtime.config.DisableCondition.OPERATOR; 034import org.ametys.runtime.config.DisableConditions; 035import org.ametys.runtime.i18n.I18nizableText; 036import org.ametys.runtime.model.exception.UndefinedItemPathException; 037import org.ametys.runtime.model.type.ElementType; 038import org.ametys.runtime.parameter.Errors; 039import org.ametys.runtime.parameter.Validator; 040 041/** 042 * Helper class for models 043 */ 044public final class ModelHelper 045{ 046 private static final Logger __LOGGER = LoggerFactory.getLogger(ModelHelper.class); 047 048 private ModelHelper() 049 { 050 // Empty constructor 051 } 052 053 /** 054 * Checks if there is a model item at the given relative path 055 * @param path path of the model item. This path is relative to the given accessors. No matter if it is a definition or data path (with repeater entry positions) 056 * @param itemAccessors model item accessors where to search if there is a model item 057 * @return <code>true</code> if there is model item at this path, <code>false</code> otherwise 058 * @throws IllegalArgumentException if the given path is null or empty 059 */ 060 public static boolean hasModelItem(String path, Collection<? extends ModelItemAccessor> itemAccessors) throws IllegalArgumentException 061 { 062 try 063 { 064 getModelItem(path, itemAccessors); 065 return true; 066 } 067 catch (UndefinedItemPathException e) 068 { 069 return false; 070 } 071 } 072 073 /** 074 * Retrieves the model item at the given relative path 075 * @param path path of the model item to retrieve. This path is relative to the given accessors. No matter if it is a definition or data path (with repeater entry positions) 076 * @param itemAccessors model item accessors where to search the model item 077 * @return the model item 078 * @throws IllegalArgumentException if the given path is null or empty 079 * @throws UndefinedItemPathException if there is no item defined at the given path in given item accessors 080 */ 081 public static ModelItem getModelItem(String path, Collection<? extends ModelItemAccessor> itemAccessors) throws IllegalArgumentException, UndefinedItemPathException 082 { 083 if (StringUtils.isEmpty(path)) 084 { 085 throw new IllegalArgumentException("Unable to retrieve the model item at the given path. This path is empty."); 086 } 087 088 String definitionPath = getDefinitionPathFromDataPath(path); 089 090 ModelItem item = null; 091 for (ModelItemAccessor accessor : itemAccessors) 092 { 093 if (accessor.hasModelItem(definitionPath)) 094 { 095 item = accessor.getModelItem(definitionPath); 096 break; 097 } 098 } 099 100 if (item != null) 101 { 102 return item; 103 } 104 else 105 { 106 String absolutePath = path; 107 if (itemAccessors.size() == 1) 108 { 109 ModelItemAccessor accessor = itemAccessors.iterator().next(); 110 if (accessor instanceof ModelItemGroup) 111 { 112 absolutePath = ((ModelItemGroup) accessor).getPath() + ModelItem.ITEM_PATH_SEPARATOR + path; 113 } 114 } 115 116 throw new UndefinedItemPathException("Unable to retrieve the model item at path '" + absolutePath + "'. This path is not defined by the model."); 117 } 118 } 119 120 /** 121 * Retrieve the list of successive model items represented by the given paths, indexed by path. 122 * @param paths paths of the model items to retrieve. These paths are relative to the given accessors. No matter if they are definition or data paths (with repeater entry positions) 123 * @param itemAccessors model item accessors where to search the model items 124 * @return the list of successive model items, indexed by path 125 * @throws IllegalArgumentException if one of the given paths is null or empty 126 * @throws UndefinedItemPathException if there is no item defined at one of the given paths in given item accessors 127 */ 128 public static Map<String, List<ModelItem>> getAllModelItemsInPaths(Set<String> paths, Collection<? extends ModelItemAccessor> itemAccessors) throws IllegalArgumentException, UndefinedItemPathException 129 { 130 Map<String, List<ModelItem>> result = new HashMap<>(); 131 132 for (String path : paths) 133 { 134 result.put(path, ModelHelper.getAllModelItemsInPath(path, itemAccessors)); 135 } 136 137 return result; 138 } 139 140 /** 141 * Retrieve the list of successive model items represented by the given path. 142 * @param path path of the model items to retrieve. This path is relative to the given accessors. No matter if it is a definition or data path (with repeater entry positions) 143 * @param itemAccessors model item accessors where to search the model items 144 * @return the list of successive model items 145 * @throws IllegalArgumentException if the given path is null or empty 146 * @throws UndefinedItemPathException if there is no item defined at the given path in given item accessors 147 */ 148 public static List<ModelItem> getAllModelItemsInPath(String path, Collection<? extends ModelItemAccessor> itemAccessors) throws IllegalArgumentException, UndefinedItemPathException 149 { 150 if (StringUtils.isEmpty(path)) 151 { 152 throw new IllegalArgumentException("Unable to retrieve the model item at the given path. This path is empty."); 153 } 154 155 String definitionPath = getDefinitionPathFromDataPath(path); 156 157 for (ModelItemAccessor itemAccessor : itemAccessors) 158 { 159 // If the current accessor has the model item at given path, no need to check in the other accessors 160 // Given containers represent only one whole model, so a path can't refer to different definitions 161 List<ModelItem> modelItemPath = _getAllModelItemsInPath(definitionPath, itemAccessor); 162 if (!modelItemPath.isEmpty()) 163 { 164 return modelItemPath; 165 } 166 } 167 168 // No path has been found in the different containers 169 throw new UndefinedItemPathException("Unable to retrieve the model items at path '" + definitionPath + "'. This path is not defined by the model."); 170 } 171 172 private static List<ModelItem> _getAllModelItemsInPath(String definitionPath, ModelItemAccessor itemAccessor) 173 { 174 List<ModelItem> definitions = new ArrayList<>(); 175 176 String[] pathSegments = StringUtils.split(definitionPath, ModelItem.ITEM_PATH_SEPARATOR); 177 178 ModelItemAccessor currentModelItemAccessor = itemAccessor; 179 for (int i = 0; i < pathSegments.length; i++) 180 { 181 ModelItem modelItem = currentModelItemAccessor.getChild(pathSegments[i]); 182 if (modelItem != null) 183 { 184 definitions.add(modelItem); 185 } 186 else 187 { 188 return Collections.emptyList(); 189 } 190 191 if (modelItem instanceof ModelItemAccessor) 192 { 193 currentModelItemAccessor = (ModelItemAccessor) modelItem; 194 } 195 } 196 197 return definitions; 198 } 199 200 201 /** 202 * Determines if a container of model items contains a model item of the given type 203 * @param container the model item container 204 * @param type the type identifier to find. 205 * @return true if a model item of the given type is found 206 */ 207 public static boolean hasModelItemOfType(ModelItemContainer container, String type) 208 { 209 for (ModelItem childItem : container.getModelItems()) 210 { 211 if (type.equals(childItem.getType().getId())) 212 { 213 return true; 214 } 215 else if (childItem instanceof ModelItemGroup) 216 { 217 // recurse on repeater and composites 218 if (hasModelItemOfType((ModelItemGroup) childItem, type)) 219 { 220 return true; 221 } 222 } 223 } 224 225 return false; 226 } 227 228 /** 229 * Find all model items of the given type 230 * @param container the model item container 231 * @param type the type identifier to find. 232 * @return the list of {@link ModelItem}s of this type 233 */ 234 public static List<ModelItem> findModelItemsByType(ModelItemContainer container, String type) 235 { 236 List<ModelItem> items = new ArrayList<>(); 237 238 for (ModelItem childItem : container.getModelItems()) 239 { 240 if (type.equals(childItem.getType().getId())) 241 { 242 items.add(childItem); 243 } 244 else if (childItem instanceof ModelItemGroup) 245 { 246 // recurse on repeater and composites 247 items.addAll(findModelItemsByType((ModelItemGroup) childItem, type)); 248 } 249 } 250 251 return items; 252 } 253 254 /** 255 * Retrieves the given dataPath as a definition path (without the repeaterEntry positions) 256 * @param dataPath the data path 257 * @return the definition path 258 */ 259 public static String getDefinitionPathFromDataPath(String dataPath) 260 { 261 return dataPath.replaceAll("\\[[0-9]+\\]", ""); 262 } 263 264 /** 265 * Checks if this item is in a group with a switch on 266 * @param modelItem the item to check 267 * @param values all items' values to get switchers' values 268 * @return false if this item is part of a group with a switch to off, true otherwise 269 */ 270 public static boolean isGroupSwitchOn(ModelItem modelItem, Map<String, Object> values) 271 { 272 Pair<Boolean, ElementDefinition> isGroupActive = _isModelItemGroupActive(modelItem.getParent(), values); 273 if (isGroupActive.getKey()) 274 { 275 return true; 276 } 277 return modelItem.equals(isGroupActive.getValue()); 278 } 279 280 private static Pair<Boolean, ElementDefinition> _isModelItemGroupActive(ModelItemGroup group, Map<String, Object> values) 281 { 282 if (group == null) 283 { 284 return new ImmutablePair<>(true, null); 285 } 286 287 ElementDefinition<Boolean> groupSwitch = group.getSwitcher(); 288 if (groupSwitch == null) 289 { 290 return _isModelItemGroupActive(group.getParent(), values); 291 } 292 293 Object value = values.get(groupSwitch.getName()); 294 295 if (value == null) 296 { 297 value = groupSwitch.getDefaultValue(); 298 } 299 300 if (!(value instanceof Boolean)) 301 { 302 throw new IllegalStateException("The switcher value of group " + group.getName() + " is null or not a boolean"); 303 } 304 305 return (Boolean) value ? _isModelItemGroupActive(group.getParent(), values) : new ImmutablePair<>(false, groupSwitch); 306 } 307 308 /** 309 * Recursively evaluate the {@link DisableConditions} against the given values 310 * @param disableConditions the disable conditions to evaluate 311 * @param definitionAndValues the values to evaluate 312 * @param logger the logger for disable conditions evaluation logs 313 * @return true if the disable conditions are true, false otherwise 314 */ 315 public static boolean evaluateDisableConditions(DisableConditions disableConditions, Map<String, DefinitionAndValue> definitionAndValues, Logger logger) 316 { 317 if (!_hasDisableConditions(disableConditions)) 318 { 319 return false; 320 } 321 322 boolean disabled; 323 boolean andOperator = disableConditions.getAssociationType() == DisableConditions.ASSOCIATION_TYPE.AND; 324 325 // initial value depends on OR or AND associations 326 disabled = andOperator; 327 328 for (DisableConditions subConditions : disableConditions.getSubConditions()) 329 { 330 boolean result = evaluateDisableConditions(subConditions, definitionAndValues, logger); 331 disabled = andOperator ? disabled && result : disabled || result; 332 } 333 334 for (DisableCondition condition : disableConditions.getConditions()) 335 { 336 boolean result = _evaluateCondition(condition, definitionAndValues, logger); 337 disabled = andOperator ? disabled && result : disabled || result; 338 } 339 340 return disabled; 341 } 342 343 private static boolean _hasDisableConditions(DisableConditions disableConditions) 344 { 345 return disableConditions != null && !disableConditions.getConditions().isEmpty() && disableConditions.getSubConditions().isEmpty(); 346 } 347 348 private static boolean _evaluateCondition(DisableCondition condition, Map<String, DefinitionAndValue> definitionAndValues, Logger logger) 349 { 350 String id = condition.getId(); 351 DisableCondition.OPERATOR operator = condition.getOperator(); 352 String conditionValue = condition.getValue(); 353 354 DefinitionAndValue definitionAndValue = definitionAndValues.get(id); 355 if (definitionAndValue == null || !(definitionAndValue.getDefinition() instanceof ElementDefinition)) 356 { 357 logger.debug("Cannot evaluate the disable condition on the undefined element {}.\nReturning false.", id); 358 return false; 359 } 360 361 ElementType type = ((ElementDefinition) definitionAndValue.getDefinition()).getType(); 362 Object compareValue = type.castValue(conditionValue); 363 if (compareValue == null) 364 { 365 throw new IllegalStateException("Cannot convert the condition value '" + conditionValue + "' to a '" + type.getId() + "' for model item '" + id + "'"); 366 } 367 368 try 369 { 370 Object value = definitionAndValue.getValue(); 371 if (value instanceof Collection) 372 { 373 if (operator == OPERATOR.EQ) 374 { 375 for (Object v: (Collection) value) 376 { 377 if (_evaluateConditionValue(v, operator, compareValue)) 378 { 379 // One entry matches 380 return true; 381 } 382 } 383 384 return false; 385 } 386 else 387 { 388 for (Object v: (Collection) value) 389 { 390 if (!_evaluateConditionValue(v, operator, compareValue)) 391 { 392 // One entry does not match 393 return false; 394 } 395 } 396 397 return true; 398 } 399 } 400 else 401 { 402 return _evaluateConditionValue(value, operator, compareValue); 403 } 404 } 405 catch (Exception e) 406 { 407 throw new IllegalStateException("An error occurred while comparing values in type'" + type + "' for model item '" + id + "'.", e); 408 } 409 } 410 private static boolean _evaluateConditionValue(Object value, DisableCondition.OPERATOR operator, Object compareValue) 411 { 412 if ((value == null || value instanceof Comparable) && (compareValue == null || compareValue instanceof Comparable)) 413 { 414 @SuppressWarnings("unchecked") 415 Comparable<Object> comparableParameterValue = (Comparable<Object>) _emptyStringToNull(value); 416 @SuppressWarnings("unchecked") 417 Comparable<Object> comparableCompareValue = (Comparable<Object>) _emptyStringToNull(compareValue); 418 419 int comparison; 420 if (comparableParameterValue != null && comparableCompareValue != null) 421 { 422 comparison = comparableParameterValue.compareTo(comparableCompareValue); 423 } 424 else if (comparableCompareValue != null) 425 { 426 comparison = -1; // null comparableParameterValue is considered less than non-null comparableCompareValue 427 } 428 else if (comparableParameterValue != null) 429 { 430 comparison = 1; // non-null comparableParameterValue is considered greater than null comparableCompareValue 431 } 432 else 433 { 434 comparison = 0; // both are null 435 } 436 437 switch (operator) 438 { 439 case NEQ: 440 return comparison != 0; 441 case GEQ: 442 return comparison >= 0; 443 case GT: 444 return comparison > 0; 445 case LT: 446 return comparison < 0; 447 case LEQ: 448 return comparison <= 0; 449 case EQ: 450 default: 451 return comparison == 0; 452 } 453 } 454 else 455 { 456 throw new IllegalStateException("Values '" + value + "' and '" + compareValue + "' are not comparable"); 457 } 458 } 459 460 private static Object _emptyStringToNull(Object compareValue) 461 { 462 if ("".equals(compareValue)) 463 { 464 return null; 465 } 466 else 467 { 468 return compareValue; 469 } 470 } 471 472 /** 473 * Validates the given value 474 * @param definition The definition to use to validate the value 475 * @param value the value to validate 476 * @return the structure with errors information if the validation failed. 477 */ 478 public static List<I18nizableText> validateValue(ElementDefinition definition, Object value) 479 { 480 return validateValue(definition, value, true); 481 } 482 483 /** 484 * Validates the given value 485 * @param definition The definition to use to validate the value 486 * @param value the value to validate 487 * @param checkEnumerated <code>true</code> true to make sure that the item with an enumerator has its value in the enumerated values 488 * @return the structure with errors information if the validation failed. 489 * TODO NEWATTRIBUTEAPI RUNTIME-2897: remove this method to always check enumerator when validating a value 490 */ 491 public static List<I18nizableText> validateValue(ElementDefinition definition, Object value, boolean checkEnumerated) 492 { 493 List<I18nizableText> errorsList = new ArrayList<>(); 494 Errors errors = new Errors(); 495 496 ElementType type = definition.getType(); 497 if (value != null && !type.isCompatible(value)) 498 { 499 errorsList.add(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED")); 500 } 501 502 Validator validator = definition.getValidator(); 503 if (validator != null) 504 { 505 validator.validate(value, errors); 506 if (errors.hasErrors()) 507 { 508 errorsList.addAll(errors.getErrors()); 509 } 510 } 511 512 if (checkEnumerated) 513 { 514 if (definition.isMultiple()) 515 { 516 Object[] values = (Object[]) value; 517 if (values != null) 518 { 519 for (Object singleValue : values) 520 { 521 _validateEnumeration(definition, singleValue, errorsList); 522 } 523 } 524 525 } 526 else 527 { 528 _validateEnumeration(definition, value, errorsList); 529 } 530 } 531 532 return errorsList; 533 } 534 535 private static void _validateEnumeration(ElementDefinition definition, Object value, List<I18nizableText> errorsList) 536 { 537 if (_checkValueEnumeration(value)) 538 { 539 // Make sure that the item with an enumerator has its value in the enumerated values 540 Enumerator<Object> enumerator = definition.getEnumerator(); 541 if (enumerator != null) 542 { 543 I18nizableText entry = null; 544 try 545 { 546 entry = enumerator.getEntry(value); 547 } 548 catch (Exception e) 549 { 550 __LOGGER.warn("An error occured while checking enumerated value '{}' for '{}'", value, enumerator, e); 551 errorsList.add(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_LED_TO_EXCEPTION")); 552 return; 553 } 554 555 if (entry == null) 556 { 557 errorsList.add(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED")); 558 } 559 } 560 } 561 } 562 563 private static boolean _checkValueEnumeration(Object value) 564 { 565 if (value == null) 566 { 567 return false; 568 } 569 else 570 { 571 return value instanceof String ? StringUtils.isNotEmpty((String) value) : true; 572 } 573 } 574}