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.commons.lang3.StringUtils; 028import org.apache.commons.lang3.tuple.ImmutablePair; 029import org.apache.commons.lang3.tuple.Pair; 030import org.slf4j.Logger; 031import org.slf4j.LoggerFactory; 032 033import org.ametys.runtime.i18n.I18nizableText; 034import org.ametys.runtime.model.disableconditions.DisableCondition; 035import org.ametys.runtime.model.disableconditions.DisableConditions; 036import org.ametys.runtime.model.exception.UndefinedItemPathException; 037import org.ametys.runtime.model.type.ElementType; 038import org.ametys.runtime.parameter.ValidationResult; 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 * Retrieves all model items of given accessors. 122 * @param itemAccessors The model items accessors 123 * @return the model items 124 * @throws IllegalArgumentException if some models define a model item with the same name and this model item does not come from a common ancestor 125 */ 126 public static Collection<? extends ModelItem> getModelItems(Collection<? extends ModelItemAccessor> itemAccessors) 127 { 128 Map<String, ModelItem> items = new LinkedHashMap<>(); 129 130 for (ModelItemAccessor itemContainer : itemAccessors) 131 { 132 for (ModelItem currentItem : itemContainer.getModelItems()) 133 { 134 final String currentItemName = currentItem.getName(); 135 if (items.containsKey(currentItemName)) 136 { 137 ModelItem existingItem = items.get(currentItemName); 138 139 if (!currentItem.getModel().equals(existingItem.getModel())) 140 { 141 // The definition does not provide from a common ancestor 142 throw new IllegalArgumentException("The model item '" + currentItemName + "' defined in model '" + currentItem.getModel().getId() + "' is already defined in another model '" 143 + existingItem.getModel().getId() + "'"); 144 } 145 continue; 146 } 147 148 items.put(currentItemName, currentItem); 149 } 150 } 151 152 return items.values(); 153 } 154 155 /** 156 * Retrieve the list of successive model items represented by the given paths, indexed by path. 157 * @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) 158 * @param itemAccessors model item accessors where to search the model items 159 * @return the list of successive model items, indexed by path 160 * @throws IllegalArgumentException if one of the given paths is null or empty 161 * @throws UndefinedItemPathException if there is no item defined at one of the given paths in given item accessors 162 */ 163 public static Map<String, List<ModelItem>> getAllModelItemsInPaths(Set<String> paths, Collection<? extends ModelItemAccessor> itemAccessors) throws IllegalArgumentException, UndefinedItemPathException 164 { 165 Map<String, List<ModelItem>> result = new HashMap<>(); 166 167 for (String path : paths) 168 { 169 result.put(path, ModelHelper.getAllModelItemsInPath(path, itemAccessors)); 170 } 171 172 return result; 173 } 174 175 /** 176 * Retrieve the list of successive model items represented by the given path. 177 * @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) 178 * @param itemAccessors model item accessors where to search the model items 179 * @return the list of successive model items 180 * @throws IllegalArgumentException if the given path is null or empty 181 * @throws UndefinedItemPathException if there is no item defined at the given path in given item accessors 182 */ 183 public static List<ModelItem> getAllModelItemsInPath(String path, Collection<? extends ModelItemAccessor> itemAccessors) throws IllegalArgumentException, UndefinedItemPathException 184 { 185 if (StringUtils.isEmpty(path)) 186 { 187 throw new IllegalArgumentException("Unable to retrieve the model item at the given path. This path is empty."); 188 } 189 190 String definitionPath = getDefinitionPathFromDataPath(path); 191 192 for (ModelItemAccessor itemAccessor : itemAccessors) 193 { 194 // If the current accessor has the model item at given path, no need to check in the other accessors 195 // Given containers represent only one whole model, so a path can't refer to different definitions 196 List<ModelItem> modelItemPath = _getAllModelItemsInPath(definitionPath, itemAccessor); 197 if (!modelItemPath.isEmpty()) 198 { 199 return modelItemPath; 200 } 201 } 202 203 // No path has been found in the different containers 204 throw new UndefinedItemPathException("Unable to retrieve the model items at path '" + definitionPath + "'. This path is not defined by the model."); 205 } 206 207 private static List<ModelItem> _getAllModelItemsInPath(String definitionPath, ModelItemAccessor itemAccessor) 208 { 209 List<ModelItem> definitions = new ArrayList<>(); 210 211 String[] pathSegments = StringUtils.split(definitionPath, ModelItem.ITEM_PATH_SEPARATOR); 212 213 ModelItemAccessor currentModelItemAccessor = itemAccessor; 214 for (int i = 0; i < pathSegments.length; i++) 215 { 216 ModelItem modelItem = currentModelItemAccessor.getChild(pathSegments[i]); 217 if (modelItem != null) 218 { 219 definitions.add(modelItem); 220 } 221 else 222 { 223 return Collections.emptyList(); 224 } 225 226 if (modelItem instanceof ModelItemAccessor) 227 { 228 currentModelItemAccessor = (ModelItemAccessor) modelItem; 229 } 230 } 231 232 return definitions; 233 } 234 235 236 /** 237 * Determines if a container of model items contains a model item of the given type 238 * @param container the model item container 239 * @param type the type identifier to find. 240 * @return true if a model item of the given type is found 241 */ 242 public static boolean hasModelItemOfType(ModelItemAccessor container, String type) 243 { 244 for (ModelItem childItem : container.getModelItems()) 245 { 246 if (type.equals(childItem.getType().getId())) 247 { 248 return true; 249 } 250 else if (childItem instanceof ModelItemGroup) 251 { 252 // recurse on repeater and composites 253 if (hasModelItemOfType((ModelItemGroup) childItem, type)) 254 { 255 return true; 256 } 257 } 258 } 259 260 return false; 261 } 262 263 /** 264 * Find all model items of the given type 265 * @param container the model item container 266 * @param type the type identifier to find. 267 * @return the list of {@link ModelItem}s of this type 268 */ 269 public static List<ModelItem> findModelItemsByType(ModelItemAccessor container, String type) 270 { 271 List<ModelItem> items = new ArrayList<>(); 272 273 for (ModelItem childItem : container.getModelItems()) 274 { 275 if (type.equals(childItem.getType().getId())) 276 { 277 items.add(childItem); 278 } 279 else if (childItem instanceof ModelItemGroup) 280 { 281 // recurse on repeater and composites 282 items.addAll(findModelItemsByType((ModelItemGroup) childItem, type)); 283 } 284 } 285 286 return items; 287 } 288 289 /** 290 * Retrieves the given dataPath as a definition path (without the repeaterEntry positions) 291 * @param dataPath the data path 292 * @return the definition path 293 */ 294 public static String getDefinitionPathFromDataPath(String dataPath) 295 { 296 return dataPath.replaceAll("\\[[0-9]+\\]", ""); 297 } 298 299 /** 300 * Checks if this item is in a group with a switch on 301 * @param modelItem the item to check 302 * @param values all items' values to get switchers' values 303 * @return false if this item is part of a group with a switch to off, true otherwise 304 */ 305 public static boolean isGroupSwitchOn(ModelItem modelItem, Map<String, Object> values) 306 { 307 Pair<Boolean, ElementDefinition> isGroupActive = _isModelItemGroupActive(modelItem.getParent(), values); 308 if (isGroupActive.getKey()) 309 { 310 return true; 311 } 312 return modelItem.equals(isGroupActive.getValue()); 313 } 314 315 private static Pair<Boolean, ElementDefinition> _isModelItemGroupActive(ModelItemGroup group, Map<String, Object> values) 316 { 317 if (group == null) 318 { 319 return new ImmutablePair<>(true, null); 320 } 321 322 ElementDefinition<Boolean> groupSwitch = group.getSwitcher(); 323 if (groupSwitch == null) 324 { 325 return _isModelItemGroupActive(group.getParent(), values); 326 } 327 328 Object value = values.get(groupSwitch.getName()); 329 330 if (value == null) 331 { 332 value = groupSwitch.getDefaultValue(); 333 } 334 335 if (!(value instanceof Boolean)) 336 { 337 throw new IllegalStateException("The switcher value of group " + group.getName() + " is null or not a boolean"); 338 } 339 340 return (Boolean) value ? _isModelItemGroupActive(group.getParent(), values) : new ImmutablePair<>(false, groupSwitch); 341 } 342 343 /** 344 * Retrieves the absolute path of the condition 345 * @param condition the condition 346 * @param relativePath the relative path for computing condition absolute path (path of the model item defining the condition, or path of an evaluated data) 347 * @return the absolute path of the condition 348 * @throws IllegalArgumentException if the path of the condition can not be computed from the given relative path 349 */ 350 public static String getDisableConditionAbsolutePath(DisableCondition condition, String relativePath) throws IllegalArgumentException 351 { 352 String[] conditionPathSegments = StringUtils.split(condition.getId(), ModelItem.ITEM_PATH_SEPARATOR); 353 String[] relativePathSegments = StringUtils.split(relativePath, ModelItem.ITEM_PATH_SEPARATOR); 354 355 int nbParentSegmentInConditionPath = 0; 356 String conditionPathSegment = conditionPathSegments[nbParentSegmentInConditionPath]; 357 while ("..".equals(conditionPathSegment)) 358 { 359 nbParentSegmentInConditionPath++; 360 conditionPathSegment = conditionPathSegments[nbParentSegmentInConditionPath]; 361 } 362 363 int prefixLength = relativePathSegments.length - (nbParentSegmentInConditionPath + 1); 364 if (prefixLength < 0) 365 { 366 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); 367 throw new IllegalArgumentException(message); 368 } 369 370 // Take the first segments of relative path 371 String prefix = StringUtils.join(relativePathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, relativePathSegments.length - (nbParentSegmentInConditionPath + 1)); 372 // and the last ones of the condition path 373 String suffix = StringUtils.join(conditionPathSegments, ModelItem.ITEM_PATH_SEPARATOR, nbParentSegmentInConditionPath, conditionPathSegments.length); 374 375 return StringUtils.isNotBlank(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + suffix : suffix; 376 } 377 378 /** 379 * Check if the given model item contains non empty disable conditions 380 * @param modelItem the model item to check 381 * @return <code>true</code> if a non empty condition has been found, <code>false</code> otherwise 382 */ 383 public static boolean hasDisableConditions(ModelItem modelItem) 384 { 385 return hasDisableConditions(modelItem.getDisableConditions()); 386 } 387 388 /** 389 * Check if there is a non empty condition in the given conditions 390 * @param disableConditions the disable conditions to check 391 * @return <code>true</code> if a non empty condition has been found, <code>false</code> otherwise 392 */ 393 public static boolean hasDisableConditions(DisableConditions disableConditions) 394 { 395 if (disableConditions == null) 396 { 397 return false; 398 } 399 400 if (!disableConditions.getConditions().isEmpty()) 401 { 402 // a condition has been found 403 return true; 404 } 405 406 return disableConditions.getSubConditions() 407 .stream() 408 // search for a non empty sub condition 409 .anyMatch(ModelHelper::hasDisableConditions); 410 } 411 412 /** 413 * Validates the given value 414 * @param definition The definition to use to validate the value 415 * @param value the value to validate 416 * @return the structure with errors information if the validation failed. 417 */ 418 public static ValidationResult validateValue(ElementDefinition definition, Object value) 419 { 420 return validateValue(definition, value, true); 421 } 422 423 /** 424 * Validates the given value 425 * @param definition The definition to use to validate the value 426 * @param value the value to validate 427 * @param checkEnumerated <code>true</code> true to make sure that the item with an enumerator has its value in the enumerated values 428 * @return the structure with errors information if the validation failed. 429 * TODO NEWATTRIBUTEAPI RUNTIME-2897: remove this method to always check enumerator when validating a value 430 */ 431 public static ValidationResult validateValue(ElementDefinition definition, Object value, boolean checkEnumerated) 432 { 433 ValidationResult result = new ValidationResult(); 434 435 ElementType type = definition.getType(); 436 if (value != null && !type.isCompatible(value)) 437 { 438 result.addError(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED")); 439 } 440 else 441 { 442 Validator validator = definition.getValidator(); 443 if (validator != null) 444 { 445 result.addResult(validator.validate(value)); 446 } 447 448 if (checkEnumerated) 449 { 450 if (definition.isMultiple()) 451 { 452 Object[] values = (Object[]) value; 453 if (values != null) 454 { 455 for (Object singleValue : values) 456 { 457 result.addResult(_validateEnumeration(definition, singleValue)); 458 } 459 } 460 461 } 462 else 463 { 464 result.addResult(_validateEnumeration(definition, value)); 465 } 466 } 467 } 468 469 return result; 470 } 471 472 private static ValidationResult _validateEnumeration(ElementDefinition definition, Object value) 473 { 474 ValidationResult result = new ValidationResult(); 475 476 if (_checkValueEnumeration(value)) 477 { 478 // Make sure that the item with an enumerator has its value in the enumerated values 479 Enumerator<Object> enumerator = definition.getEnumerator(); 480 if (enumerator != null) 481 { 482 try 483 { 484 I18nizableText entry = enumerator.getEntry(value); 485 if (entry == null) 486 { 487 result.addError(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED")); 488 } 489 } 490 catch (Exception e) 491 { 492 __LOGGER.warn("An error occured while checking enumerated value '{}' for '{}'", value, enumerator, e); 493 result.addError(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_LED_TO_EXCEPTION")); 494 } 495 } 496 } 497 498 return result; 499 } 500 501 private static boolean _checkValueEnumeration(Object value) 502 { 503 if (value == null) 504 { 505 return false; 506 } 507 else 508 { 509 return value instanceof String ? StringUtils.isNotEmpty((String) value) : true; 510 } 511 } 512}