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.avalon.framework.configuration.ConfigurationException; 029import org.apache.commons.lang3.StringUtils; 030import org.apache.commons.lang3.tuple.ImmutablePair; 031import org.apache.commons.lang3.tuple.Pair; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035import org.ametys.runtime.i18n.I18nizableText; 036import org.ametys.runtime.model.disableconditions.DisableCondition; 037import org.ametys.runtime.model.disableconditions.DisableConditions; 038import org.ametys.runtime.model.exception.UndefinedItemPathException; 039import org.ametys.runtime.model.type.ElementType; 040import org.ametys.runtime.parameter.ValidationResult; 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 accessors. 124 * @param itemAccessors The model items accessors 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 ModelItemAccessor> itemAccessors) 129 { 130 Map<String, ModelItem> items = new LinkedHashMap<>(); 131 132 for (ModelItemAccessor itemContainer : itemAccessors) 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(ModelItemAccessor 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(ModelItemAccessor 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 * Retrieves the absolute path of the condition 347 * @param condition the condition 348 * @param relativePath the relative path for computing condition absolute path (path of the model item defining the condition, or path of an evaluated data) 349 * @return the absolute path of the condition 350 * @throws IllegalArgumentException if the path of the condition can not be computed from the given relative path 351 */ 352 public static String getDisableConditionAbsolutePath(DisableCondition condition, String relativePath) throws IllegalArgumentException 353 { 354 String[] conditionPathSegments = StringUtils.split(condition.getId(), ModelItem.ITEM_PATH_SEPARATOR); 355 String[] relativePathSegments = StringUtils.split(relativePath, ModelItem.ITEM_PATH_SEPARATOR); 356 357 int nbParentSegmentInConditionPath = 0; 358 String conditionPathSegment = conditionPathSegments[nbParentSegmentInConditionPath]; 359 while ("..".equals(conditionPathSegment)) 360 { 361 nbParentSegmentInConditionPath++; 362 conditionPathSegment = conditionPathSegments[nbParentSegmentInConditionPath]; 363 } 364 365 int prefixLength = relativePathSegments.length - (nbParentSegmentInConditionPath + 1); 366 if (prefixLength < 0) 367 { 368 String message = String.format("Unable to retrieve the condition absolute path from condition '%s' and relative path '%s'. The condition path has to many relative parents segments.", condition.getId(), relativePath); 369 throw new IllegalArgumentException(message); 370 } 371 372 // Take the first segments of relative path 373 String prefix = StringUtils.join(relativePathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, relativePathSegments.length - (nbParentSegmentInConditionPath + 1)); 374 // and the last ones of the condition path 375 String suffix = StringUtils.join(conditionPathSegments, ModelItem.ITEM_PATH_SEPARATOR, nbParentSegmentInConditionPath, conditionPathSegments.length); 376 377 return StringUtils.isNotBlank(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + suffix : suffix; 378 } 379 380 /** 381 * Check if the given model item contains non empty disable conditions 382 * @param modelItem the model item to check 383 * @return <code>true</code> if a non empty condition has been found, <code>false</code> otherwise 384 */ 385 public static boolean hasDisableConditions(ModelItem modelItem) 386 { 387 return hasDisableConditions(modelItem.getDisableConditions()); 388 } 389 390 /** 391 * Check if there is a non empty condition in the given conditions 392 * @param disableConditions the disable conditions to check 393 * @return <code>true</code> if a non empty condition has been found, <code>false</code> otherwise 394 */ 395 public static boolean hasDisableConditions(DisableConditions disableConditions) 396 { 397 if (disableConditions == null) 398 { 399 return false; 400 } 401 402 if (!disableConditions.getConditions().isEmpty()) 403 { 404 // a condition has been found 405 return true; 406 } 407 408 return disableConditions.getSubConditions() 409 .stream() 410 // search for a non empty sub condition 411 .anyMatch(ModelHelper::hasDisableConditions); 412 } 413 414 /** 415 * Validates the given value 416 * @param definition The definition to use to validate the value 417 * @param value the value to validate 418 * @return the structure with errors information if the validation failed. 419 */ 420 public static ValidationResult validateValue(ElementDefinition definition, Object value) 421 { 422 return validateValue(definition, value, true); 423 } 424 425 /** 426 * Validates the given value 427 * @param definition The definition to use to validate the value 428 * @param value the value to validate 429 * @param checkEnumerated <code>true</code> true to make sure that the item with an enumerator has its value in the enumerated values 430 * @return the structure with errors information if the validation failed. 431 * TODO NEWATTRIBUTEAPI RUNTIME-2897: remove this method to always check enumerator when validating a value 432 */ 433 public static ValidationResult validateValue(ElementDefinition definition, Object value, boolean checkEnumerated) 434 { 435 ValidationResult result = new ValidationResult(); 436 437 ElementType type = definition.getType(); 438 if (value != null && !type.isCompatible(value)) 439 { 440 result.addError(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED")); 441 } 442 else 443 { 444 Validator validator = definition.getValidator(); 445 if (validator != null) 446 { 447 result.addResult(validator.validate(value)); 448 } 449 450 if (checkEnumerated) 451 { 452 if (definition.isMultiple()) 453 { 454 Object[] values = (Object[]) value; 455 if (values != null) 456 { 457 for (Object singleValue : values) 458 { 459 result.addResult(_validateEnumeration(definition, singleValue)); 460 } 461 } 462 463 } 464 else 465 { 466 result.addResult(_validateEnumeration(definition, value)); 467 } 468 } 469 } 470 471 return result; 472 } 473 474 private static ValidationResult _validateEnumeration(ElementDefinition definition, Object value) 475 { 476 ValidationResult result = new ValidationResult(); 477 478 if (_checkValueEnumeration(value)) 479 { 480 // Make sure that the item with an enumerator has its value in the enumerated values 481 Enumerator<Object> enumerator = definition.getEnumerator(); 482 if (enumerator != null) 483 { 484 try 485 { 486 I18nizableText entry = enumerator.getEntry(value); 487 if (entry == null) 488 { 489 result.addError(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED")); 490 } 491 } 492 catch (Exception e) 493 { 494 __LOGGER.warn("An error occured while checking enumerated value '{}' for '{}'", value, enumerator, e); 495 result.addError(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_LED_TO_EXCEPTION")); 496 } 497 } 498 } 499 500 return result; 501 } 502 503 private static boolean _checkValueEnumeration(Object value) 504 { 505 if (value == null) 506 { 507 return false; 508 } 509 else 510 { 511 return value instanceof String ? StringUtils.isNotEmpty((String) value) : true; 512 } 513 } 514 515 /** 516 * Stores a configuration and the name of the plugin in which is declared the configuration 517 * @param configuration the configuration itself 518 * @param pluginName the name of the plugin in which is declared the configuration 519 */ 520 public record ConfigurationAndPluginName(Configuration configuration, String pluginName) { /* empty */ } 521 522 /** 523 * Parse an i18n text. 524 * @param configurationAndPluginName the configuration to use. 525 * @param name the child name. 526 * @return the i18n text. 527 * @throws ConfigurationException if the configuration is not valid. 528 */ 529 public static I18nizableText parseI18nizableText(ConfigurationAndPluginName configurationAndPluginName, String name) throws ConfigurationException 530 { 531 return parseI18nizableText(configurationAndPluginName, name, StringUtils.EMPTY); 532 } 533 534 /** 535 * Parse an i18n text. 536 * @param configurationAndPluginName the configuration to use. 537 * @param name the child name. 538 * @param defaultValue the default value if no present 539 * @return the i18n text. 540 * @throws ConfigurationException if the configuration is not valid. 541 */ 542 public static I18nizableText parseI18nizableText(ConfigurationAndPluginName configurationAndPluginName, String name, String defaultValue) throws ConfigurationException 543 { 544 return I18nizableText.parseI18nizableText(configurationAndPluginName.configuration().getChild(name), "plugin." + configurationAndPluginName.pluginName(), defaultValue); 545 } 546 547 /** 548 * Parse an i18n text. 549 * @param configurationAndPluginName the configuration to use. 550 * @param name the child name. 551 * @param defaultValue the default value if no present 552 * @return the i18n text. 553 * @throws ConfigurationException if the configuration is not valid. 554 */ 555 public static I18nizableText parseI18nizableText(ConfigurationAndPluginName configurationAndPluginName, String name, I18nizableText defaultValue) throws ConfigurationException 556 { 557 return I18nizableText.parseI18nizableText(configurationAndPluginName.configuration().getChild(name), "plugin." + configurationAndPluginName.pluginName(), defaultValue); 558 } 559}