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