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