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                String dataPath = prefix + definition.getName();
095                Object submittedValue = values.get(dataPath);
096                ElementType parameterType = definition.getType();
097                Object value = parameterType.fromJSONForClient(submittedValue, DataContext.newInstance().withDataPath(dataPath));
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                String dataPath = prefix + item.getName();
240                // Put empty values in the values map in order make the difference between empty and not present values
241                if (dataHolder.hasValueOrEmpty(item.getName()))
242                {
243                    ElementType type = ((ElementDefinition) item).getType();
244                    Object value = dataHolder.getValue(item.getName());
245                    
246                    values.put(dataPath, type.valueToJSONForClient(value, DataContext.newInstance()));
247                }
248            }
249            else if (item instanceof RepeaterDefinition)
250            {
251                if (dataHolder.hasValue(item.getName()))
252                {
253                    ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName());
254                    for (ModelAwareRepeaterEntry entry: repeater.getEntries())
255                    {
256                        String newPrefix = prefix + item.getName() + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR;
257                        values.putAll(getParametersValues(((RepeaterDefinition) item).getChildren(), entry, newPrefix));
258                    }
259                    values.put(prefix + item.getName(), new ArrayList<>());
260                }
261            }
262        }
263        catch (Exception e) 
264        {
265            getLogger().error("Can't get values from parameter with name '{}'", item.getName(), e);
266        }
267    }
268    
269    /**
270     * Get the repeaters values
271     * @param items the list of model item
272     * @param dataHolder the data holder
273     * @param prefix prefix to get the parameter values
274     * @return the repeaters values
275     */
276    public List<Map<String, Object>> getRepeatersValues(Collection<ModelItem> items, ModelAwareDataHolder dataHolder, String prefix)
277    {
278        List<Map<String, Object>> results = new ArrayList<>();
279        
280        for (ModelItem item : items)
281        {
282            if (item instanceof RepeaterDefinition)
283            {
284                Map<String, Object> result = new HashMap<>();
285                
286                result.put("name", item.getName());
287                result.put("prefix", prefix);
288                
289                if (dataHolder.hasValue(item.getName()))
290                {
291                    ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName());
292                    result.put("count", repeater.getSize());
293                    for (ModelAwareRepeaterEntry entry: repeater.getEntries())
294                    {
295                        StringBuilder newPrefix = new StringBuilder();
296                        newPrefix.append(prefix);
297                        newPrefix.append(item.getName()).append("[").append(entry.getPosition()).append("]/");
298                        results.addAll(getRepeatersValues(((RepeaterDefinition) item).getChildren(), entry, newPrefix.toString()));
299                    }
300                }
301                else
302                {
303                    result.put("count", 0);
304                }
305                
306                results.add(result);
307            }
308        }
309        
310        return results;
311    }
312    
313    /**
314     * True if the parameter has a value or a default value
315     * @param parameterPath the parameter path
316     * @param dataHolder the data holder
317     * @param defaultValue the default value
318     * @return true if the parameter has a value or a default value
319     */
320    protected boolean _hasParameterValueOrDefaultValue(String parameterPath, ModelAwareDataHolder dataHolder, Object defaultValue)
321    {
322        if (dataHolder.hasValue(parameterPath))
323        {
324            return true;
325        }
326        else
327        {
328            if (defaultValue != null)
329            {
330                return defaultValue instanceof String ? StringUtils.isNotEmpty((String) defaultValue) : true;
331            }
332            else
333            {
334                return false;
335            }
336        }
337    }
338    
339    /**
340     * Get all parameters which start with prefix and change the name of the attribute removing this prefix
341     * @param parameterValues the parameter values
342     * @param prefix the prefix
343     * @return the map of filtered parameters
344     */
345    public Map<String, Object> getParametersStartWithPrefix(Map<String, Object> parameterValues, String prefix)
346    {
347        Map<String, Object> newParameterValues = new HashMap<>();
348        for (String name : parameterValues.keySet())
349        {
350            if (name.startsWith(prefix))
351            {
352                newParameterValues.put(StringUtils.substringAfter(name, prefix), parameterValues.get(name));
353            }
354        }
355        
356        return newParameterValues;
357    }
358    
359    /**
360     * Add prefix to all parameters
361     * @param parameterValues the parameters values
362     * @param prefix the prefix
363     * @return the map of parameters with its prefix
364     */
365    public Map<String, Object> addPrefixToParameters(Map<String, Object> parameterValues, String prefix)
366    {
367        Map<String, Object> newParameterValues = new HashMap<>();
368        for (String name : parameterValues.keySet())
369        {
370            newParameterValues.put(prefix + name, parameterValues.get(name));
371        }
372        
373        return newParameterValues;
374    }
375}