001/*
002 *  Copyright 2018 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.content;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.function.Predicate;
028import java.util.stream.Collectors;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.context.Context;
032import org.apache.avalon.framework.context.ContextException;
033import org.apache.avalon.framework.context.Contextualizable;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037import org.apache.cocoon.components.ContextHelper;
038import org.apache.cocoon.components.source.impl.SitemapSource;
039import org.apache.cocoon.environment.Request;
040import org.apache.cocoon.servlet.multipart.Part;
041import org.apache.cocoon.xml.AttributesImpl;
042import org.apache.cocoon.xml.XMLUtils;
043import org.apache.commons.collections.MapUtils;
044import org.apache.commons.lang.StringUtils;
045import org.apache.commons.lang3.ArrayUtils;
046import org.apache.excalibur.source.SourceResolver;
047import org.xml.sax.ContentHandler;
048import org.xml.sax.SAXException;
049
050import org.ametys.cms.contenttype.ContentType;
051import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
052import org.ametys.cms.contenttype.ContentTypesHelper;
053import org.ametys.cms.contenttype.MetadataDefinition;
054import org.ametys.cms.contenttype.MetadataType;
055import org.ametys.cms.repository.Content;
056import org.ametys.cms.search.query.AndQuery;
057import org.ametys.cms.search.query.ContentLanguageQuery;
058import org.ametys.cms.search.query.ContentTypeQuery;
059import org.ametys.cms.search.query.DocumentTypeQuery;
060import org.ametys.cms.search.query.Query;
061import org.ametys.cms.search.solr.SearcherFactory.Searcher;
062import org.ametys.cms.workflow.ContentWorkflowHelper;
063import org.ametys.cms.workflow.EditContentFunction;
064import org.ametys.core.upload.Upload;
065import org.ametys.core.upload.UploadManager;
066import org.ametys.core.user.CurrentUserProvider;
067import org.ametys.core.util.IgnoreRootHandler;
068import org.ametys.core.util.JSONUtils;
069import org.ametys.plugins.repository.AmetysObjectIterable;
070import org.ametys.plugins.repository.AmetysRepositoryException;
071import org.ametys.plugins.repository.RepositoryConstants;
072import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
073import org.ametys.plugins.workflow.AbstractWorkflowComponent;
074import org.ametys.runtime.i18n.I18nizableText;
075import org.ametys.runtime.parameter.Errors;
076import org.ametys.runtime.parameter.Validator;
077import org.ametys.runtime.plugin.component.AbstractLogEnabled;
078import org.ametys.web.frontoffice.FrontOfficeSearcherFactory;
079
080import com.google.common.collect.Multimap;
081import com.opensymphony.workflow.WorkflowException;
082
083/**
084 * Helper for creating and editing a content from the submitted form
085 *
086 */
087public class FOContentCreationHelper extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
088{
089    /** The component role. */
090    public static final String ROLE = FOContentCreationHelper.class.getName();
091    
092    private ContentTypesHelper _contentTypeHelper;
093
094    private JSONUtils _jsonUtils;
095
096    private UploadManager _uploadManager;
097
098    private CurrentUserProvider _currentUserProvider;
099
100    private ContentWorkflowHelper _contentWorkflowHelper;
101
102    private Context _context;
103
104    private SourceResolver _srcResolver;
105
106    private ContentTypeExtensionPoint _cTypeExtPt;
107
108    private FrontOfficeSearcherFactory _searcherFactory;
109    
110    public void service(ServiceManager smanager) throws ServiceException
111    {
112        _contentTypeHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
113        _cTypeExtPt = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
114        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
115        _uploadManager = (UploadManager) smanager.lookup(UploadManager.ROLE);
116        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
117        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
118        _srcResolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE);
119        _searcherFactory = (FrontOfficeSearcherFactory) smanager.lookup(FrontOfficeSearcherFactory.ROLE);
120    }
121    
122    public void contextualize(Context context) throws ContextException
123    {
124        _context = context;
125    }
126    
127    /**
128     * Get the request
129     * @return the request
130     */
131    protected Request _getRequest()
132    {
133        return ContextHelper.getRequest(_context);
134    }
135    
136    /**
137     * SAX the view of a content type
138     * @param contentHandler The content handler to sax into
139     * @param contentType The content type
140     * @param rootTagName the root tag name
141     * @param viewName The view name
142     * @throws SAXException if an error occurs
143     */
144    public void saxViewIfExists(ContentHandler contentHandler, ContentType contentType, String rootTagName, String viewName) throws SAXException
145    {
146        if (contentType != null && contentType.getView(viewName) != null)
147        {
148            AttributesImpl attrs = new AttributesImpl();
149            attrs.addCDATAAttribute("id", contentType.getId());
150            XMLUtils.startElement(contentHandler, rootTagName);
151            
152            // FIXME Use new model API
153            String uri = "cocoon://_content-type/metadataset.xml?contentTypeId=" + contentType.getId() + "&metadataSetName=" + viewName;
154            SitemapSource src = null;
155            
156            try
157            {
158                src = (SitemapSource) _srcResolver.resolveURI(uri);
159                src.toSAX(new IgnoreRootHandler(contentHandler));
160            }
161            catch (IOException e)
162            {
163                getLogger().error("Unable to sax view '" + viewName + "' for content type '" + contentType.getId() + "'", e);
164            }
165            finally
166            {
167                _srcResolver.release(src);
168            }
169            
170            
171            XMLUtils.endElement(contentHandler, rootTagName);
172        }
173    }
174    
175    /**
176     * SAX contents values for metadata of type CONTENT
177     * @param contentHandler The content handler to sax into
178     * @param contentType The content type
179     * @param rootTagName The root tag name
180     * @param language the current language
181     * @throws SAXException if an error occurs when saxing
182     */
183    public void saxContentValues(ContentHandler contentHandler, ContentType contentType, String rootTagName, String language) throws SAXException
184    {
185        Predicate<MetadataDefinition> p = md -> md.getType() == MetadataType.CONTENT;
186        Map<String, MetadataDefinition> contentDefs = _contentTypeHelper.getMetadataDefinitions(contentType, p);
187        
188        for (MetadataDefinition metaDef : contentDefs.values())
189        {
190            AttributesImpl attrs = new AttributesImpl();
191            attrs.addCDATAAttribute("name", metaDef.getId().replace("/", "."));
192            XMLUtils.startElement(contentHandler, rootTagName, attrs);
193            _saxContentEnumeratorValue(contentHandler, metaDef, language);
194            XMLUtils.endElement(contentHandler, rootTagName);
195        }
196    }
197
198
199    /**
200     * Sax enumeration value for enum or a content metadata
201     * @param contentHandler The content handler to sax into
202     * @param metadataDef The metadata definition.
203     * @param language The current language
204     * @throws SAXException If an error occurred while saxing
205     */
206    private void _saxContentEnumeratorValue(ContentHandler contentHandler, MetadataDefinition metadataDef, String language) throws SAXException
207    {
208        Map<String, String> values = getContentValues(metadataDef.getContentType(), language);
209        
210        XMLUtils.startElement(contentHandler, "enumeration");
211        for (Entry<String, String> entry : values.entrySet())
212        {
213            AttributesImpl attrItem = new AttributesImpl();
214            attrItem.addCDATAAttribute("value", entry.getKey());
215            XMLUtils.startElement(contentHandler, "item", attrItem);
216            XMLUtils.createElement(contentHandler, "label", entry.getValue());
217            XMLUtils.endElement(contentHandler, "item");
218        }
219        XMLUtils.endElement(contentHandler, "enumeration");
220    }
221    
222    /**
223     * Get values for contents enumeration
224     * @param cTypeId The id of content type
225     * @param language The current language
226     * @return The contents
227     */
228    protected Map<String, String> getContentValues(String cTypeId, String language)
229    {
230        try
231        {
232            Query query = null;
233            if (StringUtils.isNotEmpty(cTypeId))
234            {
235                query = new ContentTypeQuery(cTypeId);
236            }
237            
238            boolean multilingual = _cTypeExtPt.getExtension(cTypeId).isMultilingual();
239            if (!multilingual)
240            {
241                query = query != null ? new AndQuery(query, new ContentLanguageQuery(language)) : new ContentLanguageQuery(language);
242            }
243            
244            Searcher searcher = _searcherFactory.create().withQuery(query)
245                    .addFilterQuery(new DocumentTypeQuery("content"))
246                    .withLimits(0, Integer.MAX_VALUE);
247
248            AmetysObjectIterable<Content> contents = searcher.search();
249            
250            return contents.stream()
251                    .collect(Collectors.toMap(Content::getId, c -> c.getTitle(new Locale(language))))
252                    .entrySet()
253                    .stream()
254                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
255        }
256        catch (Exception e)
257        {
258            getLogger().error("Failed to get content enumeration for content type " +  cTypeId, e);
259            return MapUtils.EMPTY_MAP;
260        }
261    }
262    
263    /**
264     * Get the values for this content type from request
265     * @param rawValues the input values
266     * @param contentType the edited content type 
267     * @param viewName the view name
268     * @param errors The errors to fill
269     * @return The values
270     */
271    public Map<String, Object> getAndValidateFormValues(Map<String, Object> rawValues, ContentType contentType, String viewName, Multimap<String, I18nizableText> errors)
272    {
273        Map<String, Object> formValues = new HashMap<>();
274        
275        Map<String, MetadataDefinition> metadataDefs = _contentTypeHelper.getMetadataDefinitions(contentType.getMetadataSetForView(viewName), contentType);
276        
277        for (Entry<String, MetadataDefinition> entry : metadataDefs.entrySet())
278        {
279            String metadataName = StringUtils.replace(entry.getKey(), "/", ".");
280            MetadataDefinition metadataDef = entry.getValue();
281            
282            if (metadataDef.getType() != MetadataType.COMPOSITE)
283            {
284                Object rawValue = rawValues.get(metadataName);
285                Object value = rawValue;
286                
287                if (rawValue instanceof List || rawValue instanceof Map)
288                {
289                    value = _jsonUtils.convertObjectToJson(rawValue);
290                }
291                
292                if (_validateFormField(metadataDef, metadataName, rawValue, errors))
293                {
294                    formValues.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, value);
295                }
296            }
297        }
298        
299        return formValues;
300    }
301    
302    /**
303     * Get the values for this content type from request
304     * @param request the request
305     * @param contentType the edited content type 
306     * @param viewName the view name
307     * @param errors The errors to fill
308     * @return The values
309     */
310    public Map<String, Object> getAndValidateFormValues(Request request, ContentType contentType, String viewName, Multimap<String, I18nizableText> errors)
311    {
312        Map<String, Object> rawValues = new HashMap<>();
313        
314        Map<String, MetadataDefinition> metadataDefs = _contentTypeHelper.getMetadataDefinitions(contentType.getMetadataSetForView(viewName), contentType);
315        
316        for (Entry<String, MetadataDefinition> entry : metadataDefs.entrySet())
317        {
318            String parameterName = StringUtils.replace(entry.getKey(), "/", ".");
319            MetadataDefinition metadataDef = entry.getValue();
320            MetadataType type = metadataDef.getType();
321            
322            if (type == MetadataType.FILE || type == MetadataType.BINARY)
323            {
324                Part partUploaded = (Part) request.get(parameterName);
325                if (partUploaded != null)
326                {
327                    Map<String, Object> uploadFile = _getUploadFileValue(partUploaded);
328                    if (uploadFile != null)
329                    {
330                        rawValues.put(parameterName, _jsonUtils.convertObjectToJson(uploadFile));
331                    }
332                    else
333                    {
334                        errors.put(parameterName, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_UPLOAD_FILE_ERROR"));
335                    }
336                }
337            }
338            else if (type == MetadataType.BOOLEAN)
339            {
340                String value = request.getParameter(parameterName);
341                rawValues.put(parameterName, "on".equals(value) || "true".equals(value) ? true : false);
342            }
343            else if (type == MetadataType.STRING && metadataDef.isMultiple() && metadataDef.getEnumerator() == null)
344            {
345                String values = request.getParameter(parameterName);
346                
347                List<String> valuesAsList = new ArrayList<>();
348                if (StringUtils.isNotBlank(values))
349                {
350                    for (String value : values.split(","))
351                    {
352                        valuesAsList.add(StringUtils.trim(value));
353                    }
354                }
355                
356                rawValues.put(parameterName, valuesAsList);
357                
358            }
359            else if (metadataDef.getType() != MetadataType.COMPOSITE)
360            {
361                rawValues.put(parameterName, request.getParameter(parameterName));
362            }
363        }
364        
365        return getAndValidateFormValues(rawValues, contentType, viewName, errors);
366    }
367    
368    /**
369     * Create and edit a content
370     * @param initActionId The initial workflow action id for creation and edition
371     * @param contentTypeId The id of content type
372     * @param siteName The current site name
373     * @param contentName The content name
374     * @param contentTitle The content title
375     * @param language The content language
376     * @param values The submitted values
377     * @param workflowName The workflow name
378     * @param viewName The view name
379     * @return The workflow result
380     * @throws AmetysRepositoryException if an error occurs
381     * @throws WorkflowException if an error occurs
382     */
383    public Map<String, Object> createAndEditContent(int initActionId, String contentTypeId, String siteName, String contentName, String contentTitle, String language, Map<String, Object> values, String workflowName, String viewName) throws AmetysRepositoryException, WorkflowException
384    {
385        return createAndEditContent(initActionId, contentTypeId, siteName, contentName, contentTitle, language, values, workflowName, viewName, new HashMap<String, Object>());
386    }
387    
388    /**
389     * Create and edit a content
390     * @param initActionId The initial workflow action id for creation and edition
391     * @param contentTypeId The id of content type
392     * @param siteName The current site name
393     * @param contentName The content name
394     * @param contentTitle The content title
395     * @param language The content language
396     * @param values The submitted values
397     * @param workflowName The workflow name
398     * @param viewName The view name
399     * @param inputs The initial workflow inputs
400     * @return The workflow result
401     * @throws AmetysRepositoryException if an error occurs
402     * @throws WorkflowException if an error occurs
403     */
404    public Map<String, Object> createAndEditContent(int initActionId, String contentTypeId, String siteName, String contentName, String contentTitle, String language, Map<String, Object> values, String workflowName, String viewName, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException
405    {
406        return createAndEditContent(initActionId, new String[] {contentTypeId}, ArrayUtils.EMPTY_STRING_ARRAY, siteName, contentName, contentTitle, language, values, workflowName, viewName, inputs);
407    }
408    
409    /**
410     * Create and edit a content
411     * @param initActionId The initial workflow action id for creation and edition
412     * @param contentTypeIds The new content types. Cannot be null. Cannot be empty.
413     * @param mixinIds The new mixins. Can be null. Can be empty.
414     * @param siteName The current site name
415     * @param contentName The content name
416     * @param contentTitle The content title
417     * @param language The content language
418     * @param values The submitted values
419     * @param workflowName The workflow name
420     * @param viewName The view name
421     * @param inputs The initial workflow inputs
422     * @return The workflow result
423     * @throws AmetysRepositoryException if an error occurs
424     * @throws WorkflowException if an error occurs
425     */
426    public Map<String, Object> createAndEditContent(int initActionId, String[] contentTypeIds, String[] mixinIds, String siteName, String contentName, String contentTitle, String language, Map<String, Object> values, String workflowName, String viewName, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException
427    {
428        Request request = _getRequest();
429        
430        // Retrieve the current workspace.
431        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
432        
433        try
434        {
435            // Force the default workspace.
436            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
437            
438            // Workflow parameters.
439            inputs.put(org.ametys.web.workflow.CreateContentFunction.SITE_KEY, siteName);
440            
441            Map<String, Object> contextParameters = new HashMap<>();
442            contextParameters.put("quit", true);
443            contextParameters.put("values", values);
444            if (viewName != null)
445            {
446                contextParameters.put(EditContentFunction.METADATA_SET_PARAM, viewName);
447            }
448            
449            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
450            
451            return _contentWorkflowHelper.createContent(workflowName, initActionId, contentName, contentTitle, contentTypeIds, mixinIds, language, null, null, inputs);
452        }
453        finally
454        {
455            // Restore context
456            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
457        }
458        
459    }
460    
461    private boolean _validateFormField(MetadataDefinition metadataDef, String fieldName, Object value, Multimap<String, I18nizableText> errors)
462    {
463        Validator validator = metadataDef.getValidator();
464        if (validator != null)
465        {
466            Errors fieldErrors = new Errors();
467            validator.validate(value, fieldErrors);
468            if (fieldErrors.hasErrors())
469            {
470                for (I18nizableText error : fieldErrors.getErrors())
471                {
472                    errors.put(fieldName, error);
473                }
474                
475                return false;
476            }
477        }
478        
479        return true;
480    }
481    
482    private Map<String, Object> _getUploadFileValue(Part partUploaded)
483    {
484        Map<String, Object> file = new LinkedHashMap<>();
485
486        Upload upload = null;
487        try (InputStream is = partUploaded.getInputStream())
488        {
489            upload = _uploadManager.storeUpload(_currentUserProvider.getUser(), partUploaded.getFileName(), is);
490            
491            file.put("id", upload.getId());
492            file.put("filename", upload.getFilename());
493            file.put("size", upload.getLength());
494            file.put("viewHref", "/plugins/core/upload/file?id=" + upload.getId());
495            file.put("downloadHref", "/plugins/core/upload/file?id=" + upload.getId() + "&download=true");
496            file.put("type", "metadata");
497        }
498        catch (IOException e)
499        {
500            getLogger().error("Unable to store uploaded file: " + partUploaded, e);
501            return null;
502        }
503        
504        return file;
505    }
506}