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}