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