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