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; 035 036import org.ametys.cms.content.ContentHelper; 037import org.ametys.cms.contenttype.ContentAttributeDefinition; 038import org.ametys.cms.contenttype.ContentType; 039import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 040import org.ametys.cms.contenttype.ContentTypesHelper; 041import org.ametys.cms.data.ContentValue; 042import org.ametys.cms.model.restrictions.RestrictedModelItem; 043import org.ametys.cms.repository.Content; 044import org.ametys.cms.repository.WorkflowAwareContent; 045import org.ametys.cms.workflow.AllErrors; 046import org.ametys.cms.workflow.ContentWorkflowHelper; 047import org.ametys.cms.workflow.EditContentFunction; 048import org.ametys.cms.workflow.InvalidInputWorkflowException; 049import org.ametys.core.ui.Callable; 050import org.ametys.core.ui.StaticClientSideRelation; 051import org.ametys.plugins.repository.AmetysObjectResolver; 052import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater; 053import org.ametys.plugins.repository.data.holder.values.SynchronizableValue; 054import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode; 055import org.ametys.plugins.repository.model.RepeaterDefinition; 056import org.ametys.plugins.workflow.AbstractWorkflowComponent; 057import org.ametys.runtime.i18n.I18nizableTextParameter; 058import org.ametys.runtime.i18n.I18nizableText; 059import org.ametys.runtime.model.ElementDefinition; 060import org.ametys.runtime.model.ModelHelper; 061import org.ametys.runtime.model.ModelItem; 062import org.ametys.runtime.model.ModelItemContainer; 063import org.ametys.runtime.model.exception.UndefinedItemPathException; 064import org.ametys.runtime.parameter.Errors; 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 'contentIdsToEdit' 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 int index = size > 1 ? size - 2 : -1; 409 RepeaterDefinition lastRepeaterDefinition = null; 410 int lastRepeaterIndex = -1; 411 while (index >= 0 && lastRepeaterDefinition == null) 412 { 413 ModelItem item = items.get(index); 414 if (item instanceof RepeaterDefinition) 415 { 416 lastRepeaterDefinition = (RepeaterDefinition) item; 417 lastRepeaterIndex = index; 418 } 419 420 index--; 421 } 422 423 Object value; 424 int lastHandledIndex = size; 425 ModelItem lastHandledItem = attributeDefinition; 426 if (((ContentAttributeDefinition) attributeDefinition).isMultiple()) 427 { 428 Integer newPosition = contentsToEdit.get(content); 429 if (lastRepeaterDefinition != null) 430 { 431 // if there is a repeater we can't merge values, they will be put in a single multiple value 432 value = _getContentValues(contentIdsToReference); 433 } 434 else if (newPosition == null || newPosition < 0) 435 { 436 // Normal case, it is not a move 437 SynchronizableValue syncValue = new SynchronizableValue(_getContentValues(contentIdsToReference)); 438 syncValue.setMode(Mode.APPEND); 439 value = syncValue; 440 } 441 else 442 { 443 // Specific case where there is no new content id to reference, but a reorder in a multiple attribute 444 ContentValue[] contentValues = content.getValue(attributePath); 445 List<String> currentAttributeValue = Arrays.stream(contentValues) 446 .map(ContentValue::getContentId) 447 .collect(Collectors.toList()); 448 List<String> reorderedAttributeValue = _reorder(currentAttributeValue, contentIdsToReference, newPosition); 449 450 value = _getContentValues(reorderedAttributeValue); 451 } 452 } 453 else if (lastRepeaterDefinition != null) 454 { 455 // Special case if there is a repeater in the path and the attribute is single valued. 456 // Create as many repeater entries as there are referenced contents. 457 List<Map<String, Object>> entries = new ArrayList<>(); 458 for (int i = 0; i < contentIdsToReference.size(); i++) 459 { 460 Map<String, Object> currentValue = Map.of(attributeDefinition.getName(), contentIdsToReference.get(i)); 461 for (int j = items.size() - 2; j > lastRepeaterIndex; j--) 462 { 463 ModelItem item = items.get(j); 464 currentValue = Map.of(item.getName(), currentValue); 465 } 466 467 entries.add(currentValue); 468 } 469 470 lastHandledIndex = lastRepeaterIndex + 1; 471 lastHandledItem = lastRepeaterDefinition; 472 value = SynchronizableRepeater.appendOrRemove(entries, Set.of()); 473 } 474 else 475 { 476 value = contentIdsToReference.get(0); 477 } 478 479 Map<String, Object> values = Map.of(lastHandledItem.getName(), value); 480 for (int i = lastHandledIndex - 2; i >= 0; i--) 481 { 482 ModelItem item = items.get(i); 483 if (item instanceof RepeaterDefinition) 484 { 485 values = Map.of(item.getName(), SynchronizableRepeater.appendOrRemove(List.of(values), Set.of())); 486 } 487 else 488 { 489 values = Map.of(item.getName(), values); 490 } 491 } 492 493 // Find the edit action to use 494 _doAction(content, workflowActionIds, values, errorIds, errorMessages); 495 } 496 } 497 498 private ContentValue[] _getContentValues(List<String> ids) 499 { 500 return ids.stream().map(s -> new ContentValue(_resolver, s)).toArray(ContentValue[]::new); 501 } 502 503 private List<String> _reorder(List<String> currentElements, List<String> elementsToReorder, int newPosition) 504 { 505 List<String> reorderedList = new ArrayList<>(currentElements); 506 507 // 1/ in currentElements, replace the ones to reorder by null, in order to keep all indexes 508 for (int i = 0; i < currentElements.size(); i++) 509 { 510 String element = currentElements.get(i); 511 if (elementsToReorder.contains(element)) 512 { 513 reorderedList.set(i, null); 514 } 515 } 516 517 // 2/ insert the elements to reorder at the new position 518 reorderedList.addAll(newPosition, elementsToReorder); 519 520 // 3/ remove null elements, corresponding to the old positions of the elements that were reordered 521 reorderedList.removeIf(Objects::isNull); 522 523 return reorderedList; 524 } 525 526 private void _doAction(WorkflowAwareContent content, List<String> workflowActionIds, Map<String, Object> values, List<String> errorIds, List<I18nizableText> errorMessages) 527 { 528 Integer actionId = null; 529 530 int[] actionIds = _contentWorkflowHelper.getAvailableActions(content); 531 for (String workflowActionIdToTryAsString : workflowActionIds) 532 { 533 Integer workflowActionIdToTry = Integer.parseInt(workflowActionIdToTryAsString); 534 if (ArrayUtils.contains(actionIds, workflowActionIdToTry)) 535 { 536 actionId = workflowActionIdToTry; 537 break; 538 } 539 } 540 541 if (actionId == null) 542 { 543 List<String> parameters = new ArrayList<>(); 544 parameters.add(_contentHelper.getTitle(content)); 545 parameters.add(content.getName()); 546 parameters.add(content.getId()); 547 errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_WORKFLOW", parameters)); 548 errorIds.add(content.getId()); 549 } 550 else 551 { 552 // edit 553 Map<String, Object> contextParameters = new HashMap<>(); 554 contextParameters.put(EditContentFunction.QUIT, true); 555 contextParameters.put(EditContentFunction.VALUES_KEY, values); 556 557 Map<String, Object> inputs = new HashMap<>(); 558 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 559 560 try 561 { 562 _contentWorkflowHelper.doAction(content, actionId, inputs); 563 } 564 catch (Exception e) 565 { 566 getLogger().error("Content '" + _contentHelper.getTitle(content) + "' (" + content.getName() + "/" + content.getId() + ") was not modified", e); 567 568 Map<String, I18nizableTextParameter> parameters = new HashMap<>(); 569 parameters.put("0", new I18nizableText(_contentHelper.getTitle(content))); 570 parameters.put("1", new I18nizableText(content.getName())); 571 parameters.put("2", new I18nizableText(content.getId())); 572 573 if (e instanceof InvalidInputWorkflowException) 574 { 575 I18nizableText rootError = null; 576 577 AllErrors allErrors = ((InvalidInputWorkflowException) e).getErrors(); 578 Map<String, Errors> allErrorsMap = allErrors.getAllErrors(); 579 for (String errorMetadataPath : allErrorsMap.keySet()) 580 { 581 Errors errors = allErrorsMap.get(errorMetadataPath); 582 583 I18nizableText insideError = null; 584 585 List<I18nizableText> errorsAsList = errors.getErrors(); 586 for (I18nizableText error : errorsAsList) 587 { 588 Map<String, I18nizableTextParameter> i18nparameters = new HashMap<>(); 589 i18nparameters.put("0", error); 590 591 I18nizableText localError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_VALIDATION_ATTRIBUTE_CHAIN", i18nparameters); 592 593 if (insideError == null) 594 { 595 insideError = localError; 596 } 597 else 598 { 599 insideError.getParameterMap().put("1", localError); 600 } 601 } 602 603 Map<String, I18nizableTextParameter> i18ngeneralparameters = new HashMap<>(); 604 605 String i18ngeneralkey = null; 606 if (EditContentFunction.GLOBAL_ERROR_KEY.equals(errorMetadataPath)) 607 { 608 i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_GLOBAL_VALIDATION"; 609 i18ngeneralparameters.put("1", insideError); 610 } 611 else 612 { 613 i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_VALIDATION_ATTRIBUTE"; 614 i18ngeneralparameters.put("0", new I18nizableText(errorMetadataPath)); 615 i18ngeneralparameters.put("1", insideError); 616 } 617 618 I18nizableText generalError = new I18nizableText("plugin.cms", i18ngeneralkey, i18ngeneralparameters); 619 if (rootError == null) 620 { 621 rootError = generalError; 622 } 623 else 624 { 625 rootError.getParameterMap().put("2", generalError); 626 } 627 } 628 629 parameters.put("3", rootError); 630 } 631 else 632 { 633 if (e.getMessage() != null) 634 { 635 parameters.put("3", new I18nizableText(e.getMessage())); 636 } 637 else 638 { 639 parameters.put("3", new I18nizableText(e.getClass().getName())); 640 } 641 } 642 errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_EDIT", parameters)); 643 errorIds.add(content.getId()); 644 } 645 } 646 } 647 648 /** 649 * Resolve content by their identifiers 650 * @param contentIds The id of contents to resolve 651 * @return the contents 652 */ 653 protected List<? extends Content> _resolve(List<String> contentIds) 654 { 655 List<Content> contents = new ArrayList<>(); 656 657 for (String contentId: contentIds) 658 { 659 Content content = _resolver.resolveById(contentId); 660 contents.add(content); 661 } 662 663 return contents; 664 } 665 666 /** 667 * Resolve content by their identifiers 668 * @param contentIds The id of contents to resolve 669 * @return the contents 670 */ 671 protected Map<? extends Content, Integer> _resolve(Map<String, Integer> contentIds) 672 { 673 Map<Content, Integer> contents = new LinkedHashMap<>(); 674 675 for (Map.Entry<String, Integer> entry: contentIds.entrySet()) 676 { 677 Content content = _resolver.resolveById(entry.getKey()); 678 contents.put(content, entry.getValue()); 679 } 680 681 return contents; 682 } 683 684 private Collection<String> _getContentTypesIntersection(Collection<? extends Content> contents) 685 { 686 Collection<String> contentTypes = new ArrayList<>(); 687 for (Content content: contents) 688 { 689 Set<String> ancestorsAndMySelf = new HashSet<>(); 690 691 String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes()); 692 for (String id : allContentTypes) 693 { 694 ancestorsAndMySelf.addAll(_contentTypesHelper.getAncestors(id)); 695 ancestorsAndMySelf.add(id); 696 } 697 698 if (contentTypes.isEmpty()) 699 { 700 contentTypes = ancestorsAndMySelf; 701 } 702 else 703 { 704 contentTypes = CollectionUtils.intersection(contentTypes, ancestorsAndMySelf); 705 } 706 } 707 return contentTypes; 708 } 709}