001/*
002 *  Copyright 2010 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.plugins.workflow.cocoon;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.avalon.framework.activity.Initializable;
026import org.apache.avalon.framework.parameters.Parameters;
027import org.apache.cocoon.acting.ServiceableAction;
028import org.apache.cocoon.environment.ObjectModelHelper;
029import org.apache.cocoon.environment.Redirector;
030import org.apache.cocoon.environment.Request;
031import org.apache.cocoon.environment.SourceResolver;
032import org.apache.commons.lang3.StringUtils;
033
034import org.ametys.core.authentication.AuthenticateAction;
035import org.ametys.core.cocoon.ActionResultGenerator;
036import org.ametys.plugins.workflow.AbstractWorkflowComponent;
037import org.ametys.plugins.workflow.AbstractWorkflowComponent.ConditionFailure;
038import org.ametys.plugins.workflow.component.CheckRightsCondition;
039import org.ametys.plugins.workflow.support.WorkflowProvider;
040import org.ametys.runtime.i18n.I18nizableText;
041import org.ametys.runtime.parameter.ValidationResult;
042
043import com.opensymphony.workflow.InvalidActionException;
044import com.opensymphony.workflow.InvalidInputException;
045import com.opensymphony.workflow.WorkflowException;
046
047/**
048 * Abstract action for managing a workflow instance.
049 * The following parameters are supported:
050 * <dl>
051 *  <dt>actionId
052 *  <dd>the id of the action to fire
053 * </dl>
054 */
055public abstract class AbstractWorkflowAction extends ServiceableAction implements Initializable
056{
057    /** Workflow provider */
058    protected WorkflowProvider _workflowProvider;
059    
060    @Override
061    public void initialize() throws Exception
062    {
063        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
064    }
065    
066    @Override
067    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
068    {
069        int actionId = _getActionId(objectModel, source, parameters);
070        Map inputs = _getInputs(redirector, objectModel, source, parameters);
071        
072        try
073        {
074            Map result = _act(redirector, objectModel, source, parameters, actionId, inputs);
075            
076            @SuppressWarnings("unchecked")
077            Map<String, Object> resultMap = (Map<String, Object>) inputs.get(AbstractWorkflowComponent.RESULT_MAP_KEY);
078            resultMap.put("success", true);
079            
080            return result;
081        }
082        catch (InvalidActionException e)
083        {
084            return _processInvalidActionException(redirector, objectModel, source, parameters, actionId, inputs, e);
085        }
086        catch (WorkflowException e)
087        {
088            return _processWorkflowException(redirector, objectModel, source, parameters, actionId, e);
089        }
090    }
091
092    /**
093     * Provide the action id to use.
094     * Default implementation uses parameter <code>actionId</code>.
095     * @param objectModel the current object model.
096     * @param source the current source.
097     * @param parameters the current parameters.
098     * @return the action id.
099     * @throws Exception if an error occurs.
100     */
101    protected int _getActionId(Map objectModel, String source, Parameters parameters) throws Exception
102    {
103        return parameters.getParameterAsInteger("actionId", Integer.MIN_VALUE);
104    }
105
106    /**
107     * Provide the inputs to use.
108     * Default implementation provide the redirector.
109     * @param redirector the redirector.
110     * @param objectModel the current object model.
111     * @param source the current source.
112     * @param parameters the current parameters.
113     * @return the inputs to use.
114     * @throws Exception if an error occurs.
115     */
116    protected Map<String, Object> _getInputs(Redirector redirector, Map objectModel, String source, Parameters parameters) throws Exception
117    {
118        Map<String, Object> inputs = new HashMap<>();        
119        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, objectModel.get(ObjectModelHelper.PARENT_CONTEXT));
120        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<>());
121        
122        Request request = ObjectModelHelper.getRequest(objectModel);
123        if (request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED) != null)
124        {
125            inputs.put(CheckRightsCondition.FORCE, true);
126        }
127        
128        // Provide a map for providing data to the generator
129        Map<String, Object> result = new LinkedHashMap<>();
130        request.setAttribute(ActionResultGenerator.MAP_REQUEST_ATTR, result);
131        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, result);
132        
133        return inputs;
134    }
135
136    /**
137     * Initialize or act on a workflow instance.
138     * @param redirector the redirector.
139     * @param objectModel the current object model.
140     * @param source the current source.
141     * @param parameters the current parameters.
142     * @param actionId the action id to use.
143     * @param inputs the inputs to use
144     * @return the action result.
145     * @throws InvalidInputException if the action id is not valid.
146     * @throws WorkflowException if the action failed.
147     */
148    protected abstract Map _act(Redirector redirector, Map objectModel, String source, Parameters parameters, int actionId, Map inputs) throws InvalidInputException, WorkflowException;
149
150    /**
151     * Called when the current action is invalid.
152     * Default implementation throw an exception.
153     * @param redirector the redirector.
154     * @param objectModel the current object model.
155     * @param source the current source.
156     * @param parameters the current parameters.
157     * @param actionId the invalid action id.
158     * @param inputs the inputs
159     * @param e the invalid action exception.
160     * @return the action result.
161     * @throws Exception in order to stop the current pipeline.
162     */
163    protected Map _processInvalidActionException(Redirector redirector, Map objectModel, String source, Parameters parameters, long actionId, Map inputs, InvalidActionException e) throws Exception
164    {
165        if (_getWorkflowErrors(inputs) != null)
166        {
167            // Errors was traited in result map
168            getLogger().error(String.format("Invalid action: '%d' for %s. %s", actionId, _getExceptionContext(objectModel, source, parameters), _getFailConditions(inputs)), e);
169            return Collections.EMPTY_MAP;
170        }
171        else
172        {
173            // The error was not traited
174            throw new Exception(String.format("Invalid action: '%d' for %s. %s", actionId, _getExceptionContext(objectModel, source, parameters), _getFailConditions(inputs)), e);
175        }
176    }
177    
178    /**
179     * Get the fail conditions as a String
180     * @param inputs the inputs
181     * @return the fail conditions
182     */
183    protected String _getFailConditions (Map inputs)
184    {
185        if (inputs.containsKey(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY))
186        {
187            @SuppressWarnings("unchecked")
188            List<ConditionFailure> failConditions = (List<ConditionFailure>) inputs.get(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY);
189            if (failConditions.size() > 0)
190            {
191                return String.format("Fail conditions are: %s", StringUtils.join(failConditions.stream().map(ConditionFailure::text).toList(), " / "));
192            }
193        }
194        
195        return "";
196    }
197    
198    /**
199     * Get the workflow errors
200     * @param inputs The inputs
201     * @return The workflow errors or null if not found
202     */
203    protected List<I18nizableText> _getWorkflowErrors (Map inputs)
204    {
205        @SuppressWarnings("unchecked")
206        Map<String, Object> result = (Map<String, Object>) inputs.get(AbstractWorkflowComponent.RESULT_MAP_KEY);
207        
208        if (result.containsKey(AbstractWorkflowComponent.WORKFLOW_VALIDATION_KEY))
209        {
210            return ((ValidationResult) result.get(AbstractWorkflowComponent.WORKFLOW_VALIDATION_KEY)).getErrors();
211        }
212        
213        return null;
214    }
215    
216    /**
217     * Get the workflow warnings
218     * @param inputs The inputs
219     * @return The workflow warnings or null if not found
220     */
221    @SuppressWarnings("unchecked")
222    protected List<I18nizableText> _getWorkflowWarns (Map inputs)
223    {
224        Map<String, Object> result = (Map<String, Object>) inputs.get(AbstractWorkflowComponent.RESULT_MAP_KEY);
225        
226        if (result.containsKey(AbstractWorkflowComponent.WORKFLOW_VALIDATION_KEY))
227        {
228            return ((ValidationResult) result.get(AbstractWorkflowComponent.WORKFLOW_VALIDATION_KEY)).getWarnings();
229        }
230        
231        return null;
232    }
233
234    /**
235     * Called when the current action has thrown a {@link WorkflowException}.
236     * Default implementation throw an exception.
237     * @param redirector the redirector.
238     * @param objectModel the current object model.
239     * @param source the current source.
240     * @param parameters the current parameters.
241     * @param actionId the invalid action id.
242     * @param e the workflow exception.
243     * @return the action result.
244     * @throws Exception in order to stop the current pipeline.
245     */
246    protected Map _processWorkflowException(Redirector redirector, Map objectModel, String source, Parameters parameters, long actionId, WorkflowException e) throws Exception
247    {
248        throw new Exception(String.format("Unable to perform action: '%d' for %s", actionId, _getExceptionContext(objectModel, source, parameters)), e);
249    }
250
251    /**
252     * Provides the exception context for a more friendly error message.
253     * @param objectModel the current object model.
254     * @param source the current source.
255     * @param parameters the current parameters.
256     * @return the message describing the exception context.
257     */
258    protected abstract String _getExceptionContext(Map objectModel, String source, Parameters parameters);
259}