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}