001/* 002 * Copyright 2024 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.disableconditions; 017 018import java.util.Arrays; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023 024import org.apache.avalon.framework.component.Component; 025import org.apache.commons.lang3.StringUtils; 026 027import org.ametys.runtime.model.ElementDefinition; 028import org.ametys.runtime.model.ModelHelper; 029import org.ametys.runtime.model.ModelItem; 030import org.ametys.runtime.model.ModelItemAccessor; 031import org.ametys.runtime.model.disableconditions.DisableCondition.OPERATOR; 032import org.ametys.runtime.model.exception.BadItemTypeException; 033import org.ametys.runtime.model.exception.UndefinedItemPathException; 034import org.ametys.runtime.model.type.ElementType; 035import org.ametys.runtime.plugin.component.AbstractLogEnabled; 036 037/** 038 * Abstract evaluator for {@link DisableConditions} 039 * @param <T> Type of object holding the data to evaluate 040 */ 041public abstract class AbstractDisableConditionsEvaluator<T extends Object> extends AbstractLogEnabled implements Component, DisableConditionsEvaluator<T> 042{ 043 public boolean evaluateDisableConditions(ModelItem definition, String dataPath, Map<String, Object> values) throws UndefinedItemPathException, BadItemTypeException 044 { 045 return evaluateDisableConditions(definition, definition.getDisableConditions(), dataPath, Optional.empty(), values, Optional.empty(), new HashMap<>()); 046 } 047 048 public boolean evaluateDisableConditions(ModelItem definition, String dataPath, T object) throws UndefinedItemPathException, BadItemTypeException 049 { 050 return evaluateDisableConditions(definition, definition.getDisableConditions(), dataPath, Optional.empty(), Map.of(), Optional.ofNullable(object), new HashMap<>()); 051 } 052 053 public boolean evaluateDisableConditions(ModelItem definition, String dataPath, Optional<String> oldDataPath, Map<String, Object> values, T object, Map<String, Object> contextualParameters) throws UndefinedItemPathException, BadItemTypeException 054 { 055 return evaluateDisableConditions(definition, definition.getDisableConditions(), dataPath, oldDataPath, values, Optional.ofNullable(object), contextualParameters); 056 } 057 058 /** 059 * Recursively evaluate the given {@link DisableConditions} against the given values 060 * @param definition the definition of the evaluated data 061 * @param disableConditions the conditions to evaluate 062 * @param dataPath the path of the evaluated data. Needed to get the value to compare as condition ids are relative to this one 063 * @param oldDataPath the old path of the evaluated data. Needed to get stored value if the data has been moved 064 * @param values values to check conditions on 065 * @param object the object holding the data to evaluate and the condition value 066 * @param contextualParameters the contextual parameters 067 * @return <code>true</code> if the disable conditions are <code>true</code>, <code>false</code> otherwise 068 * @throws UndefinedItemPathException If no item is found corresponding to one of the given conditions 069 * @throws BadItemTypeException If the item referenced by one of the conditions is not an element 070 */ 071 protected boolean evaluateDisableConditions(ModelItem definition, DisableConditions disableConditions, String dataPath, Optional<String> oldDataPath, Map<String, Object> values, Optional<T> object, Map<String, Object> contextualParameters) throws UndefinedItemPathException, BadItemTypeException 072 { 073 if (!ModelHelper.hasDisableConditions(disableConditions)) 074 { 075 return false; 076 } 077 078 boolean andOperator = disableConditions.getAssociationType() == DisableConditions.ASSOCIATION_TYPE.AND; 079 080 // initial value depends on OR or AND associations 081 boolean disabled = andOperator; 082 083 for (DisableConditions subConditions : disableConditions.getSubConditions()) 084 { 085 boolean result = evaluateDisableConditions(definition, subConditions, dataPath, oldDataPath, values, object, contextualParameters); 086 if (_resultIsSufficient(andOperator, result)) 087 { 088 return result; 089 } 090 else 091 { 092 disabled = andOperator ? disabled && result : disabled || result; 093 } 094 } 095 096 for (DisableCondition condition : disableConditions.getConditions()) 097 { 098 boolean result = evaluateDisableCondition(definition, condition, dataPath, oldDataPath, values, object, contextualParameters); 099 if (_resultIsSufficient(andOperator, result)) 100 { 101 return result; 102 } 103 else 104 { 105 disabled = andOperator ? disabled && result : disabled || result; 106 } 107 } 108 109 return disabled; 110 } 111 112 /** 113 * Check if the given result is sufficient to evaluate the current disable condition 114 * @param andOperator the conditions operator 115 * @param result the result of the current condition 116 * @return <code>true</code> if the result of evaluation is sufficient, <code>false</code> otherwise 117 */ 118 protected boolean _resultIsSufficient(boolean andOperator, boolean result) 119 { 120 return andOperator && !result || !andOperator && result; 121 } 122 123 /** 124 * Evaluate the given {@link DisableCondition} against the given values 125 * @param definition the definition of the evaluated data 126 * @param condition the condition to evaluate 127 * @param dataPath the path of the evaluated data. Needed to get the value to compare as condition ids are relative to this one 128 * @param oldDataPath the old path of the evaluated data. Needed to get stored value if the data has been moved 129 * @param values values to check conditions on 130 * @param object the object holding the data to evaluate and the condition value 131 * @param contextualParameters the contextual parameters 132 * @return <code>true</code> if the disable condition is <code>true</code>, <code>false</code> otherwise 133 * @throws UndefinedItemPathException If no item is found corresponding to the given condition 134 * @throws BadItemTypeException If the item referenced by the condition is not an element 135 */ 136 protected boolean evaluateDisableCondition(ModelItem definition, DisableCondition condition, String dataPath, Optional<String> oldDataPath, Map<String, Object> values, Optional<T> object, Map<String, Object> contextualParameters) throws UndefinedItemPathException, BadItemTypeException 137 { 138 DefinitionAndValue definitionAndValue = _getConditionDefinitionAndValue(definition, condition, dataPath, oldDataPath, values, object, contextualParameters); 139 ElementDefinition conditionDefinition = definitionAndValue.definition(); 140 ElementType type = conditionDefinition.getType(); 141 String valueFromCondition = condition.getValue(); 142 143 try 144 { 145 Object typedvalueFromCondition = type.castValue(valueFromCondition); 146 Object value = definitionAndValue.value(); 147 148 DisableCondition.OPERATOR operator = condition.getOperator(); 149 if (value != null && value.getClass().isArray()) 150 { 151 if (operator == OPERATOR.EQ) 152 { 153 return Arrays.stream((Object[]) value) 154 .anyMatch(v -> evaluateDisableConditionValue(typedvalueFromCondition, operator, v)); 155 } 156 else 157 { 158 return Arrays.stream((Object[]) value) 159 .allMatch(v -> evaluateDisableConditionValue(typedvalueFromCondition, operator, v)); 160 } 161 } 162 else 163 { 164 return evaluateDisableConditionValue(typedvalueFromCondition, operator, value); 165 } 166 } 167 catch (BadItemTypeException e) 168 { 169 throw new IllegalStateException("Cannot convert the condition value '" + valueFromCondition + "' to a '" + type.getId() + "' for model item '" + condition.getId() + "'", e); 170 } 171 catch (Exception e) 172 { 173 throw new IllegalStateException("An error occurred while comparing values in type '" + type.getId() + "' for model item '" + condition.getId() + "'.", e); 174 } 175 } 176 177 /** 178 * Retrieve the value corresponding to the identifier of the condition, and its type 179 * @param object the object holding the data to evaluate and the condition value 180 * @param definition the definition containing the condition 181 * @param condition the condition 182 * @param dataPath the path of the evaluated data 183 * @param oldDataPath the old path of the evaluated data. Needed to get stored value if the data has been moved 184 * @param values values to check conditions on 185 * @param contextualParameters the contextual parameters 186 * @return the {@link DefinitionAndValue} corresponding to the condition identifier 187 * @throws UndefinedItemPathException If no item is found with the given conditionId 188 * @throws BadItemTypeException If the item referenced by the conditionId is not an element 189 */ 190 protected DefinitionAndValue _getConditionDefinitionAndValue(ModelItem definition, DisableCondition condition, String dataPath, Optional<String> oldDataPath, Map<String, Object> values, Optional<T> object, Map<String, Object> contextualParameters) throws UndefinedItemPathException, BadItemTypeException 191 { 192 String conditionAbsoluteDataPath = ModelHelper.getDisableConditionAbsolutePath(condition, dataPath); 193 Optional<String> oldConditionAbsoluteDataPath = oldDataPath.map(path -> ModelHelper.getDisableConditionAbsolutePath(condition, path)); 194 ModelItem conditionModelItem = getModelItem(definition.getModel(), conditionAbsoluteDataPath, contextualParameters); 195 if (!(conditionModelItem instanceof ElementDefinition conditionDefinition)) 196 { 197 throw new BadItemTypeException("Unable to evaluate disable condition '" + conditionAbsoluteDataPath + "'. This condition referenced does not reference an element"); 198 } 199 200 Object conditionValue = containsValue(definition.getModel(), conditionAbsoluteDataPath, values, contextualParameters) 201 ? getValueFromMap(definition.getModel(), conditionAbsoluteDataPath, values, contextualParameters) 202 : object.isPresent() 203 ? getStoredValue(conditionAbsoluteDataPath, oldConditionAbsoluteDataPath, object.get(), contextualParameters) 204 : null; 205 206 return new DefinitionAndValue(conditionDefinition, conditionValue); 207 } 208 209 /** 210 * Retrieves the model item 211 * @param modelItemAccessor the relative accessor to the given data path 212 * @param dataPath the data path of the model item to retrieve 213 * @return the model item 214 * @param contextualParameters the contextual parameters 215 * @throws UndefinedItemPathException If no item is found for the given path 216 */ 217 protected ModelItem getModelItem(ModelItemAccessor modelItemAccessor, String dataPath, Map<String, Object> contextualParameters) throws UndefinedItemPathException 218 { 219 return ModelHelper.getModelItem(dataPath, List.of(modelItemAccessor)); 220 } 221 222 /** 223 * Check if the values {@link Map} contains a value for the condition 224 * @param modelItemAccessor the relative accessor to the given condition path 225 * @param conditionDataPath the absolute data path of the condition 226 * @param values the values {@link Map} 227 * @param contextualParameters the contextual parameters 228 * @return <code>true</code> if there is a value (even empty) for the condition in the values {@link Map}, <code>false</code> otherwise 229 */ 230 protected abstract boolean containsValue(ModelItemAccessor modelItemAccessor, String conditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters); 231 232 /** 233 * Retrieves the condition value from the values {@link Map} 234 * @param modelItemAccessor the relative accessor to the given condition path 235 * @param conditionDataPath the absolute data path of the condition 236 * @param values the values {@link Map} 237 * @param contextualParameters the contextual parameters 238 * @return the condition value found in the values {@link Map} 239 */ 240 protected abstract Object getValueFromMap(ModelItemAccessor modelItemAccessor, String conditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters); 241 242 /** 243 * Retrieves the condition value from data stored in the given object 244 * @param conditionDataPath the absolute data path of the condition 245 * @param oldConditionDataPath the absolute old data path of the condition 246 * @param object the object holding the data to evaluate and the condition value 247 * @param contextualParameters the contextual parameters 248 * @return the condition value from data stored in the object 249 */ 250 protected abstract Object getStoredValue(String conditionDataPath, Optional<String> oldConditionDataPath, T object, Map<String, Object> contextualParameters); 251 252 /** 253 * Evaluate the given condition value against the given value 254 * @param conditionValue the condition value 255 * @param conditionOperator the condition operator 256 * @param value the value to check 257 * @return <code>true</code> if the condition is <code>true</code>, <code>false</code> otherwise 258 */ 259 protected boolean evaluateDisableConditionValue(Object conditionValue, DisableCondition.OPERATOR conditionOperator, Object value) 260 { 261 if ((value == null || value instanceof Comparable) && (conditionValue == null || conditionValue instanceof Comparable)) 262 { 263 @SuppressWarnings("unchecked") 264 Comparable<Object> comparableParameterValue = (Comparable<Object>) _emptyStringToNull(value); 265 @SuppressWarnings("unchecked") 266 Comparable<Object> comparableCompareValue = (Comparable<Object>) _emptyStringToNull(conditionValue); 267 268 int comparison; 269 if (comparableParameterValue != null && comparableCompareValue != null) 270 { 271 comparison = comparableParameterValue.compareTo(comparableCompareValue); 272 } 273 else if (comparableCompareValue != null) 274 { 275 comparison = -1; // null comparableParameterValue is considered less than non-null comparableCompareValue 276 } 277 else if (comparableParameterValue != null) 278 { 279 comparison = 1; // non-null comparableParameterValue is considered greater than null comparableCompareValue 280 } 281 else 282 { 283 comparison = 0; // both are null 284 } 285 286 switch (conditionOperator) 287 { 288 case NEQ: 289 return comparison != 0; 290 case GEQ: 291 return comparison >= 0; 292 case GT: 293 return comparison > 0; 294 case LT: 295 return comparison < 0; 296 case LEQ: 297 return comparison <= 0; 298 case EQ: 299 default: 300 return comparison == 0; 301 } 302 } 303 else 304 { 305 throw new IllegalStateException("Values '" + value + "' and '" + conditionValue + "' are not comparable"); 306 } 307 } 308 309 /** 310 * Convert empty string to null object, or retrieve the given value 311 * @param value the value 312 * @return null if the given value is an empty string, the value itself otherwise 313 */ 314 protected static Object _emptyStringToNull(Object value) 315 { 316 if (StringUtils.EMPTY.equals(value)) 317 { 318 return null; 319 } 320 else 321 { 322 return value; 323 } 324 } 325 326 /** 327 * Definition and value corresponding to a {@link DisableCondition} 328 * @param definition {@link ElementDefinition} corresponding to the value 329 * @param value the value 330 */ 331 protected record DefinitionAndValue(ElementDefinition definition, Object value) { /* empty */ } 332}