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