001/*
002 *  Copyright 2020 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.web.parameters;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.avalon.framework.component.Component;
025import org.apache.commons.lang.StringUtils;
026
027import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
028import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
029import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeater;
030import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry;
031import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater;
032import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeaterEntry;
033import org.ametys.plugins.repository.model.RepeaterDefinition;
034import org.ametys.runtime.i18n.I18nizableText;
035import org.ametys.runtime.model.DefinitionAndValue;
036import org.ametys.runtime.model.ElementDefinition;
037import org.ametys.runtime.model.ModelHelper;
038import org.ametys.runtime.model.ModelItem;
039import org.ametys.runtime.model.ModelItemContainer;
040import org.ametys.runtime.model.type.DataContext;
041import org.ametys.runtime.model.type.ElementType;
042import org.ametys.runtime.plugin.component.AbstractLogEnabled;
043
044/**
045 * Manager to handle parameters
046 */
047public class ParametersManager extends AbstractLogEnabled implements Component               
048{
049    /** Avalon Role */
050    public static final String ROLE = ParametersManager.class.getName();
051    
052    /** Constant for untouched binary metadata. */
053    protected static final String _PARAM_UNTOUCHED_BINARY = "untouched";
054    
055    /**
056     * Set parameters values to the data holder for each model items
057     * @param dataHolder the data holder
058     * @param modelItems the list of model items
059     * @param values the values
060     * @return the map of possible errors
061     */
062    public Map<String, List<I18nizableText>> setParameterValues(ModifiableModelAwareDataHolder dataHolder, Collection<? extends ModelItem> modelItems, Map<String, Object> values)
063    {
064        Map<String, List<I18nizableText>> allErrors = new HashMap<>();
065        
066        // FIXME CMS-10275
067        for (ModelItem definition : modelItems)
068        {
069            _setParameterValues(definition, values, dataHolder, "", allErrors);
070        }
071        
072        return allErrors;
073    }
074    
075    /**
076     * Set parameters values of the model item
077     * @param def the model item definition
078     * @param values the value
079     * @param dataHolder the data holder
080     * @param prefix the prefix to get the parameter values
081     * @param allErrors the map of possible errors
082     */
083    protected void _setParameterValues(ModelItem def, Map<String, Object> values, ModifiableModelAwareDataHolder dataHolder, String prefix, Map<String, List<I18nizableText>> allErrors)
084    {
085        if (def instanceof ElementDefinition)
086        {
087            ElementDefinition definition = (ElementDefinition) def;
088            Map<String, DefinitionAndValue> definitionAndValues = _getBrothersDefinitionAndValues(prefix, values, definition);
089            boolean isGroupSwitchOn = ModelHelper.isGroupSwitchOn(definition, values);
090            boolean isDisabled = ModelHelper.evaluateDisableConditions(definition.getDisableConditions(), definitionAndValues, getLogger());
091            
092            if (isGroupSwitchOn && !isDisabled)
093            {
094            
095                Object submittedValue = values.get(prefix + definition.getName());
096                ElementType parameterType = definition.getType();
097                Object value = parameterType.fromJSONForClient(submittedValue);
098                
099                // TODO RUNTIME-2897: call the validateValue without boolean when multiple values are managed in enumerators
100                List<I18nizableText> errors = ModelHelper.validateValue(definition, value, false);
101                if (!errors.isEmpty())
102                {
103                    allErrors.put(definition.getName(), errors);
104                    return;
105                }
106        
107                String typeId = def.getType().getId();
108                if (_PARAM_UNTOUCHED_BINARY.equals(value)
109                        /* keeps the value of an empty password field */
110                        || value == null && org.ametys.runtime.model.type.ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(typeId))
111                {
112                    // do not set value
113                    return;
114                }
115                dataHolder.setValue(definition.getName(), value);
116            }
117        }
118        else if (def instanceof RepeaterDefinition)
119        {
120            RepeaterDefinition definition = (RepeaterDefinition) def;
121            ModifiableModelAwareRepeater repeater = dataHolder.getRepeater(definition.getName(), true);
122            
123            // First move the entries according to the given previous positions
124            _moveRepeaterEntries(repeater, values, prefix + definition.getName());
125            
126            // Then save the entries' parameter values
127            for (ModifiableModelAwareRepeaterEntry entry : repeater.getEntries())
128            {
129                List<ModelItem> childrenDefinitions = definition.getChildren();
130                String newPrefix = prefix + definition.getName() + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR;
131                for (ModelItem childDefinition : childrenDefinitions)
132                {
133                    _setParameterValues(childDefinition, values, entry, newPrefix, allErrors);
134                }
135            }
136        }
137    }
138    
139    /**
140     * Get the definition and value pairs of the given definition brothers. The definition and value pairs are indexed by the parameter name
141     * @param prefix prefix to get the parameter values
142     * @param values all the parameter values
143     * @param definition the definition
144     * @return the definition and value pairs of the given definition brother
145     */
146    protected Map<String, DefinitionAndValue> _getBrothersDefinitionAndValues(String prefix, Map<String, Object> values, ElementDefinition definition)
147    {
148        Map<String, DefinitionAndValue> definitionAndValues = new HashMap<>();
149        
150        ModelItemContainer definitionContainer = definition.getParent() != null ? definition.getParent() : definition.getModel();
151        Collection< ? extends ModelItem> definitionBrothers;
152        definitionBrothers = definitionContainer.getModelItems();
153        for (ModelItem brother : definitionBrothers)
154        {
155            Object value = values.get(prefix + brother.getName());
156            if (brother instanceof ElementDefinition)
157            {
158                DefinitionAndValue definitionAndValue = new DefinitionAndValue<>(null, brother, value);
159                definitionAndValues.put(brother.getName(), definitionAndValue);
160            }
161        }
162        
163        return definitionAndValues;
164    }
165    
166    /**
167     * Moves the repeater entries according to the given values.
168     * Removes or add entries if needed.
169     * After the call of this method, the given repeater will contain the right entries
170     * @param repeater the repeater
171     * @param values the values
172     * @param repeaterPath the repeater path
173     */
174    protected void _moveRepeaterEntries(ModifiableModelAwareRepeater repeater, Map<String, Object> values, String repeaterPath)
175    {
176        Map<Integer, Integer> positionsMapping = new HashMap<>();
177
178        String sizeEntryName = "_" + repeaterPath + ModelItem.ITEM_PATH_SEPARATOR + "size";
179        if (values.containsKey(sizeEntryName))
180        {
181            int size = (int) values.get(sizeEntryName);
182            for (int position = 1; position <= size; position++)
183            {
184                String previousPositionEntryName = "_" + repeaterPath + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR + "previous-position";
185                if (values.containsKey(previousPositionEntryName))
186                {
187                    int previousPosition = (int) values.get(previousPositionEntryName);
188                    if (previousPosition > 0)
189                    {
190                        positionsMapping.put(previousPosition, position);
191                    }
192                }
193                else
194                {
195                    throw new IllegalArgumentException("The given values don't contain the previous position of the repeater entry '" + repeaterPath + "[" + position + "]" + "'.");
196                }
197            }
198            
199            repeater.moveEntries(positionsMapping, size);
200        }
201        else
202        {
203            throw new IllegalArgumentException("The given values don't contain the size of the repeater at path '" + repeaterPath + "'.");
204        }
205    }
206    
207    /**
208     * Get the parameters values
209     * @param items the list of model item
210     * @param dataHolder the data holder
211     * @param prefix prefix to get the parameter values
212     * @return the parameters values
213     */
214    public Map<String, Object> getParametersValues(Collection<? extends ModelItem> items, ModelAwareDataHolder dataHolder, String prefix)
215    {
216        Map<String, Object> values = new HashMap<>();
217        
218        for (ModelItem item : items)
219        {
220            _addParameterValues(item, dataHolder, prefix, values);
221        }
222        
223        return values;
224    }
225
226    /**
227     * Add the parameter values to all values
228     * @param item the model item
229     * @param dataHolder the data holder
230     * @param prefix prefix to get the parameter values
231     * @param values all the values
232     */
233    protected void _addParameterValues(ModelItem item, ModelAwareDataHolder dataHolder, String prefix, Map<String, Object> values)
234    {
235        try
236        {
237            if (item instanceof ElementDefinition)
238            {
239                if (dataHolder.hasValue(item.getName()))
240                {
241                    ElementType type = ((ElementDefinition) item).getType();
242                    Object value = dataHolder.getValue(item.getName());
243                    
244                    String dataPath = prefix + item.getName();
245                    values.put(dataPath, type.valueToJSONForClient(value, DataContext.newInstance()));
246                }
247            }
248            else if (item instanceof RepeaterDefinition)
249            {
250                if (dataHolder.hasValue(item.getName()))
251                {
252                    ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName());
253                    for (ModelAwareRepeaterEntry entry: repeater.getEntries())
254                    {
255                        String newPrefix = prefix + item.getName() + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR;
256                        values.putAll(getParametersValues(((RepeaterDefinition) item).getChildren(), entry, newPrefix));
257                    }
258                    values.put(prefix + item.getName(), new ArrayList<>());
259                }
260            }
261        }
262        catch (Exception e) 
263        {
264            getLogger().error("Can't get values from parameter with name '{}'", item.getName(), e);
265        }
266    }
267    
268    /**
269     * Get the repeaters values
270     * @param items the list of model item
271     * @param dataHolder the data holder
272     * @param prefix prefix to get the parameter values
273     * @return the repeaters values
274     */
275    public List<Map<String, Object>> getRepeatersValues(Collection<ModelItem> items, ModelAwareDataHolder dataHolder, String prefix)
276    {
277        List<Map<String, Object>> results = new ArrayList<>();
278        
279        for (ModelItem item : items)
280        {
281            if (item instanceof RepeaterDefinition)
282            {
283                Map<String, Object> result = new HashMap<>();
284                
285                result.put("name", item.getName());
286                result.put("prefix", prefix);
287                
288                if (dataHolder.hasValue(item.getName()))
289                {
290                    ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName());
291                    result.put("count", repeater.getSize());
292                    for (ModelAwareRepeaterEntry entry: repeater.getEntries())
293                    {
294                        StringBuilder newPrefix = new StringBuilder();
295                        newPrefix.append(prefix);
296                        newPrefix.append(item.getName()).append("[").append(entry.getPosition()).append("]/");
297                        results.addAll(getRepeatersValues(((RepeaterDefinition) item).getChildren(), entry, newPrefix.toString()));
298                    }
299                }
300                else
301                {
302                    result.put("count", 0);
303                }
304                
305                results.add(result);
306            }
307        }
308        
309        return results;
310    }
311    
312    /**
313     * True if the parameter has a value or a default value
314     * @param parameterPath the parameter path
315     * @param dataHolder the data holder
316     * @param defaultValue the default value
317     * @return true if the parameter has a value or a default value
318     */
319    protected boolean _hasParameterValueOrDefaultValue(String parameterPath, ModelAwareDataHolder dataHolder, Object defaultValue)
320    {
321        if (dataHolder.hasNonEmptyValue(parameterPath))
322        {
323            return true;
324        }
325        else
326        {
327            if (defaultValue != null)
328            {
329                return defaultValue instanceof String ? StringUtils.isNotEmpty((String) defaultValue) : true;
330            }
331            else
332            {
333                return false;
334            }
335        }
336    }
337    
338    /**
339     * Get all parameters which start with prefix and change the name of the attribute removing this prefix
340     * @param parameterValues the parameter values
341     * @param prefix the prefix
342     * @return the map of filtered parameters
343     */
344    public Map<String, Object> getParametersStartWithPrefix(Map<String, Object> parameterValues, String prefix)
345    {
346        Map<String, Object> newParameterValues = new HashMap<>();
347        for (String name : parameterValues.keySet())
348        {
349            if (name.startsWith(prefix))
350            {
351                newParameterValues.put(StringUtils.substringAfter(name, prefix), parameterValues.get(name));
352            }
353        }
354        
355        return newParameterValues;
356    }
357    
358    /**
359     * Add prefix to all parameters
360     * @param parameterValues the parameters values
361     * @param prefix the prefix
362     * @return the map of parameters with its prefix
363     */
364    public Map<String, Object> addPrefixToParameters(Map<String, Object> parameterValues, String prefix)
365    {
366        Map<String, Object> newParameterValues = new HashMap<>();
367        for (String name : parameterValues.keySet())
368        {
369            newParameterValues.put(prefix + name, parameterValues.get(name));
370        }
371        
372        return newParameterValues;
373    }
374}