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.List; 021import java.util.Map; 022 023import org.apache.commons.lang3.tuple.ImmutablePair; 024import org.apache.commons.lang3.tuple.Pair; 025import org.slf4j.Logger; 026 027import org.ametys.runtime.config.DisableCondition; 028import org.ametys.runtime.config.DisableCondition.OPERATOR; 029import org.ametys.runtime.config.DisableConditions; 030import org.ametys.runtime.i18n.I18nizableText; 031import org.ametys.runtime.model.type.ElementType; 032import org.ametys.runtime.parameter.Errors; 033import org.ametys.runtime.parameter.Validator; 034 035/** 036 * Helper class for models 037 */ 038public final class ModelHelper 039{ 040 private ModelHelper() 041 { 042 // Empty constructor 043 } 044 045 /** 046 * Checks if this item is in a group with a switch on 047 * @param modelItem the item to check 048 * @param values all items' values to get switchers' values 049 * @return false if this item is part of a group with a switch to off, true otherwise 050 */ 051 public static boolean isGroupSwitchOn(ModelItem modelItem, Map<String, Object> values) 052 { 053 Pair<Boolean, ElementDefinition> isGroupActive = _isModelItemGroupActive(modelItem.getParent(), values); 054 if (isGroupActive.getKey()) 055 { 056 return true; 057 } 058 return modelItem.equals(isGroupActive.getValue()); 059 } 060 061 private static Pair<Boolean, ElementDefinition> _isModelItemGroupActive(ModelItemGroup group, Map<String, Object> values) 062 { 063 if (group == null) 064 { 065 return new ImmutablePair<>(true, null); 066 } 067 068 ElementDefinition<Boolean> groupSwitch = group.getSwitcher(); 069 if (groupSwitch == null) 070 { 071 return _isModelItemGroupActive(group.getParent(), values); 072 } 073 074 Object value = values.get(groupSwitch.getName()); 075 076 if (value == null) 077 { 078 value = groupSwitch.getDefaultValue(); 079 } 080 081 if (!(value instanceof Boolean)) 082 { 083 throw new IllegalStateException("The switcher value of group " + group.getName() + " is null or not a boolean"); 084 } 085 086 return (Boolean) value ? _isModelItemGroupActive(group.getParent(), values) : new ImmutablePair<>(false, groupSwitch); 087 } 088 089 /** 090 * Recursively evaluate the {@link DisableConditions} against the given values 091 * @param disableConditions the disable conditions to evaluate 092 * @param definitionAndValues the values to evaluate 093 * @param logger the logger for disable conditions evaluation logs 094 * @return true if the disable conditions are true, false otherwise 095 */ 096 public static boolean evaluateDisableConditions(DisableConditions disableConditions, Map<String, DefinitionAndValue> definitionAndValues, Logger logger) 097 { 098 if (!_hasDisableConditions(disableConditions)) 099 { 100 return false; 101 } 102 103 boolean disabled; 104 boolean andOperator = disableConditions.getAssociationType() == DisableConditions.ASSOCIATION_TYPE.AND; 105 106 // initial value depends on OR or AND associations 107 disabled = andOperator; 108 109 for (DisableConditions subConditions : disableConditions.getSubConditions()) 110 { 111 boolean result = evaluateDisableConditions(subConditions, definitionAndValues, logger); 112 disabled = andOperator ? disabled && result : disabled || result; 113 } 114 115 for (DisableCondition condition : disableConditions.getConditions()) 116 { 117 boolean result = _evaluateCondition(condition, definitionAndValues, logger); 118 disabled = andOperator ? disabled && result : disabled || result; 119 } 120 121 return disabled; 122 } 123 124 private static boolean _hasDisableConditions(DisableConditions disableConditions) 125 { 126 return disableConditions != null && !disableConditions.getConditions().isEmpty() && disableConditions.getSubConditions().isEmpty(); 127 } 128 129 private static boolean _evaluateCondition(DisableCondition condition, Map<String, DefinitionAndValue> definitionAndValues, Logger logger) 130 { 131 String id = condition.getId(); 132 DisableCondition.OPERATOR operator = condition.getOperator(); 133 String conditionValue = condition.getValue(); 134 135 DefinitionAndValue definitionAndValue = definitionAndValues.get(id); 136 if (definitionAndValue == null || !(definitionAndValue.getDefinition() instanceof ElementDefinition)) 137 { 138 logger.debug("Cannot evaluate the disable condition on the undefined element {}.\nReturning false.", id); 139 return false; 140 } 141 142 ElementType type = ((ElementDefinition) definitionAndValue.getDefinition()).getType(); 143 Object compareValue = type.castValue(conditionValue); 144 if (compareValue == null) 145 { 146 throw new IllegalStateException("Cannot convert the condition value '" + conditionValue + "' to a '" + type.getId() + "' for model item '" + id + "'"); 147 } 148 149 try 150 { 151 Object value = definitionAndValue.getValue(); 152 if (value instanceof Collection) 153 { 154 if (operator == OPERATOR.EQ) 155 { 156 for (Object v: (Collection) value) 157 { 158 if (_evaluateConditionValue(v, operator, compareValue)) 159 { 160 // One entry matches 161 return true; 162 } 163 } 164 165 return false; 166 } 167 else 168 { 169 for (Object v: (Collection) value) 170 { 171 if (!_evaluateConditionValue(v, operator, compareValue)) 172 { 173 // One entry does not match 174 return false; 175 } 176 } 177 178 return true; 179 } 180 } 181 else 182 { 183 return _evaluateConditionValue(value, operator, compareValue); 184 } 185 } 186 catch (Exception e) 187 { 188 throw new IllegalStateException("An error occurred while comparing values in type'" + type + "' for model item '" + id + "'.", e); 189 } 190 } 191 private static boolean _evaluateConditionValue(Object value, DisableCondition.OPERATOR operator, Object compareValue) 192 { 193 if ((value == null || value instanceof Comparable) && (compareValue == null || compareValue instanceof Comparable)) 194 { 195 @SuppressWarnings("unchecked") 196 Comparable<Object> comparableParameterValue = (Comparable<Object>) _emptyStringToNull(value); 197 @SuppressWarnings("unchecked") 198 Comparable<Object> comparableCompareValue = (Comparable<Object>) _emptyStringToNull(compareValue); 199 200 int comparison; 201 if (comparableParameterValue != null && comparableCompareValue != null) 202 { 203 comparison = comparableParameterValue.compareTo(comparableCompareValue); 204 } 205 else if (comparableCompareValue != null) 206 { 207 comparison = -1; // null comparableParameterValue is considered less than non-null comparableCompareValue 208 } 209 else if (comparableParameterValue != null) 210 { 211 comparison = 1; // non-null comparableParameterValue is considered greater than null comparableCompareValue 212 } 213 else 214 { 215 comparison = 0; // both are null 216 } 217 218 switch (operator) 219 { 220 case NEQ: 221 return comparison != 0; 222 case GEQ: 223 return comparison >= 0; 224 case GT: 225 return comparison > 0; 226 case LT: 227 return comparison < 0; 228 case LEQ: 229 return comparison <= 0; 230 case EQ: 231 default: 232 return comparison == 0; 233 } 234 } 235 else 236 { 237 throw new IllegalStateException("Values '" + value + "' and '" + compareValue + "' are not comparable"); 238 } 239 } 240 241 private static Object _emptyStringToNull(Object compareValue) 242 { 243 if ("".equals(compareValue)) 244 { 245 return null; 246 } 247 else 248 { 249 return compareValue; 250 } 251 } 252 253 /** 254 * Validates the given value 255 * @param definition The definition to use to validate the value 256 * @param value the value to validate 257 * @return the structure with errors information if the validation failed. 258 */ 259 public static List<I18nizableText> validateValue(ElementDefinition definition, Object value) 260 { 261 return validateValue(definition, value, true); 262 } 263 264 /** 265 * Validates the given value 266 * @param definition The definition to use to validate the value 267 * @param value the value to validate 268 * @param checkEnumerated <code>true</code> true to make sure that the item with an enumerator has its value in the enumerated values 269 * @return the structure with errors information if the validation failed. 270 * TODO NEWATTRIBUTEAPI RUNTIME-2897: remove this method to always check enumerator when validating a value 271 */ 272 public static List<I18nizableText> validateValue(ElementDefinition definition, Object value, boolean checkEnumerated) 273 { 274 List<I18nizableText> errorsList = new ArrayList<>(); 275 Errors errors = new Errors(); 276 277 ElementType type = definition.getType(); 278 if (value != null && !type.getManagedClass().isInstance(value) && !type.getManagedClassArray().isInstance(value)) 279 { 280 errorsList.add(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED")); 281 } 282 283 Validator validator = definition.getValidator(); 284 if (validator != null) 285 { 286 validator.validate(value, errors); 287 if (errors.hasErrors()) 288 { 289 errorsList.addAll(errors.getErrors()); 290 } 291 } 292 293 if (value != null && checkEnumerated) 294 { 295 // Make sure that the item with an enumerator has its value in the enumerated values 296 Enumerator<Object> enumerator = definition.getEnumerator(); 297 if (enumerator != null) 298 { 299 I18nizableText entry = null; 300 try 301 { 302 entry = enumerator.getEntry(value); 303 } 304 catch (Exception e) 305 { 306 errorsList.add(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_LED_TO_EXCEPTION")); 307 } 308 309 if (entry == null) 310 { 311 errorsList.add(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED")); 312 } 313 } 314 } 315 316 return errorsList; 317 } 318}