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