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