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