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