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