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        int[] actionIds = getAvailableActions(content);
207        return ArrayUtils.contains(actionIds, actionId);
208    }
209    
210    /**
211     * Get the available workflow actions for the content
212     * @param content The content to consider. Cannot be null.
213     * @return The array of actions ids that are available now
214     */
215    public int[] getAvailableActions(WorkflowAwareContent content)
216    {
217        Map<String, Object> inputs = new HashMap<>();
218        return getAvailableActions(content, inputs);
219    }
220    
221    /**
222     * Get the available workflow actions for the content
223     * @param content The content to consider. Cannot be null.
224     * @param inputs The parameters to transmit to the workflow functions. Cannot be null. 
225     * @return The array of actions ids that are available now
226     */
227    public int[] getAvailableActions(WorkflowAwareContent content, Map<String, Object> inputs)
228    {
229        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
230        long wId = content.getWorkflowId();
231        
232        List<ConditionFailure> failures = new ArrayList<>();
233        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
234        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, failures);
235        
236        int[] availableActions = workflow.getAvailableActions(wId, inputs);
237        
238        if (!failures.isEmpty() && getLogger().isDebugEnabled())
239        {
240            getLogger().debug("Validation failures obtained while looking for available actions for content '{}'\n{}", content.getId(), String.join("\n", failures.stream().map(ConditionFailure::text).toList()));
241        }
242        
243        return availableActions;
244    }
245    
246    /**
247     * Do a workflow action on a content.
248     * @param content The content to act on. Cannot be null.
249     * @param actionId The id of the workflow action to do
250     * @return The results of the functions
251     * @throws WorkflowException If an error occurred while doing the action on the workflow
252     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
253     */
254    public Map<String, Object> doAction(WorkflowAwareContent content, int actionId) throws AmetysRepositoryException, WorkflowException
255    {
256        Map<String, Object> inputs = new HashMap<>();
257        return doAction(content, actionId, inputs);
258    }
259    
260    /**
261     * Do a workflow action on a content.
262     * @param content The content to act on. Cannot be null.
263     * @param actionId The id of the workflow action to do
264     * @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).
265     * @return The results of the functions
266     * @throws WorkflowException If an error occurred while doing the action on the workflow
267     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
268     */
269    public Map<String, Object> doAction(WorkflowAwareContent content, int actionId, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException
270    {
271        return doAction(content, actionId, inputs, true);
272    }
273    
274    /**
275     * Do a workflow action on a content.
276     * @param content The content to act on. Cannot be null.
277     * @param actionId The id of the workflow action to do
278     * @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).
279     * @param logError <code>true</code> to log the action error
280     * @return The results of the functions
281     * @throws WorkflowException If an error occurred while doing the action on the workflow
282     * @throws AmetysRepositoryException If cannot get the workflow identifier of the content
283     */
284    public Map<String, Object> doAction(WorkflowAwareContent content, int actionId, Map<String, Object> inputs, boolean logError) throws AmetysRepositoryException, WorkflowException
285    {
286        if (getLogger().isInfoEnabled())
287        {
288            getLogger().info("User " + _getUser() + " try to perform action " + actionId + " on content " + content.getId());
289        }
290
291        Map<String, Object> results = new LinkedHashMap<>();
292        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results);
293        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
294        
295        List<ConditionFailure> failures = new ArrayList<>();
296        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, failures);
297        
298        if (inputs.get(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY) == null)
299        {
300            Map objectModel = ContextHelper.getObjectModel(_context);
301            @SuppressWarnings("unchecked")
302            Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
303            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, jsParameters);
304        }
305
306        try
307        {
308            Request request = ContextHelper.getRequest(_context);
309            request.setAttribute(Content.class.getName(), content);
310            
311            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
312            workflow.doAction(content.getWorkflowId(), actionId, inputs);
313        }
314        catch (InvalidActionException e)
315        {
316            if (logError)
317            {
318                String failureString = "";
319                if (!failures.isEmpty())
320                {
321                    failureString = ", due to the following error(s):\n" + String.join("\n", failures.stream().map(ConditionFailure::text).toList());
322                }
323                
324                getLogger().error("Cannot perform workflow action {} on content '{}'{}", actionId, content.getId(), failureString, e);
325            }
326            throw e; 
327        }
328        
329        return results;
330    }
331    
332    /**
333     * Edit a {@link Content} programmatically.
334     * @param content the {@link ModifiableContent}.
335     * @param values the typed values to set.
336     * @param workflowActionId the id of the workflow action
337     * @return the workflow results.
338     * @throws WorkflowException if an error occurs while processing the workflow action
339     */
340    public Map<String, Object> editContent(WorkflowAwareContent content, Map<String, Object> values, int workflowActionId) throws WorkflowException
341    {
342        return editContent(content, values, workflowActionId, null);
343    }
344    
345    /**
346     * Edit a {@link Content} programmatically.
347     * @param content the {@link ModifiableContent}.
348     * @param values the typed values to set.
349     * @param workflowActionId the id of the workflow action
350     * @param view the view to edit
351     * @return the workflow results.
352     * @throws WorkflowException if an error occurs while processing the workflow action
353     */
354    public Map<String, Object> editContent(WorkflowAwareContent content, Map<String, Object> values, int workflowActionId, View view) throws WorkflowException
355    {
356        Map<String, Object> parameters = new HashMap<>();
357        parameters.put(EditContentFunction.VIEW, view);
358        parameters.put(EditContentFunction.VALUES_KEY, values);
359        parameters.put(EditContentFunction.QUIT, true);
360
361        Map<String, Object> inputs = new HashMap<>();
362        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, parameters);
363        
364        return doAction(content, workflowActionId, inputs);
365    }
366    
367    private UserIdentity _getUser()
368    {
369        return _userProvider.getUser();
370    }
371}