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}