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