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