001/*
002 *  Copyright 2014 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.cms.workflow;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022
023import org.apache.avalon.framework.component.Component;
024import org.apache.avalon.framework.context.Context;
025import org.apache.avalon.framework.context.ContextException;
026import org.apache.avalon.framework.context.Contextualizable;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.cocoon.components.ContextHelper;
031import org.apache.cocoon.environment.ObjectModelHelper;
032import org.apache.cocoon.environment.Request;
033import org.apache.commons.lang3.ArrayUtils;
034
035import org.ametys.cms.repository.Content;
036import org.ametys.cms.repository.ModifiableContent;
037import org.ametys.cms.repository.WorkflowAwareContent;
038import org.ametys.core.observation.ObservationManager;
039import org.ametys.core.user.CurrentUserProvider;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.plugins.repository.AmetysRepositoryException;
042import org.ametys.plugins.workflow.AbstractWorkflowComponent;
043import org.ametys.plugins.workflow.support.WorkflowProvider;
044import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
045import org.ametys.runtime.plugin.component.AbstractLogEnabled;
046
047import com.opensymphony.workflow.InvalidActionException;
048import com.opensymphony.workflow.WorkflowException;
049 
050/**
051 * A component to do workflow actions on Content
052 */
053public class ContentWorkflowHelper extends AbstractLogEnabled implements Serviceable, Contextualizable, Component
054{
055    /** The component role */
056    public static final String ROLE = ContentWorkflowHelper.class.getName();
057    
058    /** Component to get the current user */
059    protected CurrentUserProvider _userProvider;
060    
061    /** Workflow instance. */
062    protected WorkflowProvider _workflowProvider;
063
064    /** The observation manager */
065    protected ObservationManager _observationManager;
066    
067    private Context _context;
068
069
070    @Override
071    public void contextualize(Context context) throws ContextException
072    {
073        _context = context;
074    }
075    
076    @Override
077    public void service(ServiceManager manager) throws ServiceException
078    {
079        _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
080        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
081        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
082    }
083    
084    /**
085     * Creates a content using the workflow (with the CreateContentFunction).
086     * @param workflowName The name of the workflow to create
087     * @param initialActionId The workflow action id that creates content
088     * @param contentName The new name
089     * @param contentTitle The new title
090     * @param contentTypes The new content types. Cannot be null. Cannot be empty.
091     * @param mixins The new mixins. Can be null. Can be empty.
092     * @param languageCode The language code of the new content (such as 'fr', 'en'...)
093     * @return The workflow result map. See the create content function used to get the new content. Can be under the key CreateContentFunction.CONTENT_KEY, and the id under the key "contentId"
094     * @throws WorkflowException If an error occurred while doing the action on the workflow
095     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
096     */
097    public Map<String, Object> createContent(String workflowName, int initialActionId, String contentName, String contentTitle, String[] contentTypes, String[] mixins, String languageCode) throws AmetysRepositoryException, WorkflowException
098    {
099        return createContent(workflowName, initialActionId, contentName, contentTitle, contentTypes, mixins, languageCode, null, null);
100    }
101    
102    /**
103     * Creates a multilingual content with a multilingual title using the workflow (with the CreateContentFunction).
104     * @param workflowName The name of the workflow to create
105     * @param initialActionId The workflow action id that creates content
106     * @param contentName The new name
107     * @param titleVariants The title's variants
108     * @param contentTypes The new content types. Cannot be null. Cannot be empty.
109     * @param mixins The new mixins. Can be null. Can be empty.
110     * @return The workflow result map. See the create content function used to get the new content. Can be under the key CreateContentFunction.CONTENT_KEY, and the id under the key "contentId"
111     * @throws WorkflowException If an error occurred while doing the action on the workflow
112     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
113     */
114    public Map<String, Object> createContent(String workflowName, int initialActionId, String contentName, Map<String, String> titleVariants, String[] contentTypes, String[] mixins) throws AmetysRepositoryException, WorkflowException
115    {
116        return createContent(workflowName, initialActionId, contentName, titleVariants, contentTypes, mixins, null, null);
117    }
118
119    /**
120     * Creates a content using the workflow (with the CreateContentFunction).
121     * @param workflowName The name of the workflow to create
122     * @param initialActionId The workflow action id that creates content
123     * @param contentName The new name
124     * @param contentTitle The new title
125     * @param contentTypes The new content types. Cannot be null. Cannot be empty.
126     * @param mixins The new mixins. Can be null. Can be empty.
127     * @param languageCode The language code of the new content (such as 'fr', 'en'...)
128     * @param parentContentId If the new content is a subcontent, the parent content identifier.
129     * @param parentContentMetadatapath If the new content is a subcontent, the path of the metadata where the new content will take place in its parent
130     * @return The workflow result map. See the create content function used to get the new content. Can be under the key CreateContentFunction.CONTENT_KEY, and the id under the key "contentId"
131     * @throws WorkflowException If an error occurred while doing the action on the workflow
132     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
133
134     */
135    public Map<String, Object> createContent(String workflowName, int initialActionId, String contentName, String contentTitle, String[] contentTypes, String[] mixins, String languageCode, String parentContentId, String parentContentMetadatapath) throws AmetysRepositoryException, WorkflowException
136    {
137        Map<String, Object> inputs = new HashMap<>();
138        return createContent(workflowName, initialActionId, contentName, contentTitle, contentTypes, mixins, languageCode, parentContentId, parentContentMetadatapath, inputs);
139    }
140    
141    /**
142     * Creates a multilingual content with a multilingual title using the workflow (with the CreateContentFunction).
143     * @param workflowName The name of the workflow to create
144     * @param initialActionId The workflow action id that creates content
145     * @param contentName The new name
146     * @param titleVariants The title's variants
147     * @param contentTypes The new content types. Cannot be null. Cannot be empty.
148     * @param mixins The new mixins. Can be null. Can be empty.
149     * @return The workflow result map. See the create content function used to get the new content. Can be under the key CreateContentFunction.CONTENT_KEY, and the id under the key "contentId"
150     * @param parentContentId If the new content is a subcontent, the parent content identifier.
151     * @param parentContentMetadatapath If the new content is a subcontent, the path of the metadata where the new content will take place in its parent
152     * @throws WorkflowException If an error occurred while doing the action on the workflow
153     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
154     */
155    public Map<String, Object> createContent(String workflowName, int initialActionId, String contentName, Map<String, String> titleVariants, String[] contentTypes, String[] mixins, String parentContentId, String parentContentMetadatapath) throws AmetysRepositoryException, WorkflowException
156    {
157        Map<String, Object> inputs = new HashMap<>();
158        return createContent(workflowName, initialActionId, contentName, titleVariants, contentTypes, mixins, parentContentId, parentContentMetadatapath, inputs);
159    }
160    
161    /**
162     * Creates a content using the workflow (with the CreateContentFunction).
163     * @param workflowName The name of the workflow to create
164     * @param initialActionId The workflow action id that creates content
165     * @param contentName The new name
166     * @param contentTitle The new title
167     * @param contentTypes The new content types. Cannot be null. Cannot be empty.
168     * @param mixins The new mixins. Can be null. Can be empty.
169     * @param languageCode The language code of the new content (such as 'fr', 'en'...)
170     * @param parentContentId If the new content is a subcontent, the parent content identifier.
171     * @param parentContentMetadatapath If the new content is a subcontent, the path of the metadata where the new content will take place in its parent
172     * @param inputs The parameters to transmit to the workflow functions. Cannot be null. 
173     * @return The workflow result map. See the create content function used to get the new content. Can be under the key CreateContentFunction.CONTENT_KEY, and the id under the key "contentId"
174     * @throws WorkflowException If an error occurred while doing the action on the workflow
175     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
176
177     */
178    public Map<String, Object> createContent(String workflowName, int initialActionId, String contentName, String contentTitle, String[] contentTypes, String[] mixins, String languageCode, String parentContentId, String parentContentMetadatapath, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException 
179    {
180        _getCommonInputsForCreation(inputs, contentName, contentTypes, mixins, parentContentId, parentContentMetadatapath);
181        inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, contentTitle);
182        inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, languageCode);
183        
184        return _doInitialize(workflowName, contentName, initialActionId, inputs);
185    }
186    
187    /**
188     * Creates a multilingual content with a multilingual title using the workflow (with the CreateContentFunction).
189     * @param workflowName The name of the workflow to create
190     * @param initialActionId The workflow action id that creates content
191     * @param contentName The new name
192     * @param titleVariants The title's variants
193     * @param contentTypes The new content types. Cannot be null. Cannot be empty.
194     * @param mixins The new mixins. Can be null. Can be empty.
195     * @return The workflow result map. See the create content function used to get the new content. Can be under the key CreateContentFunction.CONTENT_KEY, and the id under the key "contentId"
196     * @param parentContentId If the new content is a subcontent, the parent content identifier.
197     * @param parentContentMetadatapath If the new content is a subcontent, the path of the metadata where the new content will take place in its parent
198     * @param inputs The parameters to transmit to the workflow functions. Cannot be null. 
199     * @throws WorkflowException If an error occurred while doing the action on the workflow
200     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
201     */
202    public Map<String, Object> createContent(String workflowName, int initialActionId, String contentName, Map<String, String> titleVariants, String[] contentTypes, String[] mixins, String parentContentId, String parentContentMetadatapath, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException
203    {
204        _getCommonInputsForCreation(inputs, contentName, contentTypes, mixins, parentContentId, parentContentMetadatapath);
205        inputs.put(CreateContentFunction.CONTENT_TITLE_VARIANTS_KEY, titleVariants);
206        
207        return _doInitialize(workflowName, contentName, initialActionId, inputs);
208    }
209    
210    @SuppressWarnings("unchecked")
211    private Map<String, Object> _doInitialize(String workflowName, String contentName, int initialActionId, Map<String, Object> inputs) throws WorkflowException
212    {
213        try
214        {
215            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow();
216            workflow.initialize(workflowName, initialActionId, inputs);
217            
218            return (Map<String, Object>) inputs.get(AbstractWorkflowComponent.RESULT_MAP_KEY);
219        }
220        catch (WorkflowException e)
221        {
222            getLogger().error("An error occured while creating workflow '" + workflowName + "' with action '" + initialActionId + "' to creates content '" + contentName + "'", e);
223            throw e;
224        }
225    }
226    
227    private void _getCommonInputsForCreation(Map<String, Object> inputs, String contentName, String[] contentTypes, String[] mixins, String parentContentId, String parentContentMetadatapath)
228    {
229        inputs.put(CreateContentFunction.CONTENT_NAME_KEY, contentName);
230        inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, contentTypes);
231        inputs.put(CreateContentFunction.CONTENT_MIXINS_KEY, mixins);
232        inputs.put(CreateContentFunction.PARENT_CONTENT_ID_KEY, parentContentId);
233        inputs.put(CreateContentFunction.PARENT_CONTENT_METADATA_PATH_KEY, parentContentMetadatapath);
234        
235        Map<String, Object> results = new HashMap<>();
236        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results);
237        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
238    }
239    
240    /**
241     * Determines if the workflow action is available
242     * @param content the content to consider.
243     * @param actionId the workflow action id to check
244     * @return <code>true</code> if the wortkflow action is available
245     */
246    public boolean isAvailableAction(WorkflowAwareContent content, int actionId)
247    {
248        int[] actionIds = getAvailableActions(content);
249        return ArrayUtils.contains(actionIds, actionId);
250    }
251    
252    /**
253     * Get the available workflow actions for the content
254     * @param content The content to consider. Cannot be null.
255     * @return The array of actions ids that are available now
256     */
257    public int[] getAvailableActions(WorkflowAwareContent content)
258    {
259        Map<String, Object> inputs = new HashMap<>();
260        return getAvailableActions(content, inputs);
261    }
262    
263    /**
264     * Get the available workflow actions for the content
265     * @param content The content to consider. Cannot be null.
266     * @param inputs The parameters to transmit to the workflow functions. Cannot be null. 
267     * @return The array of actions ids that are available now
268     */
269    public int[] getAvailableActions(WorkflowAwareContent content, Map<String, Object> inputs)
270    {
271        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
272        long wId = content.getWorkflowId();
273        
274        List<String> failures = new ArrayList<>();
275        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
276        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, failures);
277        
278        int[] availableActions = workflow.getAvailableActions(wId, inputs);
279        
280        if (!failures.isEmpty() && getLogger().isDebugEnabled())
281        {
282            getLogger().debug("Validation failures obtained while looking for available actions for content '{}'\n{}", content.getId(), String.join("\n", failures));
283        }
284        
285        return availableActions;
286    }
287    
288    /**
289     * Do a workflow action on a content.
290     * @param content The content to act on. Cannot be null.
291     * @param actionId The id of the workflow action to do
292     * @return The results of the functions
293     * @throws WorkflowException If an error occurred while doing the action on the workflow
294     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
295     */
296    public Map<String, Object> doAction(WorkflowAwareContent content, int actionId) throws AmetysRepositoryException, WorkflowException
297    {
298        Map<String, Object> inputs = new HashMap<>();
299        return doAction(content, actionId, inputs);
300    }
301    
302    /**
303     * Do a workflow action on a content.
304     * @param content The content to act on. Cannot be null.
305     * @param actionId The id of the workflow action to do
306     * @param inputs The parameters to transmit to the workflow functions. Cannot be null. The special key AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY will be filled with the parent context if null (this means that if your are in a request dispatched, you will automatically get the js parameters).
307     * @return The results of the functions
308     * @throws WorkflowException If an error occurred while doing the action on the workflow
309     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
310     */
311    public Map<String, Object> doAction(WorkflowAwareContent content, int actionId, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException
312    {
313        if (getLogger().isInfoEnabled())
314        {
315            getLogger().info("User " + _getUser() + " try to perform action " + actionId + " on content " + content.getId());
316        }
317
318        Map<String, Object> results = new HashMap<>();
319        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results);
320        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
321        
322        List<String> failures = new ArrayList<>();
323        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, failures);
324        
325        if (inputs.get(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY) == null)
326        {
327            Map objectModel = ContextHelper.getObjectModel(_context);
328            @SuppressWarnings("unchecked")
329            Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
330            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, jsParameters);
331        }
332
333        try
334        {
335            Request request = ContextHelper.getRequest(_context);
336            request.setAttribute(Content.class.getName(), content);
337            
338            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
339            workflow.doAction(content.getWorkflowId(), actionId, inputs);
340        }
341        catch (InvalidActionException e)
342        {
343            String failureString = "";
344            if (!failures.isEmpty())
345            {
346                failureString = ", due to the following error(s):\n" + String.join("\n", failures);
347            }
348            
349            getLogger().error("Cannot perform workflow action {} on content '{}'{}", actionId, content.getId(), failureString, e);
350            throw e; 
351        }
352        
353        return results;
354    }
355    
356    /**
357     * Edit a {@link Content} programmatically.
358     * @param content the {@link ModifiableContent}.
359     * @param values the typed values to set.
360     * @param workflowActionId the id of the workflow action
361     * @return the workflow results.
362     * @throws WorkflowException if an error occurrs while processing the workflow action
363     */
364    public Map<String, Object> editContent(WorkflowAwareContent content, Map<String, Object> values, int workflowActionId) throws WorkflowException
365    {
366        Map<String, Object> inputs = new HashMap<>();
367        Map<String, Object> parameters = new HashMap<>();
368        
369        parameters.put(EditContentFunction.VALUES_KEY, values);
370        parameters.put(EditContentFunction.QUIT, true);
371        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, parameters);
372        
373        return doAction(content, workflowActionId, inputs);
374    }
375
376    private UserIdentity _getUser()
377    {
378        return _userProvider.getUser();
379    }
380}