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.clientsideelement.relations; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.commons.collections.CollectionUtils; 034import org.apache.commons.lang.ArrayUtils; 035import org.apache.commons.lang3.tuple.ImmutablePair; 036import org.apache.commons.lang3.tuple.Pair; 037 038import org.ametys.cms.content.ContentHelper; 039import org.ametys.cms.contenttype.ContentAttributeDefinition; 040import org.ametys.cms.contenttype.ContentType; 041import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 042import org.ametys.cms.contenttype.ContentTypesHelper; 043import org.ametys.cms.data.ContentValue; 044import org.ametys.cms.model.restrictions.RestrictedModelItem; 045import org.ametys.cms.repository.Content; 046import org.ametys.cms.repository.WorkflowAwareContent; 047import org.ametys.cms.workflow.AllErrors; 048import org.ametys.cms.workflow.ContentWorkflowHelper; 049import org.ametys.cms.workflow.EditContentFunction; 050import org.ametys.cms.workflow.InvalidInputWorkflowException; 051import org.ametys.core.ui.Callable; 052import org.ametys.core.ui.StaticClientSideRelation; 053import org.ametys.plugins.repository.AmetysObjectResolver; 054import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 055import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 056import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode; 057import org.ametys.plugins.repository.model.RepeaterDefinition; 058import org.ametys.plugins.workflow.AbstractWorkflowComponent; 059import org.ametys.runtime.i18n.I18nizableTextParameter; 060import org.ametys.runtime.i18n.I18nizableText; 061import org.ametys.runtime.model.ElementDefinition; 062import org.ametys.runtime.model.ModelHelper; 063import org.ametys.runtime.model.ModelItem; 064import org.ametys.runtime.model.ModelItemContainer; 065import org.ametys.runtime.model.exception.UndefinedItemPathException; 066import org.ametys.runtime.parameter.Errors; 067 068/** 069 * Set the attribute of type 'content' of a content, with another content 070 */ 071public class SetContentAttributeClientSideElement extends StaticClientSideRelation implements Component 072{ 073 /** The Ametys object resolver */ 074 protected AmetysObjectResolver _resolver; 075 /** The content type helper */ 076 protected ContentTypesHelper _contentTypesHelper; 077 /** The content types extension point */ 078 protected ContentTypeExtensionPoint _contentTypeExtensionPoint; 079 /** The content workflow helper */ 080 protected ContentWorkflowHelper _contentWorkflowHelper; 081 /** The content helper */ 082 protected ContentHelper _contentHelper; 083 084 @Override 085 public void service(ServiceManager manager) throws ServiceException 086 { 087 super.service(manager); 088 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 089 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 090 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 091 _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 092 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 093 } 094 095 /** 096 * Find an attribute of type content in definition of 'contentIdsToReference' and set the attribute of contents 'contentIdsToEdit' with value 'contentIdsToReference' 097 * @param contentIdsToReference The list of content identifiers that will be added as values in the content field 098 * @param contentIdsToEdit The list of content identifiers to edit and that will have an attribute of type content modified 099 * @return the list of all compatible attribute definitions 100 */ 101 @Callable 102 public List<Map<String, Object>> getCompatibleAttributes(List<String> contentIdsToReference, List<String> contentIdsToEdit) 103 { 104 List<? extends Content> contentsToReference = _resolve(contentIdsToReference); 105 List<? extends Content> contentsToEdit = _resolve(contentIdsToEdit); 106 107 Set<ModelItem> compatibleAtributes = _findCompatibleAttributes(contentsToReference, contentsToEdit); 108 109 return _convert(compatibleAtributes); 110 } 111 112 /** 113 * Convert attribute definitions to JSON object 114 * @param attributeDefinitions The attribute definitions 115 * @return the JSON object 116 */ 117 protected List<Map<String, Object>> _convert(Set<ModelItem> attributeDefinitions) 118 { 119 List<Map<String, Object>> attributeInfo = new ArrayList<>(); 120 121 for (ModelItem attributeDefinition : attributeDefinitions) 122 { 123 attributeInfo.add(_convert(attributeDefinition)); 124 } 125 126 return attributeInfo; 127 } 128 129 /** 130 * Convert an attribute definition to JSON 131 * @param attributeDefinition the attribute definition 132 * @return the JSON object 133 */ 134 protected Map<String, Object> _convert(ModelItem attributeDefinition) 135 { 136 String attributePath = attributeDefinition.getPath(); 137 138 Map<String, Object> definition = new HashMap<>(); 139 definition.put("path", attributePath); 140 definition.put("name", attributeDefinition.getName()); 141 definition.put("label", attributeDefinition.getLabel()); 142 definition.put("description", attributeDefinition.getDescription()); 143 144 if (attributePath.contains(ModelItem.ITEM_PATH_SEPARATOR)) 145 { 146 ModelItem parentMetadatadef = attributeDefinition.getParent(); 147 definition.put("parent", _convert(parentMetadatadef)); 148 } 149 150 return definition; 151 } 152 153 /** 154 * Find the list of compatible attribute definitions 155 * @param contentsToReference the contents to reference 156 * @param contentsToEdit the contents to edit 157 * @return the list of compatible attribute definitions 158 */ 159 protected Set<ModelItem> _findCompatibleAttributes(List<? extends Content> contentsToReference, List<? extends Content> contentsToEdit) 160 { 161 // First we need to find the type of the target attribute we are looking for 162 Collection<String> contentTypesToReference = _getContentTypesIntersection(contentsToReference); 163 164 // Second we need to know if this attribute will be multiple or not 165 boolean requiresMultiple = contentsToReference.size() > 1; 166 167 // Third we need to know the target content type 168 Collection<String> contentTypesToEdit = _getContentTypesIntersection(contentsToEdit); 169 170 // Now lets navigate in the target content types to find an attribute of type content limited to the references content types (or its parent types), that is multiple if necessary 171 Set<ModelItem> compatibleAttributes = new HashSet<>(); 172 173 for (String targetContentTypeId : contentTypesToEdit) 174 { 175 ContentType targetContentType = _contentTypeExtensionPoint.getExtension(targetContentTypeId); 176 for (ModelItem modelItem : targetContentType.getModelItems()) 177 { 178 compatibleAttributes.addAll(_findCompatibleAttributes(contentsToReference, contentsToEdit, targetContentTypeId, modelItem, false, contentTypesToReference, requiresMultiple)); 179 } 180 } 181 182 return compatibleAttributes; 183 } 184 185 private Set<ModelItem> _findCompatibleAttributes(List<? extends Content> contentsToReference, List<? extends Content> contentsToEdit, String targetContentTypeId, ModelItem modelItem, boolean anyParentIsMultiple, Collection<String> compatibleContentTypes, boolean requiresMultiple) 186 { 187 Set<ModelItem> compatibleAttributes = new HashSet<>(); 188 189 if (modelItem instanceof ContentAttributeDefinition) 190 { 191 if (_isAttributeCompatible(contentsToEdit, targetContentTypeId, (ContentAttributeDefinition) modelItem, anyParentIsMultiple, compatibleContentTypes, requiresMultiple)) 192 { 193 compatibleAttributes.add(modelItem); 194 } 195 } 196 else if (modelItem instanceof ModelItemContainer) 197 { 198 for (ModelItem child : ((ModelItemContainer) modelItem).getModelItems()) 199 { 200 compatibleAttributes.addAll(_findCompatibleAttributes(contentsToReference, contentsToEdit, targetContentTypeId, child, anyParentIsMultiple || modelItem instanceof RepeaterDefinition, compatibleContentTypes, requiresMultiple)); 201 } 202 } 203 204 return compatibleAttributes; 205 } 206 207 private boolean _isAttributeCompatible(List<? extends Content> contentsToEdit, String targetContentTypeId, ContentAttributeDefinition attributeDefinition, boolean anyParentIsMultiple, Collection<String> compatibleContentTypes, boolean requiresMultiple) 208 { 209 String contentTypeId = attributeDefinition.getContentTypeId(); 210 211 return (contentTypeId == null || compatibleContentTypes.contains(contentTypeId)) 212 && (!requiresMultiple || attributeDefinition.isMultiple() || anyParentIsMultiple) 213 && attributeDefinition.getModel().getId().equals(targetContentTypeId) 214 && _hasRight(contentsToEdit, attributeDefinition); 215 } 216 217 private boolean _hasRight (List<? extends Content> contentsToEdit, RestrictedModelItem<Content> modelItem) 218 { 219 for (Content content : contentsToEdit) 220 { 221 if (!modelItem.canWrite(content)) 222 { 223 return false; 224 } 225 } 226 return true; 227 } 228 229 /** 230 * Set the attribute at path 'attributePath' of contents 'contentIdsToEdit' with value 'contentIdsToReference' 231 * @param contentIdsToReference The list of content identifiers that will be added as values in the content field 232 * @param contentIdsToEdit The map {key: content identifiers to edit and that will have an attribute of type content modified; value: the new position if attribute is multiple and it is a reorder of values. May be null or equals to -1 if it is not a reorder} 233 * @param contentsToEditToRemove The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove" 234 * @param attributePath The attribute path selected to do modification in the contents to edit 235 * @param workflowActionIds The identifiers of workflow actions to use to edit the attribute. Actions will be tested in this order and first available action will be used 236 * @return A map with key success: true or false. if false, it can be due to errors (list of error messages) 237 */ 238 @Callable 239 public Map<String, Object> setContentAttribute(List<String> contentIdsToReference, Map<String, Integer> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, String attributePath, List<String> workflowActionIds) 240 { 241 return setContentAttribute(contentIdsToReference, contentIdsToEdit, contentsToEditToRemove, attributePath, workflowActionIds, new HashMap<>()); 242 } 243 244 /** 245 * Set the attribute at path 'attributePath' of contents 'contentIdsToEdit' with value 'contentIdsToReference' 246 * @param contentIdsToReference The list of content identifiers that will be added as values in the content field 247 * @param contentIdsToEdit The map {key: content identifiers to edit and that will have an attribute of type content modified; value: the new position if attribute is multiple and it is a reorder of values. May be null or equals to -1 if it is not a reorder} 248 * @param contentsToEditToRemove The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove" 249 * @param attributePath The attribute path selected to do modification in the contents to edit 250 * @param workflowActionIds The identifiers of workflow actions to use to edit the attribute. Actions will be tested in this order and first available action will be used 251 * @param additionalParams the map of additional parameters 252 * @return A map with key success: true or false. if false, it can be due to errors (list of error messages) 253 */ 254 @SuppressWarnings("unchecked") 255 @Callable 256 public Map<String, Object> setContentAttribute(List<String> contentIdsToReference, Map<String, Integer> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, String attributePath, List<String> workflowActionIds, Map<String, Object> additionalParams) 257 { 258 Map<WorkflowAwareContent, Integer> contentsToEdit = (Map<WorkflowAwareContent, Integer>) _resolve(contentIdsToEdit); 259 260 List<String> errorIds = new ArrayList<>(); 261 List<I18nizableText> errorMessages = new ArrayList<>(); 262 263 if (!contentsToEditToRemove.isEmpty()) 264 { 265 // There are some relations to remove, so we consider the content has been moving 266 additionalParams.put("mode", "move"); 267 } 268 269 // We filter contents to edit. If some error occurs, the relation is not cleaned and we return the errors 270 // Default implementation returns the same map of contents to edit with no errors 271 Map<WorkflowAwareContent, Integer> filteredContentsToEdit = _filterContentsToEdit(contentsToEdit, contentIdsToReference, errorMessages, errorIds, additionalParams); 272 if (!errorIds.isEmpty() || !errorMessages.isEmpty()) 273 { 274 // If some error occurs, return the errors 275 return _returnValue(errorMessages, errorIds); 276 } 277 278 // Clean the old relations 279 _clean(contentsToEditToRemove, workflowActionIds, errorMessages, errorIds); 280 if (filteredContentsToEdit.isEmpty() || contentIdsToReference.isEmpty()) 281 { 282 return _returnValue(errorMessages, errorIds); 283 } 284 285 // Get the content types of target contents 286 Collection<String> contentTypeIdsToEdit = _getContentTypesIntersection(filteredContentsToEdit.keySet()); 287 List<ContentType> contentTypesToEdit = contentTypeIdsToEdit.stream() 288 .map(id -> _contentTypeExtensionPoint.getExtension(id)) 289 .collect(Collectors.toList()); 290 291 try 292 { 293 ModelItem attributeDefinition = ModelHelper.getModelItem(attributePath, contentTypesToEdit); 294 // Get the content type holding this attribute 295 ContentType targetContentType = _contentTypeExtensionPoint.getExtension(attributeDefinition.getModel().getId()); 296 _setContentAttribute(contentIdsToReference, filteredContentsToEdit, targetContentType, attributePath, workflowActionIds, errorMessages, errorIds, additionalParams); 297 return _returnValue(errorMessages, errorIds); 298 } 299 catch (UndefinedItemPathException e) 300 { 301 throw new IllegalStateException("Unable to set attribute at path '" + attributePath + "'.", e); 302 } 303 } 304 305 /** 306 * Filter the list of contents to edit 307 * @param contentsToEdit the map of contents to edit 308 * @param contentIdsToReference The list of content ids that will be added as values in the content field 309 * @param errorMessages the error messages 310 * @param errorIds the error content ids 311 * @param additionalParams the map of additional parameters 312 * @return the list of filtered contents 313 */ 314 protected Map<WorkflowAwareContent, Integer> _filterContentsToEdit(Map<WorkflowAwareContent, Integer> contentsToEdit, List<String> contentIdsToReference, List<I18nizableText> errorMessages, List<String> errorIds, Map<String, Object> additionalParams) 315 { 316 // Default implementation 317 return contentsToEdit; 318 } 319 320 private Map<String, Object> _returnValue(List<I18nizableText> errorMessages, List<String> errorIds) 321 { 322 Map<String, Object> returnValues = new HashMap<>(); 323 returnValues.put("success", errorMessages.isEmpty() && errorIds.isEmpty()); 324 if (!errorMessages.isEmpty()) 325 { 326 returnValues.put("errorMessages", errorMessages); 327 } 328 if (!errorIds.isEmpty()) 329 { 330 returnValues.put("errorIds", errorIds); 331 } 332 return returnValues; 333 } 334 335 private void _clean(List<Map<String, String>> contentsToEditToRemove, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds) 336 { 337 for (Map<String, String> removeObject : contentsToEditToRemove) 338 { 339 String contentIdToEdit = removeObject.get("contentId"); 340 String referencingAttributePath = removeObject.get("referencingAttributePath"); 341 String valueToRemove = removeObject.get("valueToRemove"); 342 343 WorkflowAwareContent content = _resolver.resolveById(contentIdToEdit); 344 345 List<ModelItem> items = ModelHelper.getAllModelItemsInPath(referencingAttributePath, content.getModel()); 346 347 Object valuesToRemove; 348 ModelItem attributeDefinition = items.get(items.size() - 1); 349 ContentValue contentValueToRemove = new ContentValue(_resolver, valueToRemove); 350 if (attributeDefinition instanceof ElementDefinition && ((ElementDefinition) attributeDefinition).isMultiple()) 351 { 352 valuesToRemove = new ContentValue[] {contentValueToRemove}; 353 } 354 else 355 { 356 valuesToRemove = contentValueToRemove; 357 } 358 359 SynchronizableValue value = new SynchronizableValue(valuesToRemove); 360 value.setMode(Mode.REMOVE); 361 362 Map<String, Object> values = Map.of(attributeDefinition.getName(), value); 363 for (int i = items.size() - 2; i >= 0; i--) 364 { 365 ModelItem item = items.get(i); 366 if (item instanceof RepeaterDefinition) 367 { 368 throw new IllegalArgumentException("SetContentAttributeClientSideElement does not support a path containing repeater for removing references"); 369 } 370 371 values = Map.of(item.getName(), values); 372 } 373 374 if (getLogger().isDebugEnabled()) 375 { 376 getLogger().debug("Content " + contentIdToEdit + " must be edited at " + referencingAttributePath + " to remove " + valueToRemove); 377 } 378 379 _doAction(content, workflowActionIds, values, errorIds, errorMessages); 380 } 381 } 382 383 384 /** 385 * Set the attribute at path 'attributePath' of contents 'contentsToEdit' with value 'contentIdsToReference' 386 * @param contentIdsToReference The list of content identifiers that will be added as values in the content field 387 * @param contentsToEdit The map {key: contents to edit and that will have an attribute of type content modified; value: the new position if attribute is multiple and it is a reorder of values. May be null or equals to -1 if it is not a reorder} 388 * @param contentType The content type 389 * @param attributePath The attribute path selected to do modification in the contents to edit 390 * @param workflowActionIds The identifiers of workflow actions to use to edit the attribute. Actions will be tested in this order and first available action will be used 391 * @param errorMessages The list that will be felt with error messages of content that had an issue during the operation 392 * @param errorIds The list that will be felt with ids of content that had an issue during the operation 393 * @param additionalParams the map of additional parameters 394 */ 395 protected void _setContentAttribute(List<String> contentIdsToReference, Map<WorkflowAwareContent, Integer> contentsToEdit, ContentType contentType, String attributePath, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds, Map<String, Object> additionalParams) 396 { 397 // On each content 398 for (WorkflowAwareContent content : contentsToEdit.keySet()) 399 { 400 List<ModelItem> items = ModelHelper.getAllModelItemsInPath(attributePath, content.getModel()); 401 int size = items.size(); 402 403 ModelItem attributeDefinition = items.get(size - 1); 404 if (!(attributeDefinition instanceof ContentAttributeDefinition)) 405 { 406 throw new IllegalStateException("No definition of type content found for path '" + attributePath + "' in the content type '" + contentType.getId() + "'."); 407 } 408 409 // find the last repeater in path 410 Pair<RepeaterDefinition, Integer> lastRepeaterDefinitionAndIndex = _getLastRepeaterDefinitionAndIndex(items); 411 RepeaterDefinition lastRepeaterDefinition = lastRepeaterDefinitionAndIndex.getLeft(); 412 int lastRepeaterIndex = lastRepeaterDefinitionAndIndex.getRight(); 413 414 Object value; 415 int lastHandledIndex = size; 416 ModelItem lastHandledItem = attributeDefinition; 417 if (((ContentAttributeDefinition) attributeDefinition).isMultiple()) 418 { 419 Integer newPosition = contentsToEdit.get(content); 420 if (lastRepeaterDefinition != null) 421 { 422 // if there is a repeater we can't merge values, they will be put in a single multiple value 423 value = _getContentValues(contentIdsToReference); 424 } 425 else if (newPosition == null || newPosition < 0) 426 { 427 // Normal case, it is not a move 428 SynchronizableValue syncValue = new SynchronizableValue(_getContentValues(contentIdsToReference)); 429 syncValue.setMode(Mode.APPEND); 430 value = syncValue; 431 } 432 else 433 { 434 // Specific case where there is no new content id to reference, but a reorder in a multiple attribute 435 ContentValue[] contentValues = content.getValue(attributePath); 436 List<String> currentAttributeValue = Arrays.stream(contentValues) 437 .map(ContentValue::getContentId) 438 .collect(Collectors.toList()); 439 List<String> reorderedAttributeValue = _reorder(currentAttributeValue, contentIdsToReference, newPosition); 440 441 value = _getContentValues(reorderedAttributeValue); 442 } 443 } 444 else if (lastRepeaterDefinition != null) 445 { 446 // Special case if there is a repeater in the path and the attribute is single valued. 447 // Create as many repeater entries as there are referenced contents. 448 List<Map<String, Object>> entries = new ArrayList<>(); 449 for (int i = 0; i < contentIdsToReference.size(); i++) 450 { 451 Map<String, Object> currentValue = Map.of(attributeDefinition.getName(), contentIdsToReference.get(i)); 452 for (int j = items.size() - 2; j > lastRepeaterIndex; j--) 453 { 454 ModelItem item = items.get(j); 455 currentValue = Map.of(item.getName(), currentValue); 456 } 457 458 entries.add(currentValue); 459 } 460 461 lastHandledIndex = lastRepeaterIndex + 1; 462 lastHandledItem = lastRepeaterDefinition; 463 value = SynchronizableRepeater.appendOrRemove(entries, Set.of()); 464 } 465 else 466 { 467 value = contentIdsToReference.get(0); 468 } 469 470 Map<String, Object> values = Map.of(lastHandledItem.getName(), value); 471 for (int i = lastHandledIndex - 2; i >= 0; i--) 472 { 473 ModelItem item = items.get(i); 474 if (item instanceof RepeaterDefinition) 475 { 476 values = Map.of(item.getName(), SynchronizableRepeater.appendOrRemove(List.of(values), Set.of())); 477 } 478 else 479 { 480 values = Map.of(item.getName(), values); 481 } 482 } 483 484 // Find the edit action to use 485 _doAction(content, workflowActionIds, values, errorIds, errorMessages); 486 } 487 } 488 489 private Pair<RepeaterDefinition, Integer> _getLastRepeaterDefinitionAndIndex(List<ModelItem> items) 490 { 491 int index = items.size() > 1 ? items.size() - 2 : -1; 492 RepeaterDefinition lastRepeaterDefinition = null; 493 int lastRepeaterIndex = -1; 494 while (index >= 0 && lastRepeaterDefinition == null) 495 { 496 ModelItem item = items.get(index); 497 if (item instanceof RepeaterDefinition) 498 { 499 lastRepeaterDefinition = (RepeaterDefinition) item; 500 lastRepeaterIndex = index; 501 } 502 503 index--; 504 } 505 506 return new ImmutablePair<>(lastRepeaterDefinition, lastRepeaterIndex); 507 } 508 509 private ContentValue[] _getContentValues(List<String> ids) 510 { 511 return ids.stream().map(s -> new ContentValue(_resolver, s)).toArray(ContentValue[]::new); 512 } 513 514 private List<String> _reorder(List<String> currentElements, List<String> elementsToReorder, int newPosition) 515 { 516 List<String> reorderedList = new ArrayList<>(currentElements); 517 518 // 1/ in currentElements, replace the ones to reorder by null, in order to keep all indexes 519 for (int i = 0; i < currentElements.size(); i++) 520 { 521 String element = currentElements.get(i); 522 if (elementsToReorder.contains(element)) 523 { 524 reorderedList.set(i, null); 525 } 526 } 527 528 // 2/ insert the elements to reorder at the new position 529 reorderedList.addAll(newPosition, elementsToReorder); 530 531 // 3/ remove null elements, corresponding to the old positions of the elements that were reordered 532 reorderedList.removeIf(Objects::isNull); 533 534 return reorderedList; 535 } 536 537 private void _doAction(WorkflowAwareContent content, List<String> workflowActionIds, Map<String, Object> values, List<String> errorIds, List<I18nizableText> errorMessages) 538 { 539 Integer actionId = null; 540 541 int[] actionIds = _contentWorkflowHelper.getAvailableActions(content); 542 for (String workflowActionIdToTryAsString : workflowActionIds) 543 { 544 Integer workflowActionIdToTry = Integer.parseInt(workflowActionIdToTryAsString); 545 if (ArrayUtils.contains(actionIds, workflowActionIdToTry)) 546 { 547 actionId = workflowActionIdToTry; 548 break; 549 } 550 } 551 552 if (actionId == null) 553 { 554 List<String> parameters = new ArrayList<>(); 555 parameters.add(_contentHelper.getTitle(content)); 556 parameters.add(content.getName()); 557 parameters.add(content.getId()); 558 errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_WORKFLOW", parameters)); 559 errorIds.add(content.getId()); 560 } 561 else 562 { 563 // edit 564 Map<String, Object> contextParameters = new HashMap<>(); 565 contextParameters.put(EditContentFunction.QUIT, true); 566 contextParameters.put(EditContentFunction.VALUES_KEY, values); 567 568 Map<String, Object> inputs = new HashMap<>(); 569 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 570 571 try 572 { 573 _contentWorkflowHelper.doAction(content, actionId, inputs); 574 } 575 catch (Exception e) 576 { 577 getLogger().error("Content '" + _contentHelper.getTitle(content) + "' (" + content.getName() + "/" + content.getId() + ") was not modified", e); 578 579 Map<String, I18nizableTextParameter> parameters = new HashMap<>(); 580 parameters.put("0", new I18nizableText(_contentHelper.getTitle(content))); 581 parameters.put("1", new I18nizableText(content.getName())); 582 parameters.put("2", new I18nizableText(content.getId())); 583 584 if (e instanceof InvalidInputWorkflowException) 585 { 586 I18nizableText rootError = null; 587 588 AllErrors allErrors = ((InvalidInputWorkflowException) e).getErrors(); 589 Map<String, Errors> allErrorsMap = allErrors.getAllErrors(); 590 for (String errorMetadataPath : allErrorsMap.keySet()) 591 { 592 Errors errors = allErrorsMap.get(errorMetadataPath); 593 594 I18nizableText insideError = null; 595 596 List<I18nizableText> errorsAsList = errors.getErrors(); 597 for (I18nizableText error : errorsAsList) 598 { 599 Map<String, I18nizableTextParameter> i18nparameters = new HashMap<>(); 600 i18nparameters.put("0", error); 601 602 I18nizableText localError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_VALIDATION_ATTRIBUTE_CHAIN", i18nparameters); 603 604 if (insideError == null) 605 { 606 insideError = localError; 607 } 608 else 609 { 610 insideError.getParameterMap().put("1", localError); 611 } 612 } 613 614 Map<String, I18nizableTextParameter> i18ngeneralparameters = new HashMap<>(); 615 616 String i18ngeneralkey = null; 617 if (EditContentFunction.GLOBAL_ERROR_KEY.equals(errorMetadataPath)) 618 { 619 i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_GLOBAL_VALIDATION"; 620 i18ngeneralparameters.put("1", insideError); 621 } 622 else 623 { 624 i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_VALIDATION_ATTRIBUTE"; 625 i18ngeneralparameters.put("0", new I18nizableText(errorMetadataPath)); 626 i18ngeneralparameters.put("1", insideError); 627 } 628 629 I18nizableText generalError = new I18nizableText("plugin.cms", i18ngeneralkey, i18ngeneralparameters); 630 if (rootError == null) 631 { 632 rootError = generalError; 633 } 634 else 635 { 636 rootError.getParameterMap().put("2", generalError); 637 } 638 } 639 640 parameters.put("3", rootError); 641 } 642 else 643 { 644 if (e.getMessage() != null) 645 { 646 parameters.put("3", new I18nizableText(e.getMessage())); 647 } 648 else 649 { 650 parameters.put("3", new I18nizableText(e.getClass().getName())); 651 } 652 } 653 errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_EDIT", parameters)); 654 errorIds.add(content.getId()); 655 } 656 } 657 } 658 659 /** 660 * Resolve content by their identifiers 661 * @param contentIds The id of contents to resolve 662 * @return the contents 663 */ 664 protected List<? extends Content> _resolve(List<String> contentIds) 665 { 666 List<Content> contents = new ArrayList<>(); 667 668 for (String contentId: contentIds) 669 { 670 Content content = _resolver.resolveById(contentId); 671 contents.add(content); 672 } 673 674 return contents; 675 } 676 677 /** 678 * Resolve content by their identifiers 679 * @param contentIds The id of contents to resolve 680 * @return the contents 681 */ 682 protected Map<? extends Content, Integer> _resolve(Map<String, Integer> contentIds) 683 { 684 Map<Content, Integer> contents = new LinkedHashMap<>(); 685 686 for (Map.Entry<String, Integer> entry: contentIds.entrySet()) 687 { 688 Content content = _resolver.resolveById(entry.getKey()); 689 contents.put(content, entry.getValue()); 690 } 691 692 return contents; 693 } 694 695 private Collection<String> _getContentTypesIntersection(Collection<? extends Content> contents) 696 { 697 Collection<String> contentTypes = new ArrayList<>(); 698 for (Content content: contents) 699 { 700 Set<String> ancestorsAndMySelf = new HashSet<>(); 701 702 String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes()); 703 for (String id : allContentTypes) 704 { 705 ancestorsAndMySelf.addAll(_contentTypesHelper.getAncestors(id)); 706 ancestorsAndMySelf.add(id); 707 } 708 709 if (contentTypes.isEmpty()) 710 { 711 contentTypes = ancestorsAndMySelf; 712 } 713 else 714 { 715 contentTypes = CollectionUtils.intersection(contentTypes, ancestorsAndMySelf); 716 } 717 } 718 return contentTypes; 719 } 720}