001/*
002 *  Copyright 2022 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 */
016
017package org.ametys.web;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.lang.reflect.Array;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.environment.Request;
034import org.apache.cocoon.servlet.multipart.Part;
035import org.apache.cocoon.servlet.multipart.PartOnDisk;
036import org.apache.cocoon.servlet.multipart.RejectedPart;
037import org.apache.commons.lang3.StringUtils;
038
039import org.ametys.cms.data.Binary;
040import org.ametys.cms.data.type.ModelItemTypeConstants;
041import org.ametys.cms.data.type.ResourceElementTypeHelper;
042import org.ametys.core.upload.Upload;
043import org.ametys.core.upload.UploadManager;
044import org.ametys.core.user.CurrentUserProvider;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.plugins.repository.model.ViewHelper;
047import org.ametys.runtime.i18n.I18nizableText;
048import org.ametys.runtime.i18n.I18nizableTextParameter;
049import org.ametys.runtime.model.ElementDefinition;
050import org.ametys.runtime.model.ModelItem;
051import org.ametys.runtime.model.ModelViewItemGroup;
052import org.ametys.runtime.model.View;
053import org.ametys.runtime.model.ViewItemContainer;
054import org.ametys.runtime.model.type.DataContext;
055import org.ametys.runtime.model.type.ElementType;
056import org.ametys.runtime.parameter.ValidationResults;
057import org.ametys.runtime.plugin.component.AbstractLogEnabled;
058
059import com.google.common.collect.ArrayListMultimap;
060import com.google.common.collect.Multimap;
061
062/**
063 * Helper for creating and editing an ametys object from the submitted form
064 */
065public class FOAmetysObjectCreationHelper extends AbstractLogEnabled implements Serviceable, Component
066{
067    /** The component role. */
068    public static final String ROLE = FOAmetysObjectCreationHelper.class.getName();
069    
070    /** The upload manager */
071    protected UploadManager _uploadManager;
072
073    /** The current user provider */
074    protected CurrentUserProvider _currentUserProvider;
075    
076    @Override
077    public void service(ServiceManager manager) throws ServiceException
078    {
079        _uploadManager = (UploadManager) manager.lookup(UploadManager.ROLE);
080        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
081    }
082
083    /**
084     * Get values from the request
085     * @param request the request
086     * @param viewItemContainer the view item container fo the ametys object
087     * @param prefix the prefix
088     * @param errors the errors
089     * @return the map of values
090     */
091    public Map<String, Object> getFormValues(Request request, ViewItemContainer viewItemContainer, String prefix, Multimap<String, I18nizableText> errors)
092    {
093        Map<String, Object> values = new HashMap<>();
094        
095        ViewHelper.visitView(viewItemContainer,
096            (element, definition) -> {
097                // simple element
098                String name = definition.getName();
099                String dataPath = prefix + name;
100                _getElementValue(request, definition, dataPath, errors)
101                    .ifPresent(value -> values.put(name, value));
102            },
103            (group, definition) -> {
104                // composite
105                String name = definition.getName();
106                String updatedPrefix = prefix + name + ModelItem.ITEM_PATH_SEPARATOR;
107
108                values.put(name, getFormValues(request, group, updatedPrefix, errors));
109            },
110            (group, definition) -> {
111                // repeater
112                String name = definition.getName();
113                String dataPath = prefix + name;
114                
115                List<Map<String, Object>> entries = _getRepeaterEntries(request, group, dataPath, errors);
116                values.put(name, entries);
117            },
118            group -> {
119                values.putAll(getFormValues(request, group, prefix, errors));
120            });
121
122        return values;
123    }
124    
125    /**
126     * Get the value from the request of given element
127     * @param request the request
128     * @param definition the definition of the given element
129     * @param dataPath the data path
130     * @param errors the errors
131     * @return the element values if exist
132     */
133    protected Optional<? extends Object> _getElementValue(Request request, ElementDefinition definition, String dataPath, Multimap<String, I18nizableText> errors)
134    {
135        Optional<? extends Object> value = Optional.empty();
136
137        String fieldName = StringUtils.replace(dataPath, ModelItem.ITEM_PATH_SEPARATOR, ".");
138        Object valueFromRequest = request.get(fieldName);
139        
140        if (valueFromRequest != null)
141        {
142            if (definition.isMultiple())
143            {
144                @SuppressWarnings("unchecked")
145                List<Object> multipleValue = valueFromRequest instanceof List
146                        ? (List<Object>) valueFromRequest
147                        : List.of(valueFromRequest);
148                
149                List<? extends Object> valuesAsList = multipleValue.stream()
150                        .map(v -> _getTypedValue(v, definition, dataPath, errors))
151                        .flatMap(Optional::stream)
152                        .collect(Collectors.toList());
153                
154                value = Optional.of(valuesAsList.toArray((Object[]) Array.newInstance(definition.getType().getManagedClass(), valuesAsList.size())));
155            }
156            else
157            {
158                Object singleValue;
159                if (valueFromRequest instanceof List)
160                {
161                    @SuppressWarnings("unchecked")
162                    List<Object> valuesFromRequest = (List<Object>) valueFromRequest;
163                    singleValue = valuesFromRequest.isEmpty() ? null : valuesFromRequest.get(0);
164                }
165                else
166                {
167                    singleValue = valueFromRequest;
168                }
169                value = _getTypedValue(singleValue, definition, dataPath, errors);
170            }
171        }
172        
173        return value;
174    }
175    
176    /**
177     * Get the typed value from object
178     * @param formValue the object value
179     * @param definition the definition of the given element
180     * @param dataPath the data path
181     * @param errors the errors
182     * @return the typed value if exist
183     */
184    protected Optional<? extends Object> _getTypedValue(Object formValue, ElementDefinition definition, String dataPath, Multimap<String, I18nizableText> errors)
185    {
186        try
187        {
188            Optional<? extends Object> value;
189            ElementType type = definition.getType();
190            if (ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID.equals(type.getId()) || ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID.equals(type.getId()))
191            {
192                value = _getUploadFileValue((Part) formValue, dataPath, errors);
193            }
194            else if (org.ametys.runtime.model.type.ModelItemTypeConstants.BOOLEAN_TYPE_ID.equals(type.getId()))
195            {
196                value = Optional.of("on".equals(formValue) || "true".equals(formValue));
197            }
198            else if (ModelItemTypeConstants.USER_ELEMENT_TYPE_ID.equals(type.getId()))
199            {
200                if (formValue instanceof String userIdentityAsString)
201                {
202                    value = Optional.ofNullable(UserIdentity.stringToUserIdentity(userIdentityAsString));
203                }
204                else
205                {
206                    value = Optional.ofNullable(type.fromJSONForClient(formValue, DataContext.newInstance().withDataPath(dataPath)));
207                }
208            }
209            else
210            {
211                value = Optional.ofNullable(type.fromJSONForClient(formValue, DataContext.newInstance().withDataPath(dataPath)));
212            }
213            
214            return value;
215        }
216        catch (Exception e)
217        {
218            Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
219            i18nParams.put("value", new I18nizableText(formValue.toString()));
220            i18nParams.put("datapath", new I18nizableText(dataPath.toString()));
221            errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_GET_TYPED_VALUE_ERROR"));
222            getLogger().error("Unable to get typed value " + formValue + " at path" + dataPath, e);
223            return Optional.empty();
224        }
225    }
226    
227    /**
228     * Get the repeater entries from the request
229     * @param request the request
230     * @param viewItem the view item
231     * @param dataPath the data path
232     * @param errors the errors
233     * @return list of repeater entries
234     */
235    protected List<Map<String, Object>> _getRepeaterEntries(Request request, ModelViewItemGroup viewItem, String dataPath, Multimap<String, I18nizableText> errors)
236    {
237        List<Map<String, Object>> entries = new ArrayList<>();
238
239        String fieldName = StringUtils.replace(dataPath, ModelItem.ITEM_PATH_SEPARATOR, ".");
240        int repeaterSize = Optional.ofNullable(request.getParameter(fieldName + ".size"))
241                .map(Integer::valueOf)
242                .orElse(0);
243        
244        for (int position = 1; position <= repeaterSize; position++)
245        {
246            String updatedPrefix = dataPath + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR;
247            entries.add(getFormValues(request, viewItem, updatedPrefix, errors));
248        }
249        
250        return entries;
251    }
252    
253    /**
254     * Get the uploaded file value
255     * @param partUploaded the uploaded part
256     * @param dataPath the data path
257     * @param errors the errors
258     * @return the file binary if exist
259     */
260    protected Optional<Binary> _getUploadFileValue(Part partUploaded, String dataPath, Multimap<String, I18nizableText> errors)
261    {
262        // Checks if the part is a RejectedPart
263        if (!(partUploaded instanceof PartOnDisk))
264        {
265            if (partUploaded instanceof RejectedPart rejectedPart && rejectedPart.getMaxContentLength() == 0)
266            {
267                errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_ERROR_FILE_INFECTED"));
268                return Optional.empty();
269            }
270            else // if (partUploaded == null || partUploaded instanceof RejectedPart)
271            {
272                errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_UPLOAD_FILE_ERROR"));
273                return Optional.empty();
274            }
275        }
276        
277        // the file is not infected or corrupted, continue with the upload
278        try (InputStream is = partUploaded.getInputStream())
279        {
280            Upload upload = _uploadManager.storeUpload(_currentUserProvider.getUser(), partUploaded.getFileName(), is);
281            return Optional.of(ResourceElementTypeHelper.binaryFromUpload(upload));
282        }
283        catch (IOException e)
284        {
285            getLogger().error("Unable to store uploaded file: " + partUploaded, e);
286            errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_UPLOAD_FILE_ERROR"));
287            return Optional.empty();
288        }
289    }
290    
291    /**
292     * Validate the given values
293     * @param values the values to validate
294     * @param view the view of the ametys object
295     * @return The errors if some values are not valid
296     */
297    public Multimap<String, I18nizableText> validateValues(Map<String, Object> values, View view)
298    {
299        Multimap<String, I18nizableText> errors = ArrayListMultimap.create();
300
301        ValidationResults results = ViewHelper.validateValues(view, Optional.ofNullable(values));
302        Map<String, List<I18nizableText>> errorsAsMap = results.getAllErrors();
303        for (String dataPath : errorsAsMap.keySet())
304        {
305            errors.putAll(dataPath, errorsAsMap.get(dataPath));
306        }
307        
308        return errors;
309    }
310}