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