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