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}