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.Comparator;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.TreeSet;
026
027import org.slf4j.Logger;
028
029import org.ametys.runtime.i18n.I18nizableText;
030
031/**
032 * Helper for {@link CategorizedElementDefinitionProxy}
033 */
034public final class CategorizedElementDefinitionHelper
035{
036    private CategorizedElementDefinitionHelper()
037    {
038        //Nothing
039    }
040    
041    /**
042     * Get definitions of all elements parsed in this {@link CategorizedElementDefinitionProxy} {@link Collection}, as a flat map.
043     * @param elements a {@link Collection} of {@link CategorizedElementDefinitionProxy}
044     * @return a map with the definition name as key, containing all definitions
045     */
046    public static Map<String, ElementDefinition> getFlatDefinitions(Collection<CategorizedElementDefinitionProxy> elements)
047    {
048        Map<String, ElementDefinition> flatDefinitions = new HashMap<>();
049        for (CategorizedElementDefinitionProxy element : elements)
050        {
051            ElementDefinition definition = element.getDefinition();
052            flatDefinitions.put(definition.getName(), definition);
053        }
054        return flatDefinitions;
055    }
056    
057    /**
058     * Categorize the ModlItems based on the informations in the {@link CategorizedElementDefinitionProxy} collection
059     * Category and groups are not sorted, only definitions, based on the position.
060     * Sorting of category and groups have to be done when the view is created (each usage can be different)
061     * @param elements collection of categorized elements
062     * @return a list of {@link ModelItem} with the correct tree (category/group/definitions)
063     */
064    public static List<ModelItem> categorize(Collection<CategorizedElementDefinitionProxy> elements)
065    {
066        Map<I18nizableText, Map<I18nizableText, List<CategorizedElementDefinitionProxy>>> categories = categorizeToMap(elements);
067        //This list is not sorted, only definition are sorted, not groups.
068        //Groups should be sorted elsewhere (when creating the view for example)
069        List<ModelItem> modelItems = new ArrayList<>();
070        for (Entry<I18nizableText, Map<I18nizableText, List<CategorizedElementDefinitionProxy>>> categoryEntry : categories.entrySet())
071        {
072            ModelItemGroup category = new ModelItemGroup();
073            category.setLabel(categoryEntry.getKey());
074            modelItems.add(category);
075            Map<I18nizableText, List<CategorizedElementDefinitionProxy>> values = categoryEntry.getValue();
076            for (Entry<I18nizableText, List<CategorizedElementDefinitionProxy>> groupEntry : values.entrySet())
077            {
078                ModelItemGroup group = new ModelItemGroup();
079                group.setLabel(groupEntry.getKey());
080                category.addChild(group);
081                
082                Collection<CategorizedElementDefinitionProxy> orderedDefinitions = orderDefinitions(groupEntry.getValue());
083                for (CategorizedElementDefinitionProxy orderedDefinition : orderedDefinitions)
084                {
085                    ElementDefinition definition = orderedDefinition.getDefinition();
086                    definition.setParent(group);
087                    
088                    group.addChild(definition, orderedDefinition.isGroupSwitch());
089                }
090            }
091        }
092        return modelItems;
093    }
094    
095    /**
096     * Organize a collection of skin parameters by categories and groups.
097     * @param elements a collection of elements.
098     * @return a Map of parameters sorted first by category then group.
099     */
100    protected static Map<I18nizableText, Map<I18nizableText, List<CategorizedElementDefinitionProxy>>> categorizeToMap(Collection<CategorizedElementDefinitionProxy> elements)
101    {
102        Map<I18nizableText, Map<I18nizableText, List<CategorizedElementDefinitionProxy>>> categories = new HashMap<>();
103
104        // Classify parameters by groups and categories
105        for (CategorizedElementDefinitionProxy parameter : elements)
106        {
107            I18nizableText displayCategory = parameter.getDisplayCategory();
108            I18nizableText displayGroup = parameter.getDisplayGroup();
109
110            // Get the map of groups of the category
111            Map<I18nizableText, List<CategorizedElementDefinitionProxy>> category = categories.get(displayCategory);
112            if (category == null)
113            {
114                category = new HashMap<>();
115                categories.put(displayCategory, category);
116            }
117
118            // Get the map of parameters of the group
119            List<CategorizedElementDefinitionProxy> group = category.get(displayGroup);
120            if (group == null)
121            {
122                group = new ArrayList<>();
123                category.put(displayGroup, group);
124            }
125
126            group.add(parameter);
127        }
128        
129        return categories;
130    }
131    
132    /**
133     * Order definitions, based on their position
134     * @param definitions {@link Collection} of {@link CategorizedElementDefinitionProxy}
135     * @return an ordered {@link Collection} of {@link CategorizedElementDefinitionProxy}
136     */
137    protected static Collection<CategorizedElementDefinitionProxy> orderDefinitions(Collection<CategorizedElementDefinitionProxy> definitions)
138    {
139        return orderDefinitions(definitions, null, null);
140    }
141    /**
142     * Order definitions, based on their position
143     * @param definitions {@link Collection} of {@link CategorizedElementDefinitionProxy}
144     * @param categoryLabel label of the category to filter (null to avoid filter)
145     * @param groupLabel label of the group (null to avoid filter)
146     * @return an ordered {@link Collection} of {@link CategorizedElementDefinitionProxy}
147     */
148    protected static Collection<CategorizedElementDefinitionProxy> orderDefinitions(Collection<CategorizedElementDefinitionProxy> definitions, I18nizableText categoryLabel, I18nizableText groupLabel)
149    {
150        Collection<CategorizedElementDefinitionProxy> orderedParameters = new TreeSet<>(new Comparator<CategorizedElementDefinitionProxy>()
151        {
152            public int compare(CategorizedElementDefinitionProxy o1, CategorizedElementDefinitionProxy o2)
153            {
154                int positionComparison = ((Long) o1.getPosition()).compareTo(o2.getPosition());
155                if (o1.getPosition() < 0 && o2.getPosition() < 0)
156                {
157                    positionComparison = 0;
158                }
159                else if (o1.getPosition() < 0 && o2.getPosition() >= 0)
160                {
161                    positionComparison = 1;
162                }
163                else if (o2.getPosition() < 0)
164                {
165                    positionComparison = -1;
166                }
167                
168                if (positionComparison != 0)
169                {
170                    return positionComparison;
171                }
172                return o1.getDefinition().compareTo(o2.getDefinition());
173            }
174        });
175        for (CategorizedElementDefinitionProxy parameterProxy : definitions)
176        {
177            boolean areCategoryAndGroupCoherent = true;
178            if (categoryLabel != null)
179            {
180                areCategoryAndGroupCoherent &= categoryLabel.equals(parameterProxy.getDisplayCategory());
181            }
182            if (groupLabel != null)
183            {
184                areCategoryAndGroupCoherent &= groupLabel.equals(parameterProxy.getDisplayGroup());
185            }
186            if (areCategoryAndGroupCoherent)
187            {
188                orderedParameters.add(parameterProxy);
189            }
190        }
191        return orderedParameters;
192    }
193    
194    /**
195     * Validate parameters before writing
196     * @param definitionAndValues a map of all parameters and their values
197     * @param flatDefinitions flat definition of {@link ElementDefinition} from the model file
198     * @param logger a logger
199     * @return a map containing the potential errors
200     * @deprecated TODO NEWATTRIBUTEAPI_CONFIG RUNTIME-2851 remove this method when it is not necessary anymore to have the flat definitions
201     */
202    @Deprecated
203    public static Map<String, List<I18nizableText>> validateValuesForWriting (Map<String, DefinitionAndValue> definitionAndValues, Map<String, ElementDefinition> flatDefinitions, Logger logger)
204    {
205        Map<String, List<I18nizableText>> errorFields = new HashMap<>();
206        
207        for (ElementDefinition definition : flatDefinitions.values())
208        {
209            boolean isGroupSwitchOn = ModelHelper.isGroupSwitchOn(definition, DefinitionAndValue.extractValues(definitionAndValues));
210            boolean isDisabled = ModelHelper.evaluateDisableConditions(definition.getDisableConditions(), definitionAndValues, logger);
211            
212            if (isGroupSwitchOn && !isDisabled)
213            {
214                Object value = null;
215                DefinitionAndValue definitionAndValue = definitionAndValues.get(definition.getName());
216                if (definitionAndValue != null)
217                {
218                    value = definitionAndValue.getValue();
219                }
220                
221                List<I18nizableText> errors = ModelHelper.validateValue(definition, value);
222                
223                if (!errors.isEmpty())
224                {
225                    logger.warn("The configuration parameter '{}' is not valid", definition.getName());
226                    errorFields.put(definition.getName(), errors);
227                }
228            }
229        }
230        
231        if (!errorFields.isEmpty())
232        {
233            logger.debug("Failed to save configuration because of invalid parameter values");
234        }
235        return errorFields;
236    }
237    
238    /**
239     * Validate parameters before writing
240     * @param definitionAndValues a map of all parameters and their values
241     * @param logger a logger
242     * @return a map containing the potential errors
243     */
244    public static Map<String, List<I18nizableText>> validateValuesForWriting (Map<String, DefinitionAndValue> definitionAndValues, Logger logger)
245    {
246        Map<String, List<I18nizableText>> errorFields = new HashMap<>();
247        for (Entry<String, DefinitionAndValue> entry : definitionAndValues.entrySet())
248        {
249            DefinitionAndValue definitionAndValue = entry.getValue();
250            ModelItem definition = definitionAndValue.getDefinition();
251            if (definition instanceof ElementDefinition)
252            {
253                ElementDefinition elementDefinition = (ElementDefinition) definition;
254                boolean isGroupSwitchOn = ModelHelper.isGroupSwitchOn(elementDefinition, DefinitionAndValue.extractValues(definitionAndValues));
255                boolean isDisabled = ModelHelper.evaluateDisableConditions(elementDefinition.getDisableConditions(), definitionAndValues, logger);
256                
257                if (isGroupSwitchOn && !isDisabled)
258                {
259                    Object value = definitionAndValue.getValue();
260                    
261                    List<I18nizableText> errors = ModelHelper.validateValue(elementDefinition, value);
262                    
263                    if (!errors.isEmpty())
264                    {
265                        logger.warn("The configuration parameter '{}' is not valid", elementDefinition.getName());
266                        errorFields.put(elementDefinition.getName(), errors);
267                    }
268                }
269            }
270        }
271        
272        if (!errorFields.isEmpty())
273        {
274            logger.debug("Failed to save configuration because of invalid parameter values");
275        }
276        return errorFields;
277    }
278    
279    /**
280     * Generate the wiew for a list of {@link ModelItem}, using comparators to sort the categories and the groups
281     * This works only on the very specific categories/group/fieldset hierarchy and an {@link IllegalArgumentException} will be thrown if the hierarchy is incorrect.
282     * @param categories list of {@link ModelItem}
283     * @param categoriesComparator {@link Comparator} used for the categories (can be null to keep order)
284     * @param groupsComparator {@link Comparator} for the groups (can be null to keep order)
285     * @return A {@link View} ordered from the {@link ModelItem} using the {@link Comparator}
286     * @throws IllegalArgumentException the hierarchy of the categories/groups/elements is incorrect
287     */
288    public static View buildViewFromCategories(Collection<? extends ModelItem> categories, Comparator<? super ModelItem> categoriesComparator, Comparator<? super ModelItem> groupsComparator) throws IllegalArgumentException
289    {
290        View view = new View();
291        
292        Collection<? extends ModelItem> sorted = _sort(categories, categoriesComparator);
293        
294        for (ModelItem modelItem : sorted)
295        {
296            view.addViewItem(_buildCategoryViewItem(modelItem, groupsComparator));
297        }
298        return view;
299    }
300    
301    /**
302     * sort a model item list using comparator
303     * @param items list of items to sort
304     * @param comparator a comparator, can be null to avoid sort
305     * @return a sorted list (or a copy of the list if the comparator is null)
306     */
307    private static Collection<? extends ModelItem> _sort(Collection<? extends ModelItem> items, Comparator<? super ModelItem> comparator)
308    {
309        if (comparator == null)
310        {
311            return items;
312        }
313        else
314        {
315            List<ModelItem> sorted = new ArrayList<>(items);
316            sorted.sort(comparator);
317            return sorted;
318        }
319    }
320    
321    private static ModelViewItemGroup _buildCategoryViewItem(ModelItem category, Comparator<? super ModelItem> groupsComparator) throws IllegalArgumentException
322    {
323        if (category instanceof ModelItemGroup)
324        {
325            ModelItemGroup modelGroup = (ModelItemGroup) category;
326            ModelViewItemGroup categoryViewItem = new ModelViewItemGroup();
327            categoryViewItem.setRole(ViewItemGroup.TAB_ROLE);
328            categoryViewItem.setDefinition(modelGroup);
329            
330            Collection<? extends ModelItem> groups = _sort(modelGroup.getChildren(), groupsComparator);
331            
332            for (ModelItem group : groups)
333            {
334                categoryViewItem.addViewItem(_buildGroupViewItem(group));
335            }
336            return categoryViewItem;
337        }
338        else
339        {
340            throw new IllegalArgumentException("Category " + category.getPath() + " should be an instance of ModelItemGroup");
341        }
342    }
343    
344    private static ModelViewItemGroup _buildGroupViewItem(ModelItem modelItem) throws IllegalArgumentException
345    {
346        if (modelItem instanceof ModelItemGroup)
347        {
348            ModelItemGroup modelGroup = (ModelItemGroup) modelItem;
349            ModelViewItemGroup groupViewItem = new ModelViewItemGroup();
350            groupViewItem.setRole(ViewItemGroup.FIELDSET_ROLE);
351            groupViewItem.setDefinition(modelGroup);
352            List<ModelItem> items = modelGroup.getChildren(); // They are already sorted
353            for (ModelItem item : items)
354            {
355                groupViewItem.addViewItem(_buildElementViewItem(item));
356            }
357            return groupViewItem;
358        }
359        else
360        {
361            throw new IllegalArgumentException("Group " + modelItem.getPath() + " should be an instance of ModelItemGroup");
362        }
363    }
364    
365    private static ModelViewItem _buildElementViewItem(ModelItem modelItem) throws IllegalArgumentException
366    {
367        if (modelItem instanceof ElementDefinition)
368        {
369            ElementDefinition definition = (ElementDefinition) modelItem;
370            
371            ViewElement parameterViewItem = new ViewElement();
372            parameterViewItem.setDefinition(definition);
373            return parameterViewItem;
374        }
375        else
376        {
377            throw new IllegalArgumentException("Item " + modelItem.getPath() + " should be an instance of ElementDefinition");
378        }
379    }
380}