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.Map; 021 022import org.apache.avalon.framework.component.Component; 023import org.apache.avalon.framework.context.Context; 024import org.apache.avalon.framework.context.ContextException; 025import org.apache.avalon.framework.context.Contextualizable; 026import org.apache.avalon.framework.logger.AbstractLogEnabled; 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; 034import org.apache.commons.lang3.StringUtils; 035import org.apache.commons.lang3.tuple.Pair; 036 037import org.ametys.cms.ObservationConstants; 038import org.ametys.cms.repository.Content; 039import org.ametys.cms.repository.WorkflowAwareContent; 040import org.ametys.core.observation.Event; 041import org.ametys.core.observation.ObservationManager; 042import org.ametys.core.user.CurrentUserProvider; 043import org.ametys.core.user.UserIdentity; 044import org.ametys.plugins.repository.AmetysRepositoryException; 045import org.ametys.plugins.repository.RepositoryConstants; 046import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 047import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 048import org.ametys.plugins.repository.version.VersionableAmetysObject; 049import org.ametys.plugins.workflow.AbstractWorkflowComponent; 050import org.ametys.plugins.workflow.support.WorkflowProvider; 051import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 052import org.ametys.runtime.model.ModelItem; 053 054import com.opensymphony.workflow.InvalidActionException; 055import com.opensymphony.workflow.WorkflowException; 056 057/** 058 * A component to do workflow actions on Content 059 */ 060public class ContentWorkflowHelper extends AbstractLogEnabled implements Serviceable, Contextualizable, Component 061{ 062 /** The component role */ 063 public static final String ROLE = ContentWorkflowHelper.class.getName(); 064 065 /** Component to get the current user */ 066 protected CurrentUserProvider _userProvider; 067 068 /** Workflow instance. */ 069 protected WorkflowProvider _workflowProvider; 070 071 /** The observation manager */ 072 protected ObservationManager _observationManager; 073 074 private Context _context; 075 076 077 @Override 078 public void contextualize(Context context) throws ContextException 079 { 080 _context = context; 081 } 082 083 @Override 084 public void service(ServiceManager manager) throws ServiceException 085 { 086 _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 087 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 088 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 089 } 090 091 /** 092 * Creates a content using the workflow (with the CreateContentFunction). 093 * @param workflowName The name of the workflow to create 094 * @param initialActionId The workflow action id that creates content 095 * @param contentName The new name 096 * @param contentTitle The new title 097 * @param contentTypes The new content types. Cannot be null. Cannot be empty. 098 * @param mixins The new mixins. Can be null. Can be empty. 099 * @param languageCode The language code of the new content (such as 'fr', 'en'...) 100 * @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" 101 * @throws WorkflowException If an error occurred while doing the action on the workflow 102 * @throws AmetysRepositoryException If cannot get the workflow identifier of the content 103 */ 104 public Map<String, Object> createContent(String workflowName, int initialActionId, String contentName, String contentTitle, String[] contentTypes, String[] mixins, String languageCode) throws AmetysRepositoryException, WorkflowException 105 { 106 return createContent(workflowName, initialActionId, contentName, contentTitle, contentTypes, mixins, languageCode, null, null); 107 } 108 109 /** 110 * Creates a multilingual content with a multilingual title using the workflow (with the CreateContentFunction). 111 * @param workflowName The name of the workflow to create 112 * @param initialActionId The workflow action id that creates content 113 * @param contentName The new name 114 * @param titleVariants The title's variants 115 * @param contentTypes The new content types. Cannot be null. Cannot be empty. 116 * @param mixins The new mixins. Can be null. Can be empty. 117 * @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" 118 * @throws WorkflowException If an error occurred while doing the action on the workflow 119 * @throws AmetysRepositoryException If cannot get the workflow identifier of the content 120 */ 121 public Map<String, Object> createContent(String workflowName, int initialActionId, String contentName, Map<String, String> titleVariants, String[] contentTypes, String[] mixins) throws AmetysRepositoryException, WorkflowException 122 { 123 return createContent(workflowName, initialActionId, contentName, titleVariants, contentTypes, mixins, null, null); 124 } 125 126 /** 127 * Creates a content using the workflow (with the CreateContentFunction). 128 * @param workflowName The name of the workflow to create 129 * @param initialActionId The workflow action id that creates content 130 * @param contentName The new name 131 * @param contentTitle The new title 132 * @param contentTypes The new content types. Cannot be null. Cannot be empty. 133 * @param mixins The new mixins. Can be null. Can be empty. 134 * @param languageCode The language code of the new content (such as 'fr', 'en'...) 135 * @param parentContentId If the new content is a subcontent, the parent content identifier. 136 * @param parentContentMetadatapath If the new content is a subcontent, the path of the metadata where the new content will take place in its parent 137 * @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" 138 * @throws WorkflowException If an error occurred while doing the action on the workflow 139 * @throws AmetysRepositoryException If cannot get the workflow identifier of the content 140 141 */ 142 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 143 { 144 Map<String, Object> inputs = new HashMap<>(); 145 return createContent(workflowName, initialActionId, contentName, contentTitle, contentTypes, mixins, languageCode, parentContentId, parentContentMetadatapath, inputs); 146 } 147 148 /** 149 * Creates a multilingual content with a multilingual title using the workflow (with the CreateContentFunction). 150 * @param workflowName The name of the workflow to create 151 * @param initialActionId The workflow action id that creates content 152 * @param contentName The new name 153 * @param titleVariants The title's variants 154 * @param contentTypes The new content types. Cannot be null. Cannot be empty. 155 * @param mixins The new mixins. Can be null. Can be empty. 156 * @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" 157 * @param parentContentId If the new content is a subcontent, the parent content identifier. 158 * @param parentContentMetadatapath If the new content is a subcontent, the path of the metadata where the new content will take place in its parent 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, String parentContentId, String parentContentMetadatapath) throws AmetysRepositoryException, WorkflowException 163 { 164 Map<String, Object> inputs = new HashMap<>(); 165 return createContent(workflowName, initialActionId, contentName, titleVariants, contentTypes, mixins, parentContentId, parentContentMetadatapath, inputs); 166 } 167 168 /** 169 * Creates a content using the workflow (with the CreateContentFunction). 170 * @param workflowName The name of the workflow to create 171 * @param initialActionId The workflow action id that creates content 172 * @param contentName The new name 173 * @param contentTitle The new title 174 * @param contentTypes The new content types. Cannot be null. Cannot be empty. 175 * @param mixins The new mixins. Can be null. Can be empty. 176 * @param languageCode The language code of the new content (such as 'fr', 'en'...) 177 * @param parentContentId If the new content is a subcontent, the parent content identifier. 178 * @param parentContentMetadatapath If the new content is a subcontent, the path of the metadata where the new content will take place in its parent 179 * @param inputs The parameters to transmit to the workflow functions. Cannot be null. 180 * @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" 181 * @throws WorkflowException If an error occurred while doing the action on the workflow 182 * @throws AmetysRepositoryException If cannot get the workflow identifier of the content 183 184 */ 185 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 186 { 187 _getCommonInputsForCreation(inputs, contentName, contentTypes, mixins, parentContentId, parentContentMetadatapath); 188 inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, contentTitle); 189 inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, languageCode); 190 191 return _doInitialize(workflowName, contentName, initialActionId, inputs); 192 } 193 194 /** 195 * Creates a multilingual content with a multilingual title using the workflow (with the CreateContentFunction). 196 * @param workflowName The name of the workflow to create 197 * @param initialActionId The workflow action id that creates content 198 * @param contentName The new name 199 * @param titleVariants The title's variants 200 * @param contentTypes The new content types. Cannot be null. Cannot be empty. 201 * @param mixins The new mixins. Can be null. Can be empty. 202 * @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" 203 * @param parentContentId If the new content is a subcontent, the parent content identifier. 204 * @param parentContentMetadatapath If the new content is a subcontent, the path of the metadata where the new content will take place in its parent 205 * @param inputs The parameters to transmit to the workflow functions. Cannot be null. 206 * @throws WorkflowException If an error occurred while doing the action on the workflow 207 * @throws AmetysRepositoryException If cannot get the workflow identifier of the content 208 */ 209 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 210 { 211 _getCommonInputsForCreation(inputs, contentName, contentTypes, mixins, parentContentId, parentContentMetadatapath); 212 inputs.put(CreateContentFunction.CONTENT_TITLE_VARIANTS_KEY, titleVariants); 213 214 return _doInitialize(workflowName, contentName, initialActionId, inputs); 215 } 216 217 @SuppressWarnings("unchecked") 218 private Map<String, Object> _doInitialize(String workflowName, String contentName, int initialActionId, Map<String, Object> inputs) throws WorkflowException 219 { 220 try 221 { 222 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(); 223 workflow.initialize(workflowName, initialActionId, inputs); 224 225 return (Map<String, Object>) inputs.get(AbstractWorkflowComponent.RESULT_MAP_KEY); 226 } 227 catch (WorkflowException e) 228 { 229 getLogger().error("An error occured while creating workflow '" + workflowName + "' with action '" + initialActionId + "' to creates content '" + contentName + "'", e); 230 throw e; 231 } 232 } 233 234 private void _getCommonInputsForCreation(Map<String, Object> inputs, String contentName, String[] contentTypes, String[] mixins, String parentContentId, String parentContentMetadatapath) 235 { 236 inputs.put(CreateContentFunction.CONTENT_NAME_KEY, contentName); 237 inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, contentTypes); 238 inputs.put(CreateContentFunction.CONTENT_MIXINS_KEY, mixins); 239 inputs.put(CreateContentFunction.PARENT_CONTENT_ID_KEY, parentContentId); 240 inputs.put(CreateContentFunction.PARENT_CONTENT_METADATA_PATH_KEY, parentContentMetadatapath); 241 242 Map<String, Object> results = new HashMap<>(); 243 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results); 244 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>()); 245 } 246 247 /** 248 * Determines if the workflow action is available 249 * @param content the content to consider. 250 * @param actionId the workflow action id to check 251 * @return <code>true</code> if the wortkflow action is available 252 */ 253 public boolean isAvailableAction(WorkflowAwareContent content, int actionId) 254 { 255 int[] actionIds = getAvailableActions(content); 256 return ArrayUtils.contains(actionIds, actionId); 257 } 258 259 /** 260 * Get the available workflow actions for the content 261 * @param content The content to consider. Cannot be null. 262 * @return The array of actions ids that are available now 263 */ 264 public int[] getAvailableActions(WorkflowAwareContent content) 265 { 266 Map<String, Object> inputs = new HashMap<>(); 267 return getAvailableActions(content, inputs); 268 } 269 270 /** 271 * Get the available workflow actions for the content 272 * @param content The content to consider. Cannot be null. 273 * @param inputs The parameters to transmit to the workflow functions. Cannot be null. 274 * @return The array of actions ids that are available now 275 */ 276 public int[] getAvailableActions(WorkflowAwareContent content, Map<String, Object> inputs) 277 { 278 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content); 279 long wId = content.getWorkflowId(); 280 281 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 282 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String> ()); 283 284 return workflow.getAvailableActions(wId, inputs); 285 } 286 287 /** 288 * Do a workflow action on a content. 289 * @param content The content to act on. Cannot be null. 290 * @param actionId The id of the workflow action to do 291 * @return The results of the functions 292 * @throws WorkflowException If an error occurred while doing the action on the workflow 293 * @throws AmetysRepositoryException If cannot get the workflow identifier of the content 294 */ 295 public Map<String, Object> doAction(WorkflowAwareContent content, int actionId) throws AmetysRepositoryException, WorkflowException 296 { 297 Map<String, Object> inputs = new HashMap<>(); 298 return doAction(content, actionId, inputs); 299 } 300 301 /** 302 * Do a workflow action on a content. 303 * @param content The content to act on. Cannot be null. 304 * @param actionId The id of the workflow action to do 305 * @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). 306 * @return The results of the functions 307 * @throws WorkflowException If an error occurred while doing the action on the workflow 308 * @throws AmetysRepositoryException If cannot get the workflow identifier of the content 309 */ 310 public Map<String, Object> doAction(WorkflowAwareContent content, int actionId, Map<String, Object> inputs) throws AmetysRepositoryException, WorkflowException 311 { 312 if (getLogger().isInfoEnabled()) 313 { 314 getLogger().info("User " + _getUser() + " try to perform action " + actionId + " on content " + content.getId()); 315 } 316 317 Map<String, Object> results = new HashMap<>(); 318 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, results); 319 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 320 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>()); 321 322 if (inputs.get(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY) == null) 323 { 324 Map objectModel = ContextHelper.getObjectModel(_context); 325 @SuppressWarnings("unchecked") 326 Map<String, Object> jsParameters = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT); 327 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, jsParameters); 328 } 329 330 try 331 { 332 Request request = ContextHelper.getRequest(_context); 333 request.setAttribute(Content.class.getName(), content); 334 335 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content); 336 workflow.doAction(content.getWorkflowId(), actionId, inputs); 337 } 338 catch (InvalidActionException e) 339 { 340 getLogger().error("An error occured while do workflow action '" + actionId + "' on content '" + content.getId() + "'", e); 341 throw e; 342 } 343 344 return results; 345 } 346 347 private UserIdentity _getUser() 348 { 349 return _userProvider.getUser(); 350 } 351 352 /** 353 * Remove metadata from content and apply changes 354 * @param contentToEdit the content to edit 355 * @param valuePath the value path of the metadata to remove 356 * @param actionId the workflow action id to execute if changes have been made 357 * @param comment the comment of the action. Can be null. 358 * @return true if changes are made 359 * @throws WorkflowException if failed to remove metadata 360 */ 361 public boolean removeMetadata(WorkflowAwareContent contentToEdit, String valuePath, int actionId, String comment) throws WorkflowException 362 { 363 ModifiableCompositeMetadata metadataHolder = contentToEdit.getMetadataHolder(); 364 String metadataName = valuePath; 365 if (StringUtils.contains(valuePath, ModelItem.ITEM_PATH_SEPARATOR)) 366 { 367 metadataName = StringUtils.substringAfterLast(valuePath, ModelItem.ITEM_PATH_SEPARATOR); 368 String compositeValuePath = StringUtils.substringBeforeLast(valuePath, ModelItem.ITEM_PATH_SEPARATOR); 369 metadataHolder = _getCompositeFromValuePath(contentToEdit, compositeValuePath); 370 } 371 372 if (metadataHolder.hasMetadata(metadataName)) 373 { 374 if (!isAvailableAction(contentToEdit, actionId)) 375 { 376 throw new InvalidActionException("Invalid workflow action '" + actionId + "' on content '" + contentToEdit.getId() + "': unable to remove metadata '" + valuePath + "'"); 377 } 378 379 metadataHolder.removeMetadata(metadataName); 380 381 _applyChanges(contentToEdit, actionId, comment); 382 return true; 383 } 384 385 return false; 386 } 387 388 /** 389 * Remove string value of multiple metadata from content and apply changes 390 * @param contentToEdit the content to edit 391 * @param valuePath the value path of the metadata 392 * @param valueToRemove the value to remove 393 * @param actionId the workflow action id to execute if changes have been made 394 * @param comment the comment of the action. Can be null. 395 * @return true if changes are made 396 * @throws WorkflowException if an error occurred 397 */ 398 public boolean removeStringValueFromMultipleMetadata(WorkflowAwareContent contentToEdit, String valuePath, String valueToRemove, int actionId, String comment) throws WorkflowException 399 { 400 ModifiableCompositeMetadata metadataHolder = contentToEdit.getMetadataHolder(); 401 String metadataName = valuePath; 402 if (StringUtils.contains(valuePath, ModelItem.ITEM_PATH_SEPARATOR)) 403 { 404 metadataName = StringUtils.substringAfterLast(valuePath, ModelItem.ITEM_PATH_SEPARATOR); 405 String compositeValuePath = StringUtils.substringBeforeLast(valuePath, ModelItem.ITEM_PATH_SEPARATOR); 406 metadataHolder = _getCompositeFromValuePath(contentToEdit, compositeValuePath); 407 } 408 409 if (!metadataHolder.isMultiple(metadataName)) 410 { 411 throw new IllegalArgumentException("The value path " + valuePath + " is invalid because it's not refered to multiple data."); 412 } 413 414 String[] values = metadataHolder.getStringArray(metadataName, new String[0]); 415 if (ArrayUtils.contains(values, valueToRemove)) 416 { 417 if (!isAvailableAction(contentToEdit, actionId)) 418 { 419 throw new InvalidActionException("Invalid workflow action '" + actionId + "' on content '" + contentToEdit.getId() + "': unable to remove value '" + valueToRemove + "' from '" + valuePath + "'"); 420 } 421 422 String[] newValues = ArrayUtils.removeElement(values, valueToRemove); 423 metadataHolder.setMetadata(metadataName, newValues); 424 425 _applyChanges(contentToEdit, actionId, comment); 426 return true; 427 } 428 429 return false; 430 } 431 432 /** 433 * Remove repeater entry from content and apply changes 434 * @param contentToEdit the content to edit 435 * @param entryPath the path of the entry to remove 436 * @param actionId the workflow action id to execute if changes have been made 437 * @param comment the comment of the action 438 * @return true if changes are made 439 * @throws WorkflowException if an error occurred 440 */ 441 public boolean removeRepeaterEntry(WorkflowAwareContent contentToEdit, String entryPath, int actionId, String comment) throws WorkflowException 442 { 443 if (!DataHolderHelper.isRepeaterEntryPath(entryPath)) 444 { 445 throw new IllegalArgumentException("The entry path " + entryPath + " is invalid. It must end by the entry name of the repeater as path[1]"); 446 } 447 448 ModifiableCompositeMetadata metadataHolder = contentToEdit.getMetadataHolder(); 449 String lastEntryName = entryPath; 450 if (StringUtils.contains(entryPath, ModelItem.ITEM_PATH_SEPARATOR)) 451 { 452 lastEntryName = StringUtils.substringAfterLast(entryPath, ModelItem.ITEM_PATH_SEPARATOR); 453 String compositeValuePath = StringUtils.substringBeforeLast(entryPath, ModelItem.ITEM_PATH_SEPARATOR); 454 metadataHolder = _getCompositeFromValuePath(contentToEdit, compositeValuePath); 455 } 456 457 Pair<String, Integer> lastEntryNameAndPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(lastEntryName); 458 459 if (!isAvailableAction(contentToEdit, actionId)) 460 { 461 throw new InvalidActionException("Invalid workflow action '" + actionId + "' on content '" + contentToEdit.getId() + "'. Unable to remove repeater entry " + entryPath); 462 } 463 464 _removeEntry(lastEntryNameAndPosition.getValue(), metadataHolder.getCompositeMetadata(lastEntryNameAndPosition.getKey())); 465 466 _applyChanges(contentToEdit, actionId, comment); 467 return true; 468 } 469 470 /** 471 * Removes the repeater entry at the given position. The position starts at index 1. 472 * The position can be an integer between 1 and the repeater size to remove an entry from the beginning 473 * Or the position can an integer between 0 and - the repeater size to remove an entry from the end (0 means at the end, -1 means before the last one and so on) 474 * @param position The position of the entry to remove 475 * @param repositoryComposite the repository composite of the repeater 476 * @throws IllegalArgumentException if the position is not between the negative and positive repeater size 477 */ 478 protected void _removeEntry(int position, ModifiableCompositeMetadata repositoryComposite) //TODO already exist in the new API 479 { 480 int size = repositoryComposite.getMetadataNames().length; 481 if (1 <= position && position <= size) 482 { 483 // remove the entry 484 repositoryComposite.removeMetadata(String.valueOf(position)); 485 486 // rename all entries after the removed one 487 for (int currentEntryPosition = position + 1; currentEntryPosition <= size; currentEntryPosition++) 488 { 489 ModifiableCompositeMetadata entryRepositoryComposite = repositoryComposite.getCompositeMetadata(String.valueOf(currentEntryPosition)); 490 entryRepositoryComposite.rename(RepositoryConstants.NAMESPACE_PREFIX + ":" + String.valueOf(currentEntryPosition - 1)); 491 } 492 } 493 else if (-size < position && position <= 0) 494 { 495 // Find the positive equivalent position and call the removeEntry method with this position 496 _removeEntry(size + position, repositoryComposite); 497 } 498 else 499 { 500 throw new IllegalArgumentException("Illegal entry position: the entry index '" + position + "' is either negative or exceed the size of the repeater [" + size + "]"); 501 } 502 } 503 504 /** 505 * Get the last composite and the metadata name from a value path. 506 * For example, values[1]/value returns the composite of the first data of values repeater and the metadata name value 507 * @param contentToEdit the content to edit 508 * @param valuePath the value path 509 * @return the pair of composite and metadata name 510 */ 511 protected ModifiableCompositeMetadata _getCompositeFromValuePath(WorkflowAwareContent contentToEdit, String valuePath) 512 { 513 ModifiableCompositeMetadata metadataHolder = contentToEdit.getMetadataHolder(); 514 String[] pathSegments = valuePath.split(ModelItem.ITEM_PATH_SEPARATOR); 515 for (String pathSegment : pathSegments) 516 { 517 Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(pathSegment); 518 if (repeaterNameAndEntryPosition != null) 519 { 520 String repeaterMetadataName = repeaterNameAndEntryPosition.getKey(); 521 String repeaterEntryName = String.valueOf(repeaterNameAndEntryPosition.getValue()); 522 523 metadataHolder = metadataHolder.getCompositeMetadata(repeaterMetadataName).getCompositeMetadata(repeaterEntryName); 524 } 525 else 526 { 527 metadataHolder = metadataHolder.getCompositeMetadata(pathSegment); 528 } 529 } 530 531 return metadataHolder; 532 } 533 534 535 /** 536 * Apply changes on content: create a new version and notify listeners. 537 * @param content the content to apply changes 538 * @param actionId the id of workflow action to execute 539 * @param comment the action comment. Can be null. 540 * @throws WorkflowException if an error occurred 541 */ 542 protected void _applyChanges(WorkflowAwareContent content, int actionId, String comment) throws WorkflowException 543 { 544 // Create a new version 545 if (content instanceof VersionableAmetysObject) 546 { 547 content.saveChanges(); 548 ((VersionableAmetysObject) content).checkpoint(); 549 } 550 551 // Notify listeners 552 Map<String, Object> eventParams = new HashMap<>(); 553 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 554 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 555 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _getUser(), eventParams)); 556 557 Map<String, Object> input = new HashMap<>(); 558 if (StringUtils.isNotEmpty(comment)) 559 { 560 input.put("comment", comment); 561 } 562 563 // Do workflow action 564 doAction(content, actionId, input); 565 } 566}