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