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.util.HashMap;
019import java.util.LinkedHashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Map.Entry;
023import java.util.stream.Collectors;
024
025import org.apache.avalon.framework.context.Context;
026import org.apache.avalon.framework.context.ContextException;
027import org.apache.avalon.framework.context.Contextualizable;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.cocoon.components.ContextHelper;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.xml.AttributesImpl;
033import org.apache.cocoon.xml.XMLUtils;
034import org.apache.commons.collections.MapUtils;
035import org.apache.commons.lang3.ArrayUtils;
036import org.apache.commons.lang3.LocaleUtils;
037import org.apache.commons.lang3.StringUtils;
038import org.xml.sax.ContentHandler;
039import org.xml.sax.SAXException;
040
041import org.ametys.cms.contenttype.ContentAttributeDefinition;
042import org.ametys.cms.contenttype.ContentType;
043import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
044import org.ametys.cms.data.type.ModelItemTypeConstants;
045import org.ametys.cms.repository.Content;
046import org.ametys.cms.search.query.AndQuery;
047import org.ametys.cms.search.query.ContentLanguageQuery;
048import org.ametys.cms.search.query.ContentTypeQuery;
049import org.ametys.cms.search.query.DocumentTypeQuery;
050import org.ametys.cms.search.query.Query;
051import org.ametys.cms.search.solr.SearcherFactory.Searcher;
052import org.ametys.cms.workflow.ContentWorkflowHelper;
053import org.ametys.cms.workflow.CreateContentFunction;
054import org.ametys.cms.workflow.EditContentFunction;
055import org.ametys.plugins.repository.AmetysObjectIterable;
056import org.ametys.plugins.repository.AmetysRepositoryException;
057import org.ametys.plugins.repository.RepositoryConstants;
058import org.ametys.plugins.repository.jcr.NameHelper.NameComputationMode;
059import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
060import org.ametys.plugins.workflow.AbstractWorkflowComponent;
061import org.ametys.runtime.i18n.I18nizableText;
062import org.ametys.runtime.model.ModelHelper;
063import org.ametys.web.FOAmetysObjectCreationHelper;
064import org.ametys.web.frontoffice.FrontOfficeSearcherFactory;
065
066import com.google.common.collect.Multimap;
067import com.opensymphony.workflow.WorkflowException;
068
069/**
070 * Helper for creating and editing a content from the submitted form
071 */
072public class FOContentCreationHelper extends FOAmetysObjectCreationHelper implements Contextualizable
073{
074    /** The component role. */
075    @SuppressWarnings("hiding")
076    public static final String ROLE = FOContentCreationHelper.class.getName();
077    
078    private ContentWorkflowHelper _contentWorkflowHelper;
079
080    private Context _context;
081
082    private ContentTypeExtensionPoint _cTypeExtPt;
083
084    private FrontOfficeSearcherFactory _searcherFactory;
085    
086    @Override
087    public void service(ServiceManager smanager) throws ServiceException
088    {
089        super.service(smanager);
090        _cTypeExtPt = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
091        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
092        _searcherFactory = (FrontOfficeSearcherFactory) smanager.lookup(FrontOfficeSearcherFactory.ROLE);
093    }
094    
095    public void contextualize(Context context) throws ContextException
096    {
097        _context = context;
098    }
099    
100    /**
101     * Get the request
102     * @return the request
103     */
104    protected Request _getRequest()
105    {
106        return ContextHelper.getRequest(_context);
107    }
108    
109    /**
110     * SAX contents values for metadata of type CONTENT
111     * @param contentHandler The content handler to sax into
112     * @param contentType The content type
113     * @param rootTagName The root tag name
114     * @param language the current language
115     * @throws SAXException if an error occurs when saxing
116     */
117    public void saxContentValues(ContentHandler contentHandler, ContentType contentType, String rootTagName, String language) throws SAXException
118    {
119        List<ContentAttributeDefinition> contentAttributes = ModelHelper.findModelItemsByType(contentType, ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID)
120                                                                        .stream()
121                                                                        .filter(ContentAttributeDefinition.class::isInstance) // Avoid properties, that are not modifiables
122                                                                        .map(ContentAttributeDefinition.class::cast)
123                                                                        .collect(Collectors.toList());
124        for (ContentAttributeDefinition contentAttribute : contentAttributes)
125        {
126            AttributesImpl attrs = new AttributesImpl();
127            attrs.addCDATAAttribute("name", contentAttribute.getPath().replace("/", "."));
128            XMLUtils.startElement(contentHandler, rootTagName, attrs);
129            _saxContentEnumeratorValue(contentHandler, contentAttribute, language);
130            XMLUtils.endElement(contentHandler, rootTagName);
131        }
132    }
133
134
135    /**
136     * Sax enumeration value for enum or an attribute of type content
137     * @param contentHandler The content handler to sax into
138     * @param attribute The attribute of type content
139     * @param language The current language
140     * @throws SAXException If an error occurred while saxing
141     */
142    private void _saxContentEnumeratorValue(ContentHandler contentHandler, ContentAttributeDefinition attribute, String language) throws SAXException
143    {
144        Map<String, String> values = getContentValues(attribute.getContentTypeId(), language);
145        
146        XMLUtils.startElement(contentHandler, "enumeration");
147        for (Entry<String, String> entry : values.entrySet())
148        {
149            AttributesImpl attrItem = new AttributesImpl();
150            attrItem.addCDATAAttribute("value", entry.getKey());
151            XMLUtils.startElement(contentHandler, "item", attrItem);
152            XMLUtils.createElement(contentHandler, "label", entry.getValue());
153            XMLUtils.endElement(contentHandler, "item");
154        }
155        XMLUtils.endElement(contentHandler, "enumeration");
156    }
157    
158    /**
159     * Get values for contents enumeration
160     * @param cTypeId The id of content type
161     * @param language The current language
162     * @return The contents
163     */
164    protected Map<String, String> getContentValues(String cTypeId, String language)
165    {
166        try
167        {
168            Query query = null;
169            boolean multilingual = false;
170            if (StringUtils.isNotEmpty(cTypeId))
171            {
172                query = new ContentTypeQuery(cTypeId);
173                multilingual = _cTypeExtPt.getExtension(cTypeId).isMultilingual();
174            }
175            
176            if (!multilingual)
177            {
178                query = query != null ? new AndQuery(query, new ContentLanguageQuery(language)) : new ContentLanguageQuery(language);
179            }
180            
181            Searcher searcher = _searcherFactory.create().withQuery(query)
182                    .addFilterQuery(new DocumentTypeQuery("content"))
183                    .withLimits(0, Integer.MAX_VALUE);
184
185            AmetysObjectIterable<Content> contents = searcher.search();
186            
187            return contents.stream()
188                    .collect(Collectors.toMap(Content::getId, c -> c.getTitle(LocaleUtils.toLocale(language))))
189                    .entrySet()
190                    .stream()
191                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
192        }
193        catch (Exception e)
194        {
195            getLogger().error("Failed to get content enumeration for content type " +  cTypeId, e);
196            return MapUtils.EMPTY_MAP;
197        }
198    }
199    
200    /**
201     * Get the values for this content type from request
202     * @param request the request
203     * @param contentType the edited content type
204     * @param viewName the view name
205     * @param errors The errors to fill
206     * @return The values
207     */
208    public Map<String, Object> getAndValidateFormValues(Request request, ContentType contentType, String viewName, Multimap<String, I18nizableText> errors)
209    {
210        Map<String, Object> values = getFormValues(request, contentType.getView(viewName), StringUtils.EMPTY, errors);
211        errors.putAll(validateValues(values, contentType.getView(viewName)));
212        return values;
213    }
214    
215    /**
216     * Create and edit a content
217     * @param initActionId The initial workflow action id for creation and edition
218     * @param contentTypeId The id of content type
219     * @param siteName The current site name
220     * @param contentName The content name
221     * @param contentTitle The content title
222     * @param language The content language
223     * @param values The submitted values
224     * @param workflowName The workflow name
225     * @param viewName The view name
226     * @return The workflow result
227     * @throws AmetysRepositoryException if an error occurs
228     * @throws WorkflowException if an error occurs
229     */
230    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
231    {
232        return createAndEditContent(initActionId, contentTypeId, siteName, contentName, contentTitle, language, values, workflowName, viewName, new HashMap<>());
233    }
234    
235    /**
236     * Create and edit a content
237     * @param initActionId The initial workflow action id for creation and edition
238     * @param contentTypeId The id of content type
239     * @param siteName The current site name
240     * @param contentName The content name
241     * @param contentTitle The content title
242     * @param language The content language
243     * @param values The submitted values
244     * @param workflowName The workflow name
245     * @param viewName The view name
246     * @param inputs The initial workflow inputs
247     * @return The workflow result
248     * @throws AmetysRepositoryException if an error occurs
249     * @throws WorkflowException if an error occurs
250     */
251    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
252    {
253        return createAndEditContent(initActionId, new String[] {contentTypeId}, ArrayUtils.EMPTY_STRING_ARRAY, siteName, contentName, contentTitle, language, values, workflowName, viewName, inputs);
254    }
255    
256    /**
257     * Create and edit a content
258     * @param initActionId The initial workflow action id for creation and edition
259     * @param contentTypeIds The new content types. Cannot be null. Cannot be empty.
260     * @param mixinIds The new mixins. Can be null. Can be empty.
261     * @param siteName The current site name
262     * @param contentName The content name
263     * @param contentTitle The content title
264     * @param language The content language
265     * @param values The values of the content attributes
266     * @param workflowName The workflow name
267     * @param viewName The view name
268     * @param inputs The initial workflow inputs
269     * @return The workflow result
270     * @throws AmetysRepositoryException if an error occurs
271     * @throws WorkflowException if an error occurs
272     */
273    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
274    {
275        Request request = _getRequest();
276        
277        // Retrieve the current workspace.
278        String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
279        
280        try
281        {
282            // Force the default workspace.
283            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
284            
285            // Workflow parameters.
286            inputs.put(org.ametys.web.workflow.CreateContentFunction.SITE_KEY, siteName);
287            
288            Map<String, Object> contextParameters = new HashMap<>();
289            contextParameters.put(EditContentFunction.QUIT, true);
290            contextParameters.put(EditContentFunction.VALUES_KEY, values);
291            if (viewName != null)
292            {
293                contextParameters.put(EditContentFunction.VIEW_NAME, viewName);
294            }
295            
296            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
297            // In FO mode, we always have the name as the default title, to avoid conflicts, we force generated key on content name
298            inputs.put(CreateContentFunction.NAME_COMPUTATION_MODE_KEY, NameComputationMode.GENERATED_KEY.toString());
299            
300            return _contentWorkflowHelper.createContent(workflowName, initActionId, contentName, contentTitle, contentTypeIds, mixinIds, language, inputs);
301        }
302        finally
303        {
304            // Restore context
305            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
306        }
307        
308    }
309}