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