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.List; 024import java.util.Map; 025import java.util.Set; 026 027import org.apache.avalon.framework.component.Component; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.commons.collections.CollectionUtils; 031import org.apache.commons.lang.ArrayUtils; 032import org.apache.commons.lang3.StringUtils; 033 034import org.ametys.cms.contenttype.ContentConstants; 035import org.ametys.cms.contenttype.ContentType; 036import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 037import org.ametys.cms.contenttype.ContentTypesHelper; 038import org.ametys.cms.contenttype.MetadataDefinition; 039import org.ametys.cms.contenttype.MetadataType; 040import org.ametys.cms.contenttype.RepeaterDefinition; 041import org.ametys.cms.form.AbstractField.MODE; 042import org.ametys.cms.repository.Content; 043import org.ametys.cms.repository.WorkflowAwareContent; 044import org.ametys.cms.workflow.AllErrors; 045import org.ametys.cms.workflow.ContentWorkflowHelper; 046import org.ametys.cms.workflow.EditContentFunction; 047import org.ametys.cms.workflow.InvalidInputWorkflowException; 048import org.ametys.core.ui.Callable; 049import org.ametys.core.ui.StaticClientSideRelation; 050import org.ametys.plugins.repository.AmetysObjectResolver; 051import org.ametys.plugins.workflow.AbstractWorkflowComponent; 052import org.ametys.runtime.i18n.I18nizableText; 053import org.ametys.runtime.parameter.Errors; 054 055/** 056 * Set the metadata of type 'content' of a content, with another content 057 */ 058public class SetContentMetadataClientSideElement extends StaticClientSideRelation implements Component 059{ 060 /** The Ametys object resolver */ 061 protected AmetysObjectResolver _resolver; 062 /** The content type helper */ 063 protected ContentTypesHelper _ctypesHelper; 064 /** The content types extension point */ 065 protected ContentTypeExtensionPoint _ctypesEP; 066 /** The content workflow helper */ 067 protected ContentWorkflowHelper _contentWorkflowHelper; 068 /** The EP to filter meta */ 069 protected FilterCompatibleContentMetadataExtensionPoint _filterCompatibleContentMetadataExtensionPoint; 070 071 @Override 072 public void service(ServiceManager manager) throws ServiceException 073 { 074 super.service(manager); 075 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 076 _ctypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 077 _ctypesEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 078 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 079 _filterCompatibleContentMetadataExtensionPoint = (FilterCompatibleContentMetadataExtensionPoint) manager.lookup(FilterCompatibleContentMetadataExtensionPoint.ROLE); 080 } 081 082 /** 083 * Find a metadata of type content in definition of 'contentIdsToReference' and set the metadata of contents 'contentIdsToEdit' with value 'contentIdsToReference' 084 * @param contentIdsToReference The list of content ids that will be added as values in the content field 085 * @param contentIdsToEdit The list of content ids to edit and that will have a metadata of type content modified 086 * @return A map with key success: true or false. if false, it can be due to errors (list of error messages) or because metadata cannot be determined then 'metadata' is a list of possible metadata 087 */ 088 @Callable 089 public Map<String, Object> getCompatibleMetadata(List<String> contentIdsToReference, List<String> contentIdsToEdit) 090 { 091 @SuppressWarnings("unchecked") 092 List<Content> contentToReference = (List<Content>) _resolve(contentIdsToReference); 093 @SuppressWarnings("unchecked") 094 List<Content> contentToEdit = (List<Content>) _resolve(contentIdsToEdit); 095 096 Set<MetadataDefinition> metadatadefs = _findCompatibleMetadata(contentToReference, contentToEdit); 097 098 Map<String, Object> returnValues = new HashMap<>(); 099 returnValues.put("success", false); 100 returnValues.put("metadata", _convert(metadatadefs)); 101 return returnValues; 102 } 103 104 /** 105 * Convert metadata definitions to JSON object 106 * @param metadatadefs The metadata definitions 107 * @return the JSON object 108 */ 109 protected List<Map<String, Object>> _convert(Set<MetadataDefinition> metadatadefs) 110 { 111 List<Map<String, Object>> metadatapaths = new ArrayList<>(); 112 113 for (MetadataDefinition metadataDef : metadatadefs) 114 { 115 metadatapaths.add(_convert(metadataDef)); 116 } 117 118 return metadatapaths; 119 } 120 121 /** 122 * Convert a metadata definition to JSON 123 * @param metadatadef the metadata definition 124 * @return the JSON object 125 */ 126 protected Map<String, Object> _convert(MetadataDefinition metadatadef) 127 { 128 String metaPath = metadatadef.getId(); 129 130 Map<String, Object> def = new HashMap<>(); 131 def.put("id", metaPath); 132 def.put("name", metadatadef.getName()); 133 def.put("label", metadatadef.getLabel()); 134 def.put("description", metadatadef.getDescription()); 135 136 if (metaPath.contains(ContentConstants.METADATA_PATH_SEPARATOR)) 137 { 138 String parentPath = StringUtils.substringBeforeLast(metaPath, ContentConstants.METADATA_PATH_SEPARATOR); 139 140 String cTypeId = metadatadef.getReferenceContentType(); 141 ContentType cType = _ctypesEP.getExtension(cTypeId); 142 143 MetadataDefinition parentMetadatadef = cType.getMetadataDefinitionByPath(parentPath); 144 def.put("parent", _convert(parentMetadatadef)); 145 } 146 147 return def; 148 } 149 150 /** 151 * Find the list of compatible metadata definition 152 * @param contentToReference the contents to reference 153 * @param contentToEdit the contents to edit 154 * @return the list of compatible metadata definition 155 */ 156 protected Set<MetadataDefinition> _findCompatibleMetadata(List<Content> contentToReference, List<Content> contentToEdit) 157 { 158 // First we need to find the type of the target metadata we are looking for 159 Collection<String> contentTypesToReference = new HashSet<>(); 160 for (Content content: contentToReference) 161 { 162 Set<String> ancestorsAndMySelf = new HashSet<>(); 163 164 String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes()); 165 for (String id : allContentTypes) 166 { 167 ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id)); 168 ancestorsAndMySelf.add(id); 169 } 170 171 if (contentTypesToReference.isEmpty()) 172 { 173 contentTypesToReference = ancestorsAndMySelf; 174 } 175 else 176 { 177 contentTypesToReference = CollectionUtils.intersection(contentTypesToReference, ancestorsAndMySelf); 178 } 179 } 180 181 // Second we need to know if this metadata will be mulitple or not 182 boolean requiresMultiple = contentToReference.size() > 1; 183 184 // Third we need to know the target content type 185 Collection<String> contentTypesToEdit = new ArrayList<>(); 186 for (Content content: contentToEdit) 187 { 188 Set<String> ancestorsAndMySelf = new HashSet<>(); 189 190 String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes()); 191 for (String id : allContentTypes) 192 { 193 ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id)); 194 ancestorsAndMySelf.add(id); 195 } 196 197 if (contentTypesToEdit.isEmpty()) 198 { 199 contentTypesToEdit = ancestorsAndMySelf; 200 } 201 else 202 { 203 contentTypesToEdit = CollectionUtils.intersection(contentTypesToEdit, ancestorsAndMySelf); 204 } 205 } 206 207 // Now lets navigate in the target content type to find a content metadata limited to the metadata content type (or its parent types), that is multiple if necessary 208 Set<MetadataDefinition> metadata = new HashSet<>(); 209 210 for (String targetContentTypeName : contentTypesToEdit) 211 { 212 ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName); 213 for (String metadataName : targetContentType.getMetadataNames()) 214 { 215 MetadataDefinition metaDef = targetContentType.getMetadataDefinition(metadataName); 216 metadata.addAll(_findCompatibleMetadata(contentToReference, contentToEdit, targetContentTypeName, metaDef, false, contentTypesToReference, requiresMultiple)); 217 } 218 } 219 220 return metadata; 221 } 222 223 private Set<MetadataDefinition> _findCompatibleMetadata(List<Content> contentToReference, List<Content> contentToEdit, String targetContentTypeName, MetadataDefinition metaDef, boolean anyParentIsMultiple, Collection<String> compatibleContentTypes, boolean requiresMultiple) 224 { 225 Set<MetadataDefinition> metadata = new HashSet<>(); 226 227 if (MetadataType.CONTENT.equals(metaDef.getType()) 228 && (metaDef.getContentType() == null || compatibleContentTypes.contains(metaDef.getContentType())) 229 && (!requiresMultiple || metaDef.isMultiple() || anyParentIsMultiple) 230 && metaDef.getReferenceContentType().equals(targetContentTypeName) 231 && _filterCompatibleContentMetadataExtensionPoint.filter(targetContentTypeName, metaDef, contentToReference, compatibleContentTypes) 232 && _hasRight(contentToEdit, metaDef)) 233 { 234 metadata.add(metaDef); 235 } 236 else if (MetadataType.COMPOSITE.equals(metaDef.getType())) 237 { 238 for (String subMetadataName : metaDef.getMetadataNames()) 239 { 240 MetadataDefinition subMetaDef = metaDef.getMetadataDefinition(subMetadataName); 241 metadata.addAll(_findCompatibleMetadata(contentToReference, contentToEdit, targetContentTypeName, subMetaDef, anyParentIsMultiple || metaDef instanceof RepeaterDefinition, compatibleContentTypes, requiresMultiple)); 242 } 243 } 244 245 return metadata; 246 } 247 248 private boolean _hasRight (List<Content> contentToEdit, MetadataDefinition metaDef) 249 { 250 for (Content content : contentToEdit) 251 { 252 if (!_ctypesHelper.canWrite(content, metaDef)) 253 { 254 return false; 255 } 256 } 257 return true; 258 } 259 260 /** 261 * Set the metadata 'metadatapath' of contents 'contentIdsToEdit' with value 'contentIdsToReference' 262 * @param contentIdsToReference The list of content ids that will be added as values in the content field 263 * @param contentIdsToEdit The list of content ids to edit and that will have a metadata of type content modified 264 * @param contentsToEditToRemove The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove" 265 * @param metadatapath The metadata path selected to do modification in the contentIdsToEdit contents 266 * @param workflowActionIds The ids of workflow actions to use to edit the metadata. Actions will be tested in this order and first available action will be used 267 * @return A map with key success: true or false. if false, it can be due to errors (list of error messages) 268 */ 269 @SuppressWarnings("unchecked") 270 @Callable 271 public Map<String, Object> setContentMetatada(List<String> contentIdsToReference, List<String> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, String metadatapath, List<String> workflowActionIds) 272 { 273 List<WorkflowAwareContent> contentToEdit = (List<WorkflowAwareContent>) _resolve(contentIdsToEdit); 274 275 List<String> errorIds = new ArrayList<>(); 276 List<I18nizableText> errorMessages = new ArrayList<>(); 277 278 _clean(contentsToEditToRemove, workflowActionIds, errorMessages, errorIds); 279 280 if (contentIdsToEdit.isEmpty() || contentIdsToReference.isEmpty()) 281 { 282 return _returnValue(errorMessages, errorIds); 283 } 284 285 Collection<String> contentTypesToEdit = new ArrayList<>(); 286 for (Content content: contentToEdit) 287 { 288 Set<String> ancestorsAndMySelf = new HashSet<>(); 289 290 String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes()); 291 for (String id : allContentTypes) 292 { 293 ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id)); 294 ancestorsAndMySelf.add(id); 295 } 296 297 if (contentTypesToEdit.isEmpty()) 298 { 299 contentTypesToEdit = ancestorsAndMySelf; 300 } 301 else 302 { 303 contentTypesToEdit = CollectionUtils.intersection(contentTypesToEdit, ancestorsAndMySelf); 304 } 305 } 306 307 for (String targetContentTypeName : contentTypesToEdit) 308 { 309 ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName); 310 MetadataDefinition metadataDef = targetContentType.getMetadataDefinitionByPath(metadatapath); 311 if (metadataDef != null) 312 { 313 _setContentMetatada(contentIdsToReference, contentToEdit, targetContentType, metadatapath, workflowActionIds, errorMessages, errorIds); 314 315 return _returnValue(errorMessages, errorIds); 316 } 317 } 318 319 throw new IllegalStateException("Unable to find medatata definition to path '" + metadatapath + "'."); 320 } 321 322 private Map<String, Object> _returnValue(List<I18nizableText> errorMessages, List<String> errorIds) 323 { 324 Map<String, Object> returnValues = new HashMap<>(); 325 returnValues.put("success", errorMessages.isEmpty() && errorIds.isEmpty()); 326 if (!errorMessages.isEmpty()) 327 { 328 returnValues.put("errorMessages", errorMessages); 329 } 330 if (!errorIds.isEmpty()) 331 { 332 returnValues.put("errorIds", errorIds); 333 } 334 return returnValues; 335 } 336 337 private void _clean(List<Map<String, String>> contentsToEditToRemove, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds) 338 { 339 for (Map<String, String> removeObject : contentsToEditToRemove) 340 { 341 String contentIdToEdit = removeObject.get("contentId"); 342 String referencingMetadataPath = removeObject.get("referencingMetadataPath"); 343 String valueToRemove = removeObject.get("valueToRemove"); 344 345 WorkflowAwareContent content = _resolver.resolveById(contentIdToEdit); 346 347 Map<String, Object> values = new HashMap<>(); 348 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + referencingMetadataPath + ".mode", MODE.REMOVE.name()); 349 350 MetadataDefinition metadataDef = _ctypesHelper.getMetadataDefinitionByMetadataValuePath(referencingMetadataPath, content); 351 if (metadataDef.isMultiple()) 352 { 353 values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + referencingMetadataPath, Arrays.asList(valueToRemove)); 354 } 355 else 356 { 357 values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + referencingMetadataPath, valueToRemove); 358 } 359 360 if (getLogger().isDebugEnabled()) 361 { 362 getLogger().debug("Content " + contentIdToEdit + " must be edited at " + referencingMetadataPath + " to remove " + valueToRemove); 363 } 364 365 _doAction(content, workflowActionIds, values, errorIds, errorMessages); 366 } 367 } 368 369 370 /** 371 * Set the metadata 'metadatapath' of contents 'contentIdsToEdit' with value 'contentIdsToReference' 372 * @param contentIdsToReference The list of content ids that will be added as values in the content field 373 * @param contentToEdit The list of content to edit and that will have a metadata of type content modified 374 * @param contentType The content type 375 * @param metadataPath The metadata selected to do modification in the contentIdsToEdit contents 376 * @param workflowActionIds The ids of workflow actions to use to edit the metadata. Actions will be tested in this order and first available action will be used 377 * @param errorMessages The list that will be felt with error messages of content that had an issue during the operation 378 * @param errorIds The list that will be felt with ids of content that had an issue during the operation 379 */ 380 protected void _setContentMetatada(List<String> contentIdsToReference, List<WorkflowAwareContent> contentToEdit, ContentType contentType, String metadataPath, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds) 381 { 382 // On each content 383 for (WorkflowAwareContent content : contentToEdit) 384 { 385 Map<String, Object> values = new HashMap<>(); 386 387 String metadataName = ""; 388 String[] metadataDefPath = StringUtils.split(metadataPath, '/'); 389 390 // Find repeaters in the path 391 String lastRepeaterPath = null; 392 MetadataDefinition metadataDef = null; 393 for (String element : metadataDefPath) 394 { 395 metadataDef = metadataDef == null ? contentType.getMetadataDefinition(element) : metadataDef.getMetadataDefinition(element); 396 metadataName += ("".equals(metadataName) ? "" : ".") + metadataDef.getName(); 397 398 if (metadataDef instanceof RepeaterDefinition) 399 { 400 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".size", "1"); 401 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name()); 402 lastRepeaterPath = metadataName; 403 metadataName += ".1"; 404 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".position", "0"); // 0 means at the end 405 } 406 } 407 408 if (metadataDef == null) 409 { 410 throw new IllegalStateException("Definition cannot be null"); 411 } 412 413 // The value to set 414 if (metadataDef.isMultiple()) 415 { 416 values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, contentIdsToReference); 417 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name()); 418 } 419 else if (lastRepeaterPath != null) // Special case in there is a repeater in the path and the metadata is single valued. 420 { 421 // create as many repeater entries as there is referenced contents. 422 int nbContentIdsToReference = contentIdsToReference.size(); 423 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + ".size", String.valueOf(nbContentIdsToReference)); 424 425 String inRepeaterPath = StringUtils.removeStart(metadataName, lastRepeaterPath + ".1"); 426 427 for (int i = 1; i <= nbContentIdsToReference; i++) 428 { 429 values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + inRepeaterPath, contentIdsToReference.get(i - 1)); 430 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + inRepeaterPath + ".mode", MODE.INSERT.name()); 431 432 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + ".position", "0"); // 0 means at the end 433 } 434 } 435 else 436 { 437 values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, contentIdsToReference.get(0)); 438 values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name()); 439 } 440 441 // Find the edit action to use 442 _doAction(content, workflowActionIds, values, errorIds, errorMessages); 443 } 444 } 445 446 private void _doAction(WorkflowAwareContent content, List<String> workflowActionIds, Map<String, Object> values, List<String> errorIds, List<I18nizableText> errorMessages) 447 { 448 Integer actionId = null; 449 450 int[] actionIds = _contentWorkflowHelper.getAvailableActions(content); 451 for (String workflowActionIdToTryAsString : workflowActionIds) 452 { 453 Integer workflowActionIdToTry = Integer.parseInt(workflowActionIdToTryAsString); 454 if (ArrayUtils.contains(actionIds, workflowActionIdToTry)) 455 { 456 actionId = workflowActionIdToTry; 457 break; 458 } 459 } 460 461 if (actionId == null) 462 { 463 List<String> parameters = new ArrayList<>(); 464 parameters.add(content.getTitle()); 465 parameters.add(content.getName()); 466 parameters.add(content.getId()); 467 errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_WORKFLOW", parameters)); 468 errorIds.add(content.getId()); 469 } 470 else 471 { 472 // edit 473 Map<String, Object> contextParameters = new HashMap<>(); 474 contextParameters.put("quit", true); 475 contextParameters.put("values", values); 476 477 Map<String, Object> inputs = new HashMap<>(); 478 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 479 480 try 481 { 482 _contentWorkflowHelper.doAction(content, actionId, inputs); 483 } 484 catch (Exception e) 485 { 486 getLogger().error("Content '" + content.getTitle() + "' (" + content.getName() + "/" + content.getId() + ") was not modified", e); 487 488 Map<String, I18nizableText> parameters = new HashMap<>(); 489 parameters.put("0", new I18nizableText(content.getTitle())); 490 parameters.put("1", new I18nizableText(content.getName())); 491 parameters.put("2", new I18nizableText(content.getId())); 492 493 if (e instanceof InvalidInputWorkflowException) 494 { 495 I18nizableText rootError = null; 496 497 AllErrors allErrors = ((InvalidInputWorkflowException) e).getErrors(); 498 Map<String, Errors> allErrorsMap = allErrors.getAllErrors(); 499 for (String errorMetadataPath : allErrorsMap.keySet()) 500 { 501 Errors errors = allErrorsMap.get(errorMetadataPath); 502 503 I18nizableText insideError = null; 504 505 List<I18nizableText> errorsAsList = errors.getErrors(); 506 for (I18nizableText error : errorsAsList) 507 { 508 Map<String, I18nizableText> i18nparameters = new HashMap<>(); 509 i18nparameters.put("0", error); 510 511 I18nizableText localError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_VALIDATION_METADATA_CHAIN", i18nparameters); 512 513 if (insideError == null) 514 { 515 insideError = localError; 516 } 517 else 518 { 519 insideError.getParameterMap().put("1", localError); 520 } 521 } 522 523 Map<String, I18nizableText> i18ngeneralparameters = new HashMap<>(); 524 i18ngeneralparameters.put("0", new I18nizableText(errorMetadataPath)); 525 i18ngeneralparameters.put("1", insideError); 526 527 I18nizableText generalError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_VALIDATION_METADATA", i18ngeneralparameters); 528 if (rootError == null) 529 { 530 rootError = generalError; 531 } 532 else 533 { 534 rootError.getParameterMap().put("2", generalError); 535 } 536 } 537 538 parameters.put("3", rootError); 539 } 540 else 541 { 542 if (e.getMessage() != null) 543 { 544 parameters.put("3", new I18nizableText(e.getMessage())); 545 } 546 else 547 { 548 parameters.put("3", new I18nizableText(e.getClass().getName())); 549 } 550 } 551 errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_EDIT", parameters)); 552 errorIds.add(content.getId()); 553 } 554 } 555 } 556 557 /** 558 * Resolve content by their ids 559 * @param contentIds The id of contents to resolve 560 * @return the contents 561 */ 562 protected List<? extends Content> _resolve(List<String> contentIds) 563 { 564 List<Content> contents = new ArrayList<>(); 565 566 for (String contentId: contentIds) 567 { 568 Content content = _resolver.resolveById(contentId); 569 contents.add(content); 570 } 571 572 return contents; 573 } 574}