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}