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.Strings;
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 = Strings.CS.replace(dataPath, ModelItem.ITEM_PATH_SEPARATOR, ".");
138        Object valueFromRequest = request.get(fieldName);
139        
140        ElementType type = definition.getType();
141        if (org.ametys.runtime.model.type.ModelItemTypeConstants.BOOLEAN_TYPE_ID.equals(type.getId()))
142        {
143            // For boolean, if the valueFromRequest is null, it means that the checkbox was not checked, so we set the value to false
144            // For true value, the client can send "on" or "true"
145            value = Optional.of("on".equals(valueFromRequest) || "true".equals(valueFromRequest));
146        }
147        else if (valueFromRequest != null)
148        {
149            if (definition.isMultiple())
150            {
151                @SuppressWarnings("unchecked")
152                List<Object> multipleValue = valueFromRequest instanceof List
153                        ? (List<Object>) valueFromRequest
154                        : List.of(valueFromRequest);
155                
156                List<? extends Object> valuesAsList = multipleValue.stream()
157                        .map(v -> _getTypedValue(v, definition, dataPath, errors))
158                        .flatMap(Optional::stream)
159                        .collect(Collectors.toList());
160                
161                value = Optional.of(valuesAsList.toArray((Object[]) Array.newInstance(definition.getType().getManagedClass(), valuesAsList.size())));
162            }
163            else
164            {
165                Object singleValue;
166                if (valueFromRequest instanceof List)
167                {
168                    @SuppressWarnings("unchecked")
169                    List<Object> valuesFromRequest = (List<Object>) valueFromRequest;
170                    singleValue = valuesFromRequest.isEmpty() ? null : valuesFromRequest.get(0);
171                }
172                else
173                {
174                    singleValue = valueFromRequest;
175                }
176                value = _getTypedValue(singleValue, definition, dataPath, errors);
177            }
178        }
179        
180        return value;
181    }
182    
183    /**
184     * Get the typed value from object
185     * @param formValue the object value
186     * @param definition the definition of the given element
187     * @param dataPath the data path
188     * @param errors the errors
189     * @return the typed value if exist
190     */
191    protected Optional<? extends Object> _getTypedValue(Object formValue, ElementDefinition definition, String dataPath, Multimap<String, I18nizableText> errors)
192    {
193        try
194        {
195            Optional<? extends Object> value;
196            ElementType type = definition.getType();
197            if (ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID.equals(type.getId()) || ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID.equals(type.getId()))
198            {
199                value = _getUploadFileValue((Part) formValue, dataPath, errors);
200            }
201            else if (org.ametys.runtime.model.type.ModelItemTypeConstants.USER_ELEMENT_TYPE_ID.equals(type.getId()))
202            {
203                if (formValue instanceof String userIdentityAsString)
204                {
205                    value = Optional.ofNullable(UserIdentity.stringToUserIdentity(userIdentityAsString));
206                }
207                else
208                {
209                    value = Optional.ofNullable(type.fromJSONForClient(formValue, DataContext.newInstance().withDataPath(dataPath)));
210                }
211            }
212            else
213            {
214                value = Optional.ofNullable(type.fromJSONForClient(formValue, DataContext.newInstance().withDataPath(dataPath)));
215            }
216            
217            return value;
218        }
219        catch (Exception e)
220        {
221            Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
222            i18nParams.put("value", new I18nizableText(formValue.toString()));
223            i18nParams.put("datapath", new I18nizableText(dataPath.toString()));
224            errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_GET_TYPED_VALUE_ERROR"));
225            getLogger().error("Unable to get typed value " + formValue + " at path" + dataPath, e);
226            return Optional.empty();
227        }
228    }
229    
230    /**
231     * Get the repeater entries from the request
232     * @param request the request
233     * @param viewItem the view item
234     * @param dataPath the data path
235     * @param errors the errors
236     * @return list of repeater entries
237     */
238    protected List<Map<String, Object>> _getRepeaterEntries(Request request, ModelViewItemGroup viewItem, String dataPath, Multimap<String, I18nizableText> errors)
239    {
240        List<Map<String, Object>> entries = new ArrayList<>();
241
242        String fieldName = Strings.CS.replace(dataPath, ModelItem.ITEM_PATH_SEPARATOR, ".");
243        int repeaterSize = Optional.ofNullable(request.getParameter(fieldName + ".size"))
244                .map(Integer::valueOf)
245                .orElse(0);
246        
247        for (int position = 1; position <= repeaterSize; position++)
248        {
249            String updatedPrefix = dataPath + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR;
250            entries.add(getFormValues(request, viewItem, updatedPrefix, errors));
251        }
252        
253        return entries;
254    }
255    
256    /**
257     * Get the uploaded file value
258     * @param partUploaded the uploaded part
259     * @param dataPath the data path
260     * @param errors the errors
261     * @return the file binary if exist
262     */
263    protected Optional<Binary> _getUploadFileValue(Part partUploaded, String dataPath, Multimap<String, I18nizableText> errors)
264    {
265        // Checks if the part is a RejectedPart
266        if (!(partUploaded instanceof PartOnDisk))
267        {
268            if (partUploaded instanceof RejectedPart rejectedPart && rejectedPart.getMaxContentLength() == 0)
269            {
270                errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_ERROR_FILE_INFECTED"));
271                return Optional.empty();
272            }
273            else // if (partUploaded == null || partUploaded instanceof RejectedPart)
274            {
275                errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_UPLOAD_FILE_ERROR"));
276                return Optional.empty();
277            }
278        }
279        
280        // the file is not infected or corrupted, continue with the upload
281        try (InputStream is = partUploaded.getInputStream())
282        {
283            Upload upload = _uploadManager.storeUpload(_currentUserProvider.getUser(), partUploaded.getFileName(), is);
284            return Optional.of(ResourceElementTypeHelper.binaryFromUpload(upload));
285        }
286        catch (IOException e)
287        {
288            getLogger().error("Unable to store uploaded file: " + partUploaded, e);
289            errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_UPLOAD_FILE_ERROR"));
290            return Optional.empty();
291        }
292    }
293    
294    /**
295     * Validate the given values
296     * @param values the values to validate
297     * @param view the view of the ametys object
298     * @return The errors if some values are not valid
299     */
300    public Multimap<String, I18nizableText> validateValues(Map<String, Object> values, View view)
301    {
302        Multimap<String, I18nizableText> errors = ArrayListMultimap.create();
303
304        ValidationResults results = ViewHelper.validateValues(view, Optional.ofNullable(values));
305        Map<String, List<I18nizableText>> errorsAsMap = results.getAllErrors();
306        for (String dataPath : errorsAsMap.keySet())
307        {
308            errors.putAll(dataPath, errorsAsMap.get(dataPath));
309        }
310        
311        return errors;
312    }
313}