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