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