001/*
002 *  Copyright 2018 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;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.List;
021import java.util.Map;
022
023import org.apache.commons.lang3.tuple.ImmutablePair;
024import org.apache.commons.lang3.tuple.Pair;
025import org.slf4j.Logger;
026
027import org.ametys.runtime.config.DisableCondition;
028import org.ametys.runtime.config.DisableCondition.OPERATOR;
029import org.ametys.runtime.config.DisableConditions;
030import org.ametys.runtime.i18n.I18nizableText;
031import org.ametys.runtime.model.type.ElementType;
032import org.ametys.runtime.parameter.Errors;
033import org.ametys.runtime.parameter.Validator;
034
035/**
036 * Helper class for models
037 */
038public final class ModelHelper
039{
040    private ModelHelper()
041    {
042        // Empty constructor
043    }
044    
045    /**
046     * Checks if this item is in a group with a switch on
047     * @param modelItem the item to check
048     * @param values all items' values to get switchers' values
049     * @return false if this item is part of a group with a switch to off, true otherwise 
050     */
051    public static boolean isGroupSwitchOn(ModelItem modelItem, Map<String, Object> values)
052    {
053        Pair<Boolean, ElementDefinition> isGroupActive = _isModelItemGroupActive(modelItem.getParent(), values);
054        if (isGroupActive.getKey())
055        {
056            return true;
057        }
058        return modelItem.equals(isGroupActive.getValue());
059    }
060    
061    private static Pair<Boolean, ElementDefinition> _isModelItemGroupActive(ModelItemGroup group, Map<String, Object> values)
062    {
063        if (group == null)
064        {
065            return new ImmutablePair<>(true, null);
066        }
067        
068        ElementDefinition<Boolean> groupSwitch = group.getSwitcher();
069        if (groupSwitch == null)
070        {
071            return _isModelItemGroupActive(group.getParent(), values);
072        }
073        
074        Object value = values.get(groupSwitch.getName());
075        
076        if (value == null)
077        {
078            value = groupSwitch.getDefaultValue();
079        }
080        
081        if (!(value instanceof Boolean))
082        {
083            throw new IllegalStateException("The switcher value of group " + group.getName() + " is null or not a boolean");
084        }
085        
086        return (Boolean) value ? _isModelItemGroupActive(group.getParent(), values) : new ImmutablePair<>(false, groupSwitch);
087    }
088    
089    /**
090     * Recursively evaluate the {@link DisableConditions} against the given values
091     * @param disableConditions the disable conditions to evaluate
092     * @param definitionAndValues the values to evaluate
093     * @param logger the logger for disable conditions evaluation logs
094     * @return true if the disable conditions are true, false otherwise
095     */
096    public static boolean evaluateDisableConditions(DisableConditions disableConditions, Map<String, DefinitionAndValue> definitionAndValues, Logger logger)
097    {
098        if (!_hasDisableConditions(disableConditions))
099        {
100            return false;
101        }
102        
103        boolean disabled;
104        boolean andOperator = disableConditions.getAssociationType() == DisableConditions.ASSOCIATION_TYPE.AND;
105        
106        // initial value depends on OR or AND associations
107        disabled = andOperator;
108        
109        for (DisableConditions subConditions : disableConditions.getSubConditions())
110        {
111            boolean result = evaluateDisableConditions(subConditions, definitionAndValues, logger);
112            disabled = andOperator ?  disabled && result : disabled || result;
113        }
114        
115        for (DisableCondition condition : disableConditions.getConditions())
116        {
117            boolean result = _evaluateCondition(condition, definitionAndValues, logger);
118            disabled = andOperator ?  disabled && result : disabled || result;
119        }
120                
121        return disabled;
122    }
123
124    private static boolean _hasDisableConditions(DisableConditions disableConditions)
125    {
126        return disableConditions != null && !disableConditions.getConditions().isEmpty() && disableConditions.getSubConditions().isEmpty();
127    }
128    
129    private static boolean _evaluateCondition(DisableCondition condition, Map<String, DefinitionAndValue> definitionAndValues, Logger logger)
130    {
131        String id = condition.getId();
132        DisableCondition.OPERATOR operator = condition.getOperator();
133        String conditionValue = condition.getValue();
134        
135        DefinitionAndValue definitionAndValue = definitionAndValues.get(id);
136        if (definitionAndValue == null || !(definitionAndValue.getDefinition() instanceof ElementDefinition))
137        {
138            logger.debug("Cannot evaluate the disable condition on the undefined element {}.\nReturning false.", id);
139            return false;
140        }
141        
142        ElementType type = ((ElementDefinition) definitionAndValue.getDefinition()).getType();
143        Object compareValue = type.castValue(conditionValue);
144        if (compareValue == null)
145        {
146            throw new IllegalStateException("Cannot convert the condition value '" + conditionValue + "' to a '" + type.getId() + "' for model item '" + id + "'");
147        }
148        
149        try
150        {
151            Object value = definitionAndValue.getValue();
152            if (value instanceof Collection)
153            {
154                if (operator == OPERATOR.EQ)
155                {
156                    for (Object v: (Collection) value)
157                    {
158                        if (_evaluateConditionValue(v, operator, compareValue))
159                        {
160                            // One entry matches
161                            return true;
162                        }
163                    }
164                
165                    return false;
166                }
167                else
168                {
169                    for (Object v: (Collection) value)
170                    {
171                        if (!_evaluateConditionValue(v, operator, compareValue))
172                        {
173                            // One entry does not match
174                            return false;
175                        }
176                    }
177                
178                    return true;
179                }
180            }
181            else
182            {
183                return _evaluateConditionValue(value, operator, compareValue);
184            }
185        }
186        catch (Exception e)
187        {
188            throw new IllegalStateException("An error occurred while comparing values in type'" + type + "' for model item '" + id + "'.", e);
189        }
190    }
191    private static boolean _evaluateConditionValue(Object value, DisableCondition.OPERATOR operator, Object compareValue)
192    {
193        if ((value == null || value instanceof Comparable) && (compareValue == null || compareValue instanceof Comparable))
194        {
195            @SuppressWarnings("unchecked")
196            Comparable<Object> comparableParameterValue = (Comparable<Object>) _emptyStringToNull(value);
197            @SuppressWarnings("unchecked")
198            Comparable<Object> comparableCompareValue = (Comparable<Object>) _emptyStringToNull(compareValue);
199            
200            int comparison;
201            if (comparableParameterValue != null && comparableCompareValue != null)
202            {
203                comparison = comparableParameterValue.compareTo(comparableCompareValue);
204            }
205            else if (comparableCompareValue != null)
206            {
207                comparison = -1; // null comparableParameterValue is considered less than non-null comparableCompareValue
208            }
209            else if (comparableParameterValue != null)
210            {
211                comparison = 1; // non-null comparableParameterValue is considered greater than null comparableCompareValue
212            }
213            else
214            {
215                comparison = 0; // both are null
216            }
217            
218            switch (operator)
219            {
220                case NEQ:
221                    return comparison != 0;
222                case GEQ:
223                    return comparison >= 0;
224                case GT:
225                    return comparison > 0;
226                case LT:
227                    return comparison < 0;
228                case LEQ:
229                    return comparison <= 0;
230                case EQ:
231                default:
232                    return comparison == 0;
233            }
234        }
235        else
236        {
237            throw new IllegalStateException("Values '" + value + "' and '" + compareValue + "' are not comparable");
238        }
239    }
240    
241    private static Object _emptyStringToNull(Object compareValue)
242    {
243        if ("".equals(compareValue))
244        {
245            return null;
246        }
247        else
248        {
249            return  compareValue;
250        }
251    }
252
253    /**
254     * Validates the given value
255     * @param definition The definition to use to validate the value
256     * @param value the value to validate
257     * @return the structure with errors information if the validation failed.
258     */
259    public static List<I18nizableText> validateValue(ElementDefinition definition, Object value)
260    {
261        return validateValue(definition, value, true);
262    }
263    
264    /**
265     * Validates the given value
266     * @param definition The definition to use to validate the value
267     * @param value the value to validate
268     * @param checkEnumerated <code>true</code> true to make sure that the item with an enumerator has its value in the enumerated values
269     * @return the structure with errors information if the validation failed.
270     * TODO NEWATTRIBUTEAPI RUNTIME-2897: remove this method to always check enumerator when validating a value 
271     */
272    public static List<I18nizableText> validateValue(ElementDefinition definition, Object value, boolean checkEnumerated)
273    {
274        List<I18nizableText> errorsList = new ArrayList<>();
275        Errors errors = new Errors();
276
277        ElementType type = definition.getType();
278        if (value != null && !type.getManagedClass().isInstance(value) && !type.getManagedClassArray().isInstance(value))
279        {
280            errorsList.add(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED"));
281        }
282        
283        Validator validator = definition.getValidator();
284        if (validator != null)
285        {
286            validator.validate(value, errors);
287            if (errors.hasErrors())
288            {
289                errorsList.addAll(errors.getErrors());
290            }
291        }
292        
293        if (value != null && checkEnumerated)
294        {
295            // Make sure that the item with an enumerator has its value in the enumerated values
296            Enumerator<Object> enumerator = definition.getEnumerator();
297            if (enumerator != null)
298            {
299                I18nizableText entry = null;
300                try
301                {
302                    entry = enumerator.getEntry(value);
303                }
304                catch (Exception e)
305                {
306                    errorsList.add(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_LED_TO_EXCEPTION"));
307                }
308                
309                if (entry == null)
310                {
311                    errorsList.add(new I18nizableText("plugin.core", "PLUGINS_CORE_ELEMENT_DEFINITION_VALUE_NOT_ALLOWED"));
312                }
313            }
314        }
315            
316        return errorsList;
317    }
318}