001/* 002 * Copyright 2016 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.content; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.Iterator; 024import java.util.LinkedHashMap; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028 029import javax.jcr.Node; 030import javax.jcr.NodeIterator; 031import javax.jcr.RepositoryException; 032 033import org.apache.avalon.framework.component.Component; 034import org.apache.avalon.framework.context.Context; 035import org.apache.avalon.framework.context.ContextException; 036import org.apache.avalon.framework.context.Contextualizable; 037import org.apache.avalon.framework.logger.AbstractLogEnabled; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.cocoon.components.ContextHelper; 042import org.apache.commons.collections.CollectionUtils; 043import org.apache.commons.lang3.ArrayUtils; 044import org.apache.commons.lang3.StringUtils; 045import org.apache.commons.lang3.tuple.ImmutablePair; 046import org.apache.commons.lang3.tuple.Pair; 047 048import org.ametys.cms.ObservationConstants; 049import org.ametys.cms.content.references.OutgoingReferencesHelper; 050import org.ametys.cms.contenttype.ContentConstants; 051import org.ametys.cms.contenttype.ContentType; 052import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 053import org.ametys.cms.contenttype.ContentTypesHelper; 054import org.ametys.cms.contenttype.MetadataDefinition; 055import org.ametys.cms.contenttype.MetadataManager; 056import org.ametys.cms.contenttype.MetadataType; 057import org.ametys.cms.contenttype.RepeaterDefinition; 058import org.ametys.cms.repository.Content; 059import org.ametys.cms.repository.DefaultContent; 060import org.ametys.cms.repository.ModifiableContent; 061import org.ametys.cms.repository.WorkflowAwareContent; 062import org.ametys.cms.search.model.SystemProperty; 063import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 064import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 065import org.ametys.core.observation.Event; 066import org.ametys.core.observation.ObservationManager; 067import org.ametys.core.ui.Callable; 068import org.ametys.core.user.CurrentUserProvider; 069import org.ametys.plugins.explorer.resources.Resource; 070import org.ametys.plugins.repository.AmetysObjectResolver; 071import org.ametys.plugins.repository.AmetysRepositoryException; 072import org.ametys.plugins.repository.RepositoryConstants; 073import org.ametys.plugins.repository.UnknownAmetysObjectException; 074import org.ametys.plugins.repository.jcr.JCRAmetysObject; 075import org.ametys.plugins.repository.metadata.CompositeMetadata; 076import org.ametys.plugins.repository.metadata.MultilingualString; 077import org.ametys.plugins.repository.metadata.MultilingualStringHelper; 078import org.ametys.plugins.repository.metadata.UnknownMetadataException; 079import org.ametys.plugins.workflow.AbstractWorkflowComponent; 080import org.ametys.plugins.workflow.support.WorkflowProvider; 081import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 082 083import com.opensymphony.workflow.WorkflowException; 084 085/** 086 * Helper for {@link Content} 087 * 088 */ 089public class ContentHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 090{ 091 /** The component role. */ 092 public static final String ROLE = ContentHelper.class.getName(); 093 094 private AmetysObjectResolver _resolver; 095 private ContentTypesHelper _contentTypesHelper; 096 private ContentTypeExtensionPoint _contentTypeEP; 097 098 private ObservationManager _observationManager; 099 private WorkflowProvider _workflowProvider; 100 private CurrentUserProvider _currentUserProvider; 101 private SystemPropertyExtensionPoint _systemPropertyExtensionPoint; 102 103 private Context _context; 104 105 @Override 106 public void service(ServiceManager smanager) throws ServiceException 107 { 108 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 109 _contentTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE); 110 _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 111 _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE); 112 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 113 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 114 _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) smanager.lookup(SystemPropertyExtensionPoint.ROLE); 115 } 116 117 @Override 118 public void contextualize(Context context) throws ContextException 119 { 120 _context = context; 121 } 122 123 /** 124 * Add a content type to an existing content 125 * @param contentId The content id 126 * @param contentTypeId The content type to add 127 * @param actionId The workflow action id 128 * @return The result in a Map 129 * @throws WorkflowException if 130 * @throws AmetysRepositoryException if an error occurred 131 */ 132 @Callable 133 public Map<String, Object> addContentType (String contentId, String contentTypeId, int actionId) throws AmetysRepositoryException, WorkflowException 134 { 135 return _setContentType(contentId, contentTypeId, actionId, false); 136 } 137 138 /** 139 * Remove a content type to an existing content 140 * @param contentId The content id 141 * @param contentTypeId The content type to add 142 * @param actionId The workflow action id 143 * @return The result in a Map 144 * @throws WorkflowException if 145 * @throws AmetysRepositoryException if an error occurred 146 */ 147 @Callable 148 public Map<String, Object> removeContentType (String contentId, String contentTypeId, int actionId) throws AmetysRepositoryException, WorkflowException 149 { 150 return _setContentType(contentId, contentTypeId, actionId, true); 151 } 152 153 /** 154 * Add a mixin type to an existing content 155 * @param contentId The content id 156 * @param mixinId The mixin type to add 157 * @param actionId The workflow action id 158 * @return The result in a Map 159 * @throws WorkflowException if 160 * @throws AmetysRepositoryException if an error occurred 161 */ 162 @Callable 163 public Map<String, Object> addMixinType (String contentId, String mixinId, int actionId) throws AmetysRepositoryException, WorkflowException 164 { 165 return _setMixinType(contentId, mixinId, actionId, false); 166 } 167 168 /** 169 * Remove a mixin type to an existing content 170 * @param contentId The content id 171 * @param mixinId The mixin type to add 172 * @param actionId The workflow action id 173 * @return The result in a Map 174 * @throws WorkflowException if 175 * @throws AmetysRepositoryException if an error occurred 176 */ 177 @Callable 178 public Map<String, Object> removeMixinType (String contentId, String mixinId, int actionId) throws AmetysRepositoryException, WorkflowException 179 { 180 return _setMixinType(contentId, mixinId, actionId, true); 181 } 182 183 /** 184 * Get content edition information. 185 * @param contentId the content ID. 186 * @return a Map containing content edition information. 187 */ 188 @Callable 189 public Map<String, Object> getContentEditionInformation(String contentId) 190 { 191 Map<String, Object> info = new HashMap<>(); 192 193 Content content = _resolver.resolveById(contentId); 194 195 info.put("hasIndexingReferences", hasIndexingReferences(content)); 196 197 return info; 198 } 199 200 /** 201 * Test if the given content has indexing references, i.e. if modifying it 202 * potentially implies reindexing other contents. 203 * @param content the content to test. 204 * @return <code>true</code> if one of the content types or mixins has indexing references, <code>false</code> otherwise. 205 */ 206 public boolean hasIndexingReferences(Content content) 207 { 208 for (String cTypeId : content.getTypes()) 209 { 210 if (_contentTypeEP.hasIndexingReferences(cTypeId)) 211 { 212 return true; 213 } 214 } 215 216 for (String mixinId : content.getMixinTypes()) 217 { 218 if (_contentTypeEP.hasIndexingReferences(mixinId)) 219 { 220 return true; 221 } 222 } 223 224 return false; 225 } 226 227 private Map<String, Object> _setContentType (String contentId, String contentTypeId, int actionId, boolean remove) throws AmetysRepositoryException, WorkflowException 228 { 229 Map<String, Object> result = new HashMap<>(); 230 231 Content content = _resolver.resolveById(contentId); 232 233 if (content instanceof ModifiableContent) 234 { 235 ModifiableContent modifiableContent = (ModifiableContent) content; 236 237 List<String> currentTypes = new ArrayList<>(Arrays.asList(content.getTypes())); 238 239 boolean hasChange = false; 240 if (remove) 241 { 242 if (currentTypes.size() > 1) 243 { 244 hasChange = currentTypes.remove(contentTypeId); 245 } 246 else 247 { 248 result.put("failure", true); 249 result.put("msg", "empty-list"); 250 } 251 } 252 else if (!currentTypes.contains(contentTypeId)) 253 { 254 ContentType cType = _contentTypeEP.getExtension(contentTypeId); 255 if (cType.isMixin()) 256 { 257 result.put("failure", true); 258 result.put("msg", "no-content-type"); 259 getLogger().error("Content type '" + contentTypeId + "' is a mixin type. It can not be added as content type."); 260 } 261 else if (!_contentTypesHelper.isCompatibleContentType(content, contentTypeId)) 262 { 263 result.put("failure", true); 264 result.put("msg", "invalid-content-type"); 265 getLogger().error("Content type '" + contentTypeId + "' is incompatible with content '" + contentId + "'."); 266 } 267 else 268 { 269 currentTypes.add(contentTypeId); 270 hasChange = true; 271 } 272 } 273 274 if (hasChange) 275 { 276 // TODO check if the content type is compatible 277 modifiableContent.setTypes(currentTypes.toArray(new String[currentTypes.size()])); 278 modifiableContent.saveChanges(); 279 280 if (content instanceof WorkflowAwareContent) 281 { 282 283 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 284 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 285 286 Map<String, Object> inputs = new HashMap<>(); 287 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 288 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>()); 289 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>()); 290 291 workflow.doAction(waContent.getWorkflowId(), actionId, inputs); 292 } 293 294 result.put("success", true); 295 296 Map<String, Object> eventParams = new HashMap<>(); 297 eventParams.put(ObservationConstants.ARGS_CONTENT, modifiableContent); 298 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, contentId); 299 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams)); 300 } 301 } 302 else 303 { 304 result.put("failure", true); 305 result.put("msg", "no-modifiable-content"); 306 getLogger().error("Can not modified content types to a non-modifiable content '" + content.getId() + "'."); 307 } 308 309 return result; 310 } 311 312 private Map<String, Object> _setMixinType (String contentId, String mixinId, int actionId, boolean remove) throws AmetysRepositoryException, WorkflowException 313 { 314 Map<String, Object> result = new HashMap<>(); 315 316 Content content = _resolver.resolveById(contentId); 317 318 if (content instanceof ModifiableContent) 319 { 320 ModifiableContent modifiableContent = (ModifiableContent) content; 321 322 List<String> currentMixins = new ArrayList<>(Arrays.asList(content.getMixinTypes())); 323 324 boolean hasChange = false; 325 if (remove) 326 { 327 hasChange = currentMixins.remove(mixinId); 328 } 329 else if (!currentMixins.contains(mixinId)) 330 { 331 ContentType cType = _contentTypeEP.getExtension(mixinId); 332 if (!cType.isMixin()) 333 { 334 result.put("failure", true); 335 result.put("msg", "no-mixin"); 336 getLogger().error("The content type '" + mixinId + "' is not a mixin type, it can be not be added as a mixin."); 337 } 338 else if (!_contentTypesHelper.isCompatibleContentType(content, mixinId)) 339 { 340 result.put("failure", true); 341 result.put("msg", "invalid-mixin"); 342 getLogger().error("Mixin '" + mixinId + "' is incompatible with content '" + contentId + "'."); 343 } 344 else 345 { 346 currentMixins.add(mixinId); 347 hasChange = true; 348 } 349 } 350 351 if (hasChange) 352 { 353 // TODO check if the content type is compatible 354 modifiableContent.setMixinTypes(currentMixins.toArray(new String[currentMixins.size()])); 355 modifiableContent.saveChanges(); 356 357 if (content instanceof WorkflowAwareContent) 358 { 359 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 360 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 361 362 Map<String, Object> inputs = new HashMap<>(); 363 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 364 inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>()); 365 inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>()); 366 367 workflow.doAction(waContent.getWorkflowId(), actionId, inputs); 368 } 369 370 result.put("success", true); 371 372 Map<String, Object> eventParams = new HashMap<>(); 373 eventParams.put(ObservationConstants.ARGS_CONTENT, modifiableContent); 374 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, contentId); 375 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams)); 376 } 377 } 378 else 379 { 380 result.put("failure", true); 381 result.put("msg", "no-modifiable-content"); 382 getLogger().error("Can not modified mixins to a non-modifiable content '" + content.getId() + "'."); 383 } 384 385 return result; 386 } 387 388 /** 389 * Determines if the content is a reference table content type 390 * @param content The content 391 * @return true if content is a reference table 392 */ 393 public boolean isReferenceTable(Content content) 394 { 395 for (String cTypeId : content.getTypes()) 396 { 397 ContentType cType = _contentTypeEP.getExtension(cTypeId); 398 if (cType != null) 399 { 400 if (!cType.isReferenceTable()) 401 { 402 return false; 403 } 404 } 405 else 406 { 407 if (getLogger().isWarnEnabled()) 408 { 409 getLogger().warn(String.format("Unable to determine if a content is a reference table, unknown content type : '%s'.", cTypeId)); 410 } 411 } 412 } 413 return true; 414 } 415 416 /** 417 * Determines if a content is a multilingual content 418 * @param content The content 419 * @return <code>true</code> if the content is an instance of content type 420 */ 421 public boolean isMultilingual(Content content) 422 { 423 for (String cTypeId : content.getTypes()) 424 { 425 ContentType cType = _contentTypeEP.getExtension(cTypeId); 426 if (cType != null && cType.isMultilingual()) 427 { 428 return true; 429 } 430 } 431 return false; 432 } 433 434 /** 435 * Determines if the content is a simple content type 436 * @param content The content 437 * @return true if content is simple 438 */ 439 public boolean isSimple (Content content) 440 { 441 for (String cTypeId : content.getTypes()) 442 { 443 ContentType cType = _contentTypeEP.getExtension(cTypeId); 444 if (cType != null) 445 { 446 if (!cType.isSimple()) 447 { 448 return false; 449 } 450 } 451 else 452 { 453 if (getLogger().isWarnEnabled()) 454 { 455 getLogger().warn(String.format("Unable to determine if a content is simple, unknown content type : '%s'.", cTypeId)); 456 } 457 } 458 } 459 return true; 460 } 461 462 /** 463 * Get the typed value(s) of a content at given path. 464 * The path can represent a system property id or a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'. 465 * The returned value is typed. 466 * @param content The content 467 * @param fieldPath The field id or the path to the metadata, separated by '/' 468 * @param defaultLocale The default locale to resolve localized values if the content's language is null. Can be null. 469 * @param resolveReferences <code>true</code> true to resolve references (such as resource or content) 470 * @return The typed final value. If the final field is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection 471 */ 472 public Object getValue(Content content, String fieldPath, Locale defaultLocale, boolean resolveReferences) 473 { 474 return getValue(content, fieldPath, defaultLocale, resolveReferences, false); 475 } 476 477 /** 478 * Get the typed value(s) of a content at given path. 479 * The path can represent a system property id or a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'. 480 * The returned value is typed. 481 * @param content The content 482 * @param fieldPath The field id or the path to the metadata, separated by '/' 483 * @param defaultLocale The default locale to resolve localized values if the content's language is null. Can be null. 484 * @param resolveReferences <code>true</code> true to resolve references (such as resource or content) 485 * @param returnNullValues <code>true</code> true to return null values when the metadata does not exists in a repeater or linked content. 486 * @return The typed final value. If the final field is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection 487 */ 488 public Object getValue(Content content, String fieldPath, Locale defaultLocale, boolean resolveReferences, boolean returnNullValues) 489 { 490 if (StringUtils.isAnyEmpty(fieldPath)) 491 { 492 return null; 493 } 494 495 // Manage System Properties 496 String[] pathSegments = fieldPath.split(ContentConstants.METADATA_PATH_SEPARATOR); 497 String propertyName = pathSegments[pathSegments.length - 1]; 498 499 if (_systemPropertyExtensionPoint.hasExtension(propertyName)) 500 { 501 if (_systemPropertyExtensionPoint.isDisplayable(propertyName)) 502 { 503 SystemProperty systemProperty = _systemPropertyExtensionPoint.getExtension(propertyName); 504 return _getSystemPropertyValue(content, pathSegments, systemProperty); 505 } 506 else 507 { 508 throw new IllegalArgumentException("The system property '" + propertyName + "' is not displayable."); 509 } 510 } 511 else 512 { 513 return getMetadataValue(content, fieldPath, defaultLocale, resolveReferences, returnNullValues); 514 } 515 } 516 517 private Object _getSystemPropertyValue(Content content, String[] pathSegments, SystemProperty systemProperty) 518 { 519 String contentFieldPath = StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 0, pathSegments.length - 1); 520 List<Content> contentsContainingSystemProperty = getTargetContents(content, contentFieldPath); 521 if (contentsContainingSystemProperty.size() == 1) 522 { 523 Object value = systemProperty.getValue(contentsContainingSystemProperty.get(0)); 524 525 if (value instanceof Object[]) 526 { 527 return Arrays.asList((Object[]) value); 528 } 529 else 530 { 531 return value; 532 } 533 } 534 else 535 { 536 List<Object> values = new ArrayList<>(); 537 for (Content contentContainingSystemProperty : contentsContainingSystemProperty) 538 { 539 Object value = systemProperty.getValue(contentContainingSystemProperty); 540 541 if (value instanceof Object[]) 542 { 543 values.addAll(Arrays.asList((Object[]) value)); 544 } 545 else 546 { 547 values.add(value); 548 } 549 } 550 return values; 551 } 552 } 553 554 /** 555 * Get the content from which to get the system property. 556 * @param sourceContent The source content. 557 * @param fieldPath The field path 558 * @return The target content. 559 */ 560 public Content getTargetContent(Content sourceContent, String fieldPath) 561 { 562 if (StringUtils.isBlank(fieldPath)) 563 { 564 return sourceContent; 565 } 566 else 567 { 568 Object value = getMetadataValue(sourceContent, fieldPath, null, true); 569 if (value != null && value instanceof Content) 570 { 571 return (Content) value; 572 } 573 else if (value != null && value instanceof Collection<?> && ((Collection) value).size() > 0) 574 { 575 Object first = ((Collection) value).iterator().next(); 576 if (first instanceof Content) 577 { 578 return (Content) first; 579 } 580 } 581 } 582 583 return null; 584 } 585 586 /** 587 * Get the contents from which to get the system property. 588 * @param sourceContent The source content. 589 * @param fieldPath The field path 590 * @return The target contents. 591 */ 592 public List<Content> getTargetContents(Content sourceContent, String fieldPath) 593 { 594 List<Content> targetContents = new ArrayList<>(); 595 596 if (StringUtils.isBlank(fieldPath)) 597 { 598 targetContents.add(sourceContent); 599 } 600 else 601 { 602 Object value = getMetadataValue(sourceContent, fieldPath, null, true); 603 if (value != null && value instanceof Content) 604 { 605 targetContents.add((Content) value); 606 } 607 else if (value != null && value instanceof Collection<?>) 608 { 609 Iterator it = ((Collection) value).iterator(); 610 while (it.hasNext()) 611 { 612 Object object = it.next(); 613 if (object instanceof Content) 614 { 615 targetContents.add((Content) object); 616 } 617 618 } 619 } 620 } 621 622 return targetContents; 623 } 624 625 /** 626 * Get the typed metadata value(s) of a content at given path. 627 * The path can represent a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'. 628 * The returned value is typed. 629 * @param content The content 630 * @param metadataPath The path to the metadata, separated by '/' 631 * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. 632 * Only to be valued if initial content's language is null, otherwise set this parameter to null. 633 * @param resolveReferences <code>true</code> true to resolve references (such as resource or content) 634 * @return The typed final value. If the final metadata is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection 635 */ 636 public Object getMetadataValue(Content content, String metadataPath, Locale defaultLocale, boolean resolveReferences) 637 { 638 return getMetadataValue(content, metadataPath, defaultLocale, resolveReferences, false); 639 } 640 641 /** 642 * Get the typed metadata value(s) of a content at given path. 643 * The path can represent a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'. 644 * The returned value is typed. 645 * @param content The content 646 * @param metadataPath The path to the metadata, separated by '/' 647 * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. 648 * Only to be valued if initial content's language is null, otherwise set this parameter to null. 649 * @param resolveReferences <code>true</code> true to resolve references (such as resource or content) 650 * @param returnNullValues <code>true</code> true to return null values when the metadata does not exists in a repeater or linked content. 651 * @return The typed final value. If the final metadata is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection 652 */ 653 public Object getMetadataValue(Content content, String metadataPath, Locale defaultLocale, boolean resolveReferences, boolean returnNullValues) 654 { 655 String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR); 656 657 MetadataDefinition definition = _contentTypesHelper.getMetadataDefinition(pathSegments[0], content); 658 if (definition != null) 659 { 660 Locale contentLocale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale; 661 return getMetadataValue(content.getMetadataHolder(), definition, metadataPath, contentLocale, resolveReferences, returnNullValues); 662 } 663 664 getLogger().warn("Unknown metadata definition at path '" + metadataPath + "' for content '" + content.getId()); 665 return null; 666 } 667 668 /** 669 * Get the typed values of a content at given path. 670 * The value is always returned into a collection of object event if the metadata is a single metadata. 671 * The path can represent a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'. 672 * The returned value is typed. 673 * @param contentId The ID of the content 674 * @param metadataPath The Path to the metadata, separated by '/' 675 * @return The typed final value. If the final metadata is single, the returned value will be a Collection of one element 676 */ 677 @Callable 678 public List<Object> getMetadataValues(String contentId, String metadataPath) 679 { 680 return getMetadataValues(_resolver.resolveById(contentId), metadataPath, null, false, true); 681 } 682 683 /** 684 * Get the typed values of a content at given path. 685 * The value is always returned into a collection of object event if the metadata is a single metadata. 686 * The path can represent a path of a metadata into the content or a metadata on one or more linked contents, such as 'composite/linkedContent/secondContent/composite/metadata'. 687 * The returned value is typed. 688 * @param content The content 689 * @param metadataPath The path to the metadata, separated by '/' 690 * @param defaultLocale The locale to use to sax localized values such as multilingual content or multilingual string. 691 * Only to be valued if initial content's language is null, otherwise set this parameter to null. 692 * @param resolveReferences <code>true</code> true to resolve references (such as resource or content) 693 * @param returnNullValues <code>true</code> true to return null values when the metadata does not exists in a repeater or linked content. 694 * @return The typed final value. If the final metadata is single, the returned value will be a Collection of one element 695 */ 696 public List<Object> getMetadataValues(Content content, String metadataPath, Locale defaultLocale, boolean resolveReferences, boolean returnNullValues) 697 { 698 String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR); 699 700 MetadataDefinition definition = _contentTypesHelper.getMetadataDefinition(pathSegments[0], content); 701 if (definition != null) 702 { 703 Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale; 704 Object values = getMetadataValue(content.getMetadataHolder(), definition, metadataPath, locale, resolveReferences, returnNullValues); 705 if (values instanceof Collection<?>) 706 { 707 return new ArrayList<>((Collection<?>) values); 708 } 709 else if (values != null || returnNullValues) 710 { 711 return Arrays.asList(values); 712 } 713 else 714 { 715 return Collections.EMPTY_LIST; 716 } 717 } 718 719 getLogger().warn("Unknown metadata definition at path '" + metadataPath + "' for content '" + content.getId()); 720 721 return null; 722 } 723 724 /** 725 * Get the title of a content.<br> 726 * If the content is a multilingual content, the title will be retrieved for the current locale if exists, or for default locale 'en' if exists, or for the first found locale. 727 * @param content The content 728 * @return The title of the content 729 */ 730 public String getTitle(Content content) 731 { 732 Locale defaultLocale = null; 733 734 try 735 { 736 Map objectModel = (Map) _context.get(ContextHelper.CONTEXT_OBJECT_MODEL); 737 if (objectModel != null) 738 { 739 // The object model can be null if #getTitle(content) is called outside a request 740 defaultLocale = org.apache.cocoon.i18n.I18nUtils.findLocale(objectModel, "locale", null, Locale.getDefault(), true); 741 } 742 } 743 catch (ContextException e) 744 { 745 // There is no context 746 } 747 748 // TODO Use user preference language ? 749 return content.getTitle(defaultLocale); 750 } 751 752 /** 753 * Get the title variants of a multilingual content 754 * @param content The multilingual content 755 * @return the content's title for each locale 756 * @throws IllegalArgumentException if the content is not a multilingual content 757 */ 758 public Map<String, String> getTitleVariants(Content content) 759 { 760 if (!isMultilingual(content)) 761 { 762 throw new IllegalArgumentException("Can not get title variants for a non-multilingual content " + content.getId()); 763 } 764 765 Map<String, String> variants = new HashMap<>(); 766 767 MultilingualString value = content.getMetadataHolder().getMultilingualString(DefaultContent.METADATA_TITLE); 768 for (Locale locale : value.getLocales()) 769 { 770 variants.put(locale.getLanguage(), value.getValue(locale)); 771 } 772 773 return variants; 774 } 775 776 /** 777 * Get the typed value(s) at given path. 778 * The path can represent a path of a metadata in the parent composite metadata or a path of a metadata into a linked content. 779 * The returned value is typed. 780 * @param metadataHolder The parent composite metadata 781 * @param definition The definition of the first metadata in path 782 * @param metadataPath The path to the metadata, separated by '/' 783 * @param locale The locale to used to resolve localized metadata 784 * @param resolveReferences <code>true</code> true to resolve references (such as resource or content) 785 * @param returnNullValues <code>true</code> true to return null values when metadata does not exists. 786 * @return The typed final value. If the final metadata is multiple, or contains into a repeater or multiple 'CONTENT' metadata, the returned value will be a Collection 787 */ 788 public Object getMetadataValue(CompositeMetadata metadataHolder, MetadataDefinition definition, String metadataPath, Locale locale, boolean resolveReferences, boolean returnNullValues) 789 { 790 String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR); 791 792 String metadataName = pathSegments[0]; 793 794 if (!metadataHolder.hasMetadata(metadataName)) 795 { 796 return null; 797 } 798 799 MetadataType type = definition.getType(); 800 switch (type) 801 { 802 case COMPOSITE: 803 return _getCompositeMetadataValue(metadataHolder, definition, locale, resolveReferences, returnNullValues, pathSegments, metadataName); 804 805 case CONTENT: 806 return _getContentMetadataValue(metadataHolder, definition, locale, resolveReferences, returnNullValues, pathSegments, metadataName); 807 default: 808 if (pathSegments.length == 1) 809 { 810 return getSimpleMetadataValue(metadataHolder, definition, metadataName, locale, resolveReferences); 811 } 812 813 throw new IllegalArgumentException("Metadata at path '" + definition.getId() + "' is a simple metadata : can not invoked get sub metadata values at path " + StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length)); 814 } 815 } 816 817 private Object _getContentMetadataValue(CompositeMetadata metadataHolder, MetadataDefinition definition, Locale defaultLocale, boolean resolveReferences, boolean returnNullValues, 818 String[] pathSegments, String metadataName) 819 { 820 if (pathSegments.length > 1) 821 { 822 if (definition.isMultiple()) 823 { 824 List<Object> values = new ArrayList<>(); 825 String[] refContentIds = metadataHolder.getStringArray(metadataName, new String[0]); 826 for (String refContentId : refContentIds) 827 { 828 Content refContent = _resolver.resolveById(refContentId); 829 Locale locale = refContent.getLanguage() != null ? new Locale(refContent.getLanguage()) : defaultLocale; 830 Object remoteValue = getMetadataValue(refContent, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences, returnNullValues); 831 if (remoteValue != null && remoteValue instanceof Collection<?>) 832 { 833 values.addAll((Collection<?>) remoteValue); 834 } 835 else if (remoteValue != null || returnNullValues) 836 { 837 values.add(remoteValue); 838 } 839 } 840 return values; 841 } 842 else 843 { 844 String refContentId = metadataHolder.getString(metadataName); 845 Content refContent = _resolver.resolveById(refContentId); 846 Locale locale = refContent.getLanguage() != null ? new Locale(refContent.getLanguage()) : defaultLocale; 847 return getMetadataValue(refContent, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences, returnNullValues); 848 } 849 } 850 else 851 { 852 return getSimpleMetadataValue(metadataHolder, definition, metadataName, defaultLocale, resolveReferences); 853 } 854 } 855 856 private Object _getCompositeMetadataValue(CompositeMetadata metadataHolder, MetadataDefinition definition, Locale locale, boolean resolveReferences, boolean returnNullValues, String[] pathSegments, String metadataName) 857 { 858 if (pathSegments.length > 1) 859 { 860 if (definition instanceof RepeaterDefinition) 861 { 862 // Repeater: get and sort the entry names. 863 CompositeMetadata repeater = metadataHolder.getCompositeMetadata(metadataName); 864 String[] entries = repeater.getMetadataNames(); 865 Arrays.sort(entries, MetadataManager.REPEATER_ENTRY_COMPARATOR); 866 867 List<Object> values = new ArrayList<>(); 868 869 for (String entryName : entries) 870 { 871 CompositeMetadata entry = repeater.getCompositeMetadata(entryName); 872 873 Object entryValue = getMetadataValue(entry, definition.getMetadataDefinition(pathSegments[1]), StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences, returnNullValues); 874 if (entryValue != null && entryValue instanceof Collection<?>) 875 { 876 values.addAll((Collection<?>) entryValue); 877 } 878 else if (entryValue != null || returnNullValues) 879 { 880 values.add(entryValue); 881 } 882 } 883 884 return values; 885 } 886 else 887 { 888 // Composite. 889 CompositeMetadata subMetadataHolder = metadataHolder.getCompositeMetadata(metadataName); 890 MetadataDefinition subMetadataDef = definition.getMetadataDefinition(pathSegments[1]); 891 return getMetadataValue(subMetadataHolder, subMetadataDef, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences, returnNullValues); 892 } 893 } 894 895 throw new IllegalArgumentException("Metadata at path '" + definition.getId() + "' is a composite metadata : can not invoked #getMetadataValue"); 896 } 897 898 /** 899 * Get the typed value(s) of a simple metadata. 900 * @param metadataHolder The parent composite metadata 901 * @param definition The definition of the first metadata in path 902 * @param metadataName The name of the metadata 903 * @param locale The locale to used to resolve localized metadata 904 * @param resolveReferences <code>true</code> true to resolve references (such as resource or content) 905 * @return The typed final value. 906 */ 907 public Object getSimpleMetadataValue(CompositeMetadata metadataHolder, MetadataDefinition definition, String metadataName, Locale locale, boolean resolveReferences) 908 { 909 if (metadataName.contains(ContentConstants.METADATA_PATH_SEPARATOR)) 910 { 911 throw new IllegalArgumentException("The metadata name cannot represent a path : " + metadataName); 912 } 913 914 Object value = null; 915 916 switch (definition.getType()) 917 { 918 case LONG: 919 value = _getLongValue(metadataHolder, metadataName, definition); 920 break; 921 case DOUBLE: 922 value = _getDoubleValue(metadataHolder, metadataName, definition); 923 break; 924 case BOOLEAN: 925 value = _getBooleanValue(metadataHolder, metadataName, definition); 926 break; 927 case DATE: 928 case DATETIME: 929 value = _getDateValue(metadataHolder, metadataName, definition); 930 break; 931 case USER: 932 value = _getUserValue(metadataHolder, metadataName, definition); 933 break; 934 case BINARY: 935 value = _getBinaryValue(metadataHolder, metadataName); 936 break; 937 case FILE: 938 value = _getFileValue(metadataHolder, metadataName, definition, resolveReferences); 939 break; 940 case GEOCODE: 941 value = _getGeocodeValue(metadataHolder, metadataName); 942 break; 943 case RICH_TEXT: 944 value = _getRichTextValue(metadataHolder, metadataName); 945 break; 946 case CONTENT: 947 value = _getContentValue(metadataHolder, metadataName, definition, resolveReferences); 948 break; 949 case SUB_CONTENT: 950 // TODO or ignore ? 951 break; 952 case REFERENCE: 953 value = _getReferenceValue(metadataHolder, metadataName, definition); 954 break; 955 case MULTILINGUAL_STRING: 956 value = _getMultilingualStringValue(metadataHolder, metadataName, locale, resolveReferences); 957 break; 958 case STRING: 959 default: 960 value = _getStringValue(metadataHolder, metadataName, definition); 961 } 962 963 return value; 964 } 965 966 private Object _getStringValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition) 967 { 968 try 969 { 970 if (definition.isMultiple()) 971 { 972 return Arrays.asList(metadataHolder.getStringArray(metadataName)); 973 } 974 else 975 { 976 return metadataHolder.getString(metadataName); 977 } 978 } 979 catch (UnknownMetadataException e) 980 { 981 // Ignore, just return null. 982 return null; 983 } 984 } 985 986 private Object _getMultilingualStringValue(CompositeMetadata metadataHolder, String metadataName, Locale locale, boolean resolve) 987 { 988 if (resolve) 989 { 990 return metadataHolder.getMultilingualString(metadataName); 991 } 992 else 993 { 994 return MultilingualStringHelper.getValue(metadataHolder, metadataName, locale); 995 } 996 } 997 998 private Object _getContentValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition, boolean resolve) 999 { 1000 try 1001 { 1002 if (definition.isMultiple()) 1003 { 1004 String[] refContentIds = metadataHolder.getStringArray(metadataName); 1005 if (resolve) 1006 { 1007 List<Content> contents = new ArrayList<>(); 1008 for (String refContentId : refContentIds) 1009 { 1010 try 1011 { 1012 contents.add(_resolver.resolveById(refContentId)); 1013 } 1014 catch (UnknownAmetysObjectException e) 1015 { 1016 // Ignore 1017 if (getLogger().isWarnEnabled()) 1018 { 1019 getLogger().warn("Metadata '" + definition.getId() + "' refers a non-existing content of id '" + refContentId + "'", e); 1020 } 1021 } 1022 } 1023 return contents; 1024 } 1025 else 1026 { 1027 return Arrays.asList(refContentIds); 1028 } 1029 } 1030 else 1031 { 1032 if (resolve) 1033 { 1034 try 1035 { 1036 return _resolver.resolveById(metadataHolder.getString(metadataName)); 1037 } 1038 catch (UnknownAmetysObjectException e) 1039 { 1040 return null; 1041 } 1042 } 1043 else 1044 { 1045 return metadataHolder.getString(metadataName); 1046 } 1047 } 1048 } 1049 catch (UnknownMetadataException e) 1050 { 1051 // Ignore, just return null. 1052 return null; 1053 } 1054 } 1055 1056 private Object _getUserValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition) 1057 { 1058 try 1059 { 1060 if (definition.isMultiple()) 1061 { 1062 return Arrays.asList(metadataHolder.getUserArray(metadataName)); 1063 } 1064 else 1065 { 1066 return metadataHolder.getUser(metadataName); 1067 } 1068 } 1069 catch (UnknownMetadataException e) 1070 { 1071 // Ignore, just return null. 1072 return null; 1073 } 1074 } 1075 1076 private Object _getDateValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition) 1077 { 1078 try 1079 { 1080 if (definition.isMultiple()) 1081 { 1082 return Arrays.asList(metadataHolder.getDateArray(metadataName)); 1083 } 1084 else 1085 { 1086 return metadataHolder.getDate(metadataName); 1087 } 1088 } 1089 catch (UnknownMetadataException e) 1090 { 1091 // Ignore, just return null. 1092 return null; 1093 } 1094 } 1095 1096 private Object _getLongValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition) 1097 { 1098 try 1099 { 1100 if (definition.isMultiple()) 1101 { 1102 return Arrays.asList(ArrayUtils.toObject(metadataHolder.getLongArray(metadataName))); 1103 } 1104 else 1105 { 1106 return metadataHolder.getLong(metadataName); 1107 } 1108 } 1109 catch (UnknownMetadataException e) 1110 { 1111 // Ignore, just return null. 1112 return null; 1113 } 1114 } 1115 1116 private Object _getDoubleValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition) 1117 { 1118 try 1119 { 1120 if (definition.isMultiple()) 1121 { 1122 return Arrays.asList(ArrayUtils.toObject(metadataHolder.getDoubleArray(metadataName))); 1123 } 1124 else 1125 { 1126 return metadataHolder.getDouble(metadataName); 1127 } 1128 } 1129 catch (UnknownMetadataException e) 1130 { 1131 // Ignore, just return null. 1132 return null; 1133 } 1134 } 1135 1136 private Object _getBooleanValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition) 1137 { 1138 try 1139 { 1140 if (definition.isMultiple()) 1141 { 1142 return Arrays.asList(ArrayUtils.toObject(metadataHolder.getBooleanArray(metadataName))); 1143 } 1144 else 1145 { 1146 return metadataHolder.getBoolean(metadataName); 1147 } 1148 } 1149 catch (UnknownMetadataException e) 1150 { 1151 // Ignore, just return null. 1152 return null; 1153 } 1154 } 1155 1156 private Object _getRichTextValue(CompositeMetadata metadataHolder, String metadataName) 1157 { 1158 try 1159 { 1160 return metadataHolder.getRichText(metadataName); 1161 } 1162 catch (UnknownMetadataException e) 1163 { 1164 // Ignore, just return null. 1165 return null; 1166 } 1167 } 1168 1169 private Object _getBinaryValue(CompositeMetadata metadataHolder, String metadataName) 1170 { 1171 try 1172 { 1173 return metadataHolder.getBinaryMetadata(metadataName); 1174 } 1175 catch (UnknownMetadataException e) 1176 { 1177 // Ignore, just return null. 1178 return null; 1179 } 1180 } 1181 1182 private Object _getResourceValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition, boolean resolve) 1183 { 1184 try 1185 { 1186 if (definition.isMultiple()) 1187 { 1188 String[] resourceIds = metadataHolder.getStringArray(metadataName); 1189 if (resolve) 1190 { 1191 List<Resource> resources = new ArrayList<>(); 1192 for (String resourceId : resourceIds) 1193 { 1194 try 1195 { 1196 resources.add(_resolver.resolveById(resourceId)); 1197 } 1198 catch (UnknownAmetysObjectException e) 1199 { 1200 // Ignore 1201 } 1202 } 1203 return resources; 1204 } 1205 else 1206 { 1207 return Arrays.asList(resourceIds); 1208 } 1209 } 1210 else 1211 { 1212 if (resolve) 1213 { 1214 try 1215 { 1216 return _resolver.resolveById(metadataHolder.getString(metadataName)); 1217 } 1218 catch (UnknownAmetysObjectException e) 1219 { 1220 return null; 1221 } 1222 } 1223 else 1224 { 1225 return metadataHolder.getString(metadataName); 1226 } 1227 } 1228 } 1229 catch (UnknownMetadataException e) 1230 { 1231 // Ignore, just return null. 1232 return null; 1233 } 1234 } 1235 1236 private Object _getGeocodeValue(CompositeMetadata metadataHolder, String metadataName) 1237 { 1238 try 1239 { 1240 // FIXME should return a GeoCode object 1241 CompositeMetadata geoMetadata = metadataHolder.getCompositeMetadata(metadataName); 1242 1243 if (geoMetadata.hasMetadata("longitude") && geoMetadata.hasMetadata("latitude")) 1244 { 1245 Double longitude = geoMetadata.getDouble("longitude"); 1246 Double latitude = geoMetadata.getDouble("latitude"); 1247 1248 Map<String, Double> geocode = new LinkedHashMap<>(); 1249 geocode.put("longitude", longitude); 1250 geocode.put("latitude", latitude); 1251 1252 return geocode; 1253 } 1254 } 1255 catch (UnknownMetadataException e) 1256 { 1257 // Ignore, just return null. 1258 } 1259 1260 return null; 1261 } 1262 1263 private Object _getReferenceValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition) 1264 { 1265 try 1266 { 1267 // FIXME should return a Reference object 1268 1269 CompositeMetadata referencesComposite = metadataHolder.getCompositeMetadata(metadataName); 1270 1271 if (definition.isMultiple()) 1272 { 1273 List<Map<String, Object>> references = new ArrayList<>(); 1274 1275 String[] types = referencesComposite.getStringArray("types"); 1276 String[] values = referencesComposite.getStringArray("values"); 1277 1278 for (int i = 0; i < types.length; i++) 1279 { 1280 Map<String, Object> reference = new HashMap<>(2); 1281 reference.put("type", types[i]); 1282 reference.put("value", values[i]); 1283 1284 references.add(reference); 1285 } 1286 1287 return references; 1288 } 1289 else 1290 { 1291 String type = referencesComposite.getString("type"); 1292 String value = referencesComposite.getString("value"); 1293 1294 Map<String, Object> reference = new HashMap<>(2); 1295 reference.put("type", type); 1296 reference.put("value", value); 1297 1298 return reference; 1299 } 1300 } 1301 catch (UnknownMetadataException e) 1302 { 1303 // Ignore, just return null. 1304 return null; 1305 } 1306 } 1307 1308 private Object _getFileValue(CompositeMetadata metadataHolder, String metadataName, MetadataDefinition definition, boolean resolveReference) 1309 { 1310 if (org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.BINARY.equals(metadataHolder.getType(metadataName))) 1311 { 1312 return _getBinaryValue(metadataHolder, metadataName); 1313 } 1314 else 1315 { 1316 return _getResourceValue(metadataHolder, metadataName, definition, resolveReference); 1317 } 1318 } 1319 1320 /** 1321 * Determines if the content has referencing contents other than whose type is in content types to ignore. 1322 * @param content The content to check 1323 * @param ignoreContentTypes The content types to ignore for referencing contents 1324 * @param includeSubTypes True if sub content types are take into account in ignore content types 1325 * @return <code>true</code> if there is at least one Content referencing the content 1326 */ 1327 public boolean hasReferencingContents(Content content, List<String> ignoreContentTypes, boolean includeSubTypes) 1328 { 1329 List<String> newIgnoreContentTypes = new ArrayList<>(); 1330 newIgnoreContentTypes.addAll(ignoreContentTypes); 1331 if (includeSubTypes) 1332 { 1333 for (String contentType : ignoreContentTypes) 1334 { 1335 newIgnoreContentTypes.addAll(_contentTypeEP.getSubTypes(contentType)); 1336 } 1337 } 1338 1339 for (Content refContent : content.getReferencingContents()) 1340 { 1341 List<String> contentTypes = Arrays.asList(refContent.getTypes()); 1342 if (!CollectionUtils.containsAny(contentTypes, newIgnoreContentTypes)) 1343 { 1344 return true; 1345 } 1346 } 1347 1348 return false; 1349 } 1350 1351 /** 1352 * Returns all Contents referencing the given content with their value path 1353 * @param content The content to get references 1354 * @return the list of pair path / contents 1355 */ 1356 public List<Pair<String, Content>> getReferencingContents(Content content) 1357 { 1358 List<Pair<String, Content>> incomingReferences = new ArrayList<>(); 1359 try 1360 { 1361 NodeIterator results = OutgoingReferencesHelper.getContentOutgoingReferences((JCRAmetysObject) content); 1362 while (results.hasNext()) 1363 { 1364 Node node = results.nextNode(); 1365 1366 Node outgoingRefsNode = node.getParent(); // go up towards node 'ametys-internal:outgoing-references; 1367 String path = outgoingRefsNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + DefaultContent.METADATA_OUTGOING_REFERENCES_PATH_PROPERTY).getString(); 1368 1369 Node contentNode = outgoingRefsNode.getParent() // go up towards node 'ametys-internal:root-outgoing-references 1370 .getParent(); // go up towards node of the content 1371 Content refContent = _resolver.resolve(contentNode, false); 1372 1373 incomingReferences.add(new ImmutablePair<>(path, refContent)); 1374 } 1375 } 1376 catch (RepositoryException e) 1377 { 1378 throw new AmetysRepositoryException("Unable to resolve references for content " + content.getId(), e); 1379 } 1380 1381 return incomingReferences; 1382 } 1383}