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}