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.search.content; 017 018import java.io.InputStream; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Date; 023import java.util.HashMap; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028import java.util.Optional; 029import java.util.stream.Collectors; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.context.Context; 033import org.apache.avalon.framework.context.ContextException; 034import org.apache.avalon.framework.context.Contextualizable; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.commons.lang3.ArrayUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.excalibur.xml.sax.SAXParser; 041import org.xml.sax.InputSource; 042 043import org.ametys.cms.content.ContentHelper; 044import org.ametys.cms.content.RichTextHandler; 045import org.ametys.cms.contenttype.ContentConstants; 046import org.ametys.cms.contenttype.ContentType; 047import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 048import org.ametys.cms.contenttype.ContentTypesHelper; 049import org.ametys.cms.contenttype.MetadataDefinition; 050import org.ametys.cms.contenttype.MetadataManager; 051import org.ametys.cms.contenttype.MetadataType; 052import org.ametys.cms.contenttype.RepeaterDefinition; 053import org.ametys.cms.contenttype.indexing.CustomIndexingField; 054import org.ametys.cms.contenttype.indexing.DefaultMetadataIndexingField; 055import org.ametys.cms.contenttype.indexing.IndexingField; 056import org.ametys.cms.contenttype.indexing.IndexingModel; 057import org.ametys.cms.contenttype.indexing.MetadataIndexingField; 058import org.ametys.cms.repository.Content; 059import org.ametys.cms.repository.DefaultContent; 060import org.ametys.cms.search.SearchField; 061import org.ametys.cms.search.model.SystemProperty; 062import org.ametys.cms.search.model.SystemPropertyExtensionPoint; 063import org.ametys.cms.search.solr.field.BooleanSearchField; 064import org.ametys.cms.search.solr.field.ContentSearchField; 065import org.ametys.cms.search.solr.field.DateSearchField; 066import org.ametys.cms.search.solr.field.DoubleSearchField; 067import org.ametys.cms.search.solr.field.JoinedSystemSearchField; 068import org.ametys.cms.search.solr.field.LongSearchField; 069import org.ametys.cms.search.solr.field.MultilingualStringSearchField; 070import org.ametys.cms.search.solr.field.StringSearchField; 071import org.ametys.cms.transformation.xslt.ResolveURIComponent; 072import org.ametys.cms.workflow.EditContentFunction; 073import org.ametys.core.ui.Callable; 074import org.ametys.core.user.UserIdentity; 075import org.ametys.core.util.DateUtils; 076import org.ametys.core.util.I18nUtils; 077import org.ametys.plugins.core.user.UserHelper; 078import org.ametys.plugins.explorer.resources.Resource; 079import org.ametys.plugins.repository.AmetysObjectResolver; 080import org.ametys.plugins.repository.metadata.BinaryMetadata; 081import org.ametys.plugins.repository.metadata.CompositeMetadata; 082import org.ametys.plugins.repository.metadata.MultilingualString; 083import org.ametys.plugins.repository.metadata.MultilingualStringHelper; 084import org.ametys.plugins.repository.metadata.RichText; 085import org.ametys.runtime.parameter.Enumerator; 086import org.ametys.runtime.plugin.component.AbstractLogEnabled; 087 088/** 089 * Component which helps content searching by providing a simple way to access 090 * content properties (either metadata or system properties). 091 */ 092public class ContentSearchHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 093{ 094 095 /** The component role. */ 096 public static final String ROLE = ContentSearchHelper.class.getName(); 097 098 /** The content type extension point. */ 099 protected ContentTypeExtensionPoint _cTypeEP; 100 101 /** The content type helper. */ 102 protected ContentTypesHelper _cTypeHelper; 103 104 /** The content helper. */ 105 protected ContentHelper _contentHelper; 106 107 /** The system property extension point. */ 108 protected SystemPropertyExtensionPoint _sysPropEP; 109 110 /** The ametys object resolver. */ 111 protected AmetysObjectResolver _resolver; 112 113 /** The user helper */ 114 protected UserHelper _userHelper; 115 116 /** Content Types helper */ 117 protected ContentTypesHelper _contentTypesHelper; 118 119 /** i18n Utils */ 120 protected I18nUtils _i18nUtils; 121 122 /** Avalon service manager */ 123 protected ServiceManager _manager; 124 125 private Context _context; 126 127 @Override 128 public void contextualize(Context context) throws ContextException 129 { 130 _context = context; 131 } 132 133 @Override 134 public void service(ServiceManager manager) throws ServiceException 135 { 136 _manager = manager; 137 _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 138 _cTypeHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 139 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 140 _sysPropEP = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE); 141 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 142 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 143 _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE); 144 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 145 } 146 147 /** 148 * Get the metadata indexing field for the "title" standard metadata. 149 * @return The standard title metadata indexing field. 150 */ 151 public MetadataIndexingField getTitleMetadataIndexingField() 152 { 153 return new DefaultMetadataIndexingField("title", ContentTypesHelper.getTitleMetadataDefinition(), "title"); 154 } 155 156 /** 157 * Get a {@link SearchField} corresponding to a metadata. 158 * @param metadataPath The metadata path. 159 * @param metadataType The metadata type. 160 * @param isTypeContentWithMultilingualTitle <code>true</code> if the type is Content and the linked contents have multilingual titles 161 * @return the search field. 162 */ 163 public SearchField getMetadataSearchField(String metadataPath, MetadataType metadataType, boolean isTypeContentWithMultilingualTitle) 164 { 165 return getMetadataSearchField(null, metadataPath, metadataType, isTypeContentWithMultilingualTitle); 166 } 167 168 /** 169 * Get a {@link SearchField} corresponding to a metadata. 170 * @param joinPaths The join paths 171 * @param metadataPath The metadata path. 172 * @param metadataType The metadata type. 173 * @param isTypeContentWithMultilingualTitle <code>true</code> if the type is Content and the linked contents have multilingual titles 174 * @return the search field. 175 */ 176 public SearchField getMetadataSearchField(List<String> joinPaths, String metadataPath, MetadataType metadataType, boolean isTypeContentWithMultilingualTitle) 177 { 178 switch (metadataType) 179 { 180 case STRING: 181 case USER: 182 return new StringSearchField(joinPaths, metadataPath); 183 case CONTENT: 184 case SUB_CONTENT: 185 return new ContentSearchField(joinPaths, metadataPath, isTypeContentWithMultilingualTitle, Optional.ofNullable(_context)); 186 case LONG: 187 return new LongSearchField(joinPaths, metadataPath); 188 case DOUBLE: 189 return new DoubleSearchField(joinPaths, metadataPath); 190 case BOOLEAN: 191 return new BooleanSearchField(joinPaths, metadataPath); 192 case DATE: 193 case DATETIME: 194 return new DateSearchField(joinPaths, metadataPath); 195 case MULTILINGUAL_STRING: 196 return new MultilingualStringSearchField(joinPaths, metadataPath, Optional.ofNullable(_context)); 197 case COMPOSITE: 198 case BINARY: 199 case FILE: 200 case RICH_TEXT: 201 case REFERENCE: 202 default: 203 return null; 204 } 205 } 206 207 /** 208 * Get a {@link SearchField} from a field name in a batch of content types. 209 * @param contentTypes The content types, can be empty to search on any content type. 210 * In that case, only the title metadata and system properties will be usable in sort and facets specs. 211 * @param fieldPath The field path, can be either a system property ID or a indexing field name or path. 212 * @return The {@link SearchField} corresponding to the field path, or an {@link Optional#empty() empty optional} if not found 213 */ 214 public Optional<SearchField> getSearchField(Collection<String> contentTypes, String fieldPath) 215 { 216 SearchField searchField = null; 217 218 String[] pathSegments = StringUtils.split(fieldPath, ContentConstants.METADATA_PATH_SEPARATOR); 219 String fieldName = pathSegments[0]; 220 221 // System property ? 222 if (_sysPropEP.hasExtension(fieldName) && pathSegments.length == 1) 223 { 224 return Optional.ofNullable(_sysPropEP.getExtension(fieldName).getSearchField()); 225 } 226 227 String commonContentTypeId = _cTypeHelper.getCommonAncestor(contentTypes); 228 229 MetadataType type = null; 230 MetadataDefinition def = null; 231 232 if (commonContentTypeId != null) 233 { 234 235 // Indexing field. 236 ContentType cType = _cTypeEP.getExtension(commonContentTypeId); 237 IndexingField indexingField = cType.getIndexingModel().getField(fieldName); 238 239 if (indexingField == null) 240 { 241 throw new IllegalArgumentException("Search field with path '" + fieldPath + "' refers to an unknown indexing field: " + fieldName); 242 } 243 244 if (indexingField instanceof MetadataIndexingField) 245 { 246 List<String> joinPaths = new ArrayList<>(); 247 String[] remainingPathSegments = pathSegments.length > 1 ? (String[]) ArrayUtils.subarray(pathSegments, 1, pathSegments.length) : new String[0]; 248 249 searchField = _getSearchField((MetadataIndexingField) indexingField, remainingPathSegments, joinPaths, true); 250 def = ((MetadataIndexingField) indexingField).getMetadataDefinition(); 251 } 252 else if (indexingField instanceof CustomIndexingField) 253 { 254 type = indexingField.getType(); 255 } 256 } 257 else if (fieldPath.equals("title")) 258 { 259 // No specific content type: allow only title. 260 type = ContentTypesHelper.getTitleMetadataDefinition().getType(); 261 } 262 263 if (searchField == null) 264 { 265 searchField = type != null ? getMetadataSearchField(fieldPath, type, isTitleMultilingual(def)) : null; 266 } 267 268 return Optional.ofNullable(searchField); 269 } 270 271 /** 272 * Get the search field from the indexing field and compute the join paths. Can be null if the last indexing field is a custom indexing field. 273 * @param indexingField The initial indexing field 274 * @param remainingPathSegments The path to access the metadata or an another indexing field from the initial indexing field 275 * @param joinPaths The consecutive's path in case of joint to access the field/metadata 276 * @param addLast <code>true</code> to add the last join path element to the list, <code>false</code> otherwise. 277 * @return The search field or null if not found 278 */ 279 protected SearchField _getSearchField(MetadataIndexingField indexingField, String[] remainingPathSegments, List<String> joinPaths, boolean addLast) 280 { 281 StringBuilder currentMetaPath = new StringBuilder(); 282 currentMetaPath.append(indexingField.getName()); 283 284 MetadataDefinition definition = indexingField.getMetadataDefinition(); 285 286 for (int i = 0; i < remainingPathSegments.length && definition != null; i++) 287 { 288 String currentPathSegment = remainingPathSegments[i]; 289 if (definition.getType() == MetadataType.CONTENT || definition.getType() == MetadataType.SUB_CONTENT) 290 { 291 // Add path to content from current content type to join paths. 292 // Join paths are the consecutive metadata paths (separated with '/') to access 293 // the searched content, for instance [address/city, links/department]. 294 joinPaths.add(currentMetaPath.toString()); 295 296 String refCTypeId = definition.getContentType(); 297 if (refCTypeId != null) 298 { 299 if (!_cTypeEP.hasExtension(refCTypeId)) 300 { 301 throw new IllegalArgumentException("Search criterion with path '" + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR) + "' references an unknown content type:" + refCTypeId); 302 } 303 304 ContentType refCType = _cTypeEP.getExtension(refCTypeId); 305 IndexingModel refIndexingModel = refCType.getIndexingModel(); 306 307 IndexingField refIndexingField = refIndexingModel.getField(currentPathSegment); 308 if (refIndexingField == null && _sysPropEP.hasExtension(currentPathSegment)) 309 { 310 SystemProperty sysProperty = _sysPropEP.getExtension(currentPathSegment); 311 return new JoinedSystemSearchField(joinPaths, sysProperty.getSearchField()); 312 } 313 else if (refIndexingField == null) 314 { 315 throw new IllegalArgumentException("Search criterion with path '" + StringUtils.join(remainingPathSegments, ContentConstants.METADATA_PATH_SEPARATOR) + "' refers to an unknown indexing field: " + currentPathSegment); 316 } 317 318 return _getSearchField((MetadataIndexingField) refIndexingField, ArrayUtils.subarray(remainingPathSegments, i + 1, remainingPathSegments.length), joinPaths, addLast); 319 } 320 else if ("title".equals(currentPathSegment)) 321 { 322 // No specific content type: allow only title. 323 return _getSearchField(ContentTypesHelper.getTitleMetadataDefinition(), joinPaths); 324 } 325 } 326 else 327 { 328 if (definition instanceof RepeaterDefinition) 329 { 330 // Add path to repeater from current content type or last repeater to join paths 331 joinPaths.add(currentMetaPath.toString()); 332 currentMetaPath = new StringBuilder(); 333 currentMetaPath.append(currentPathSegment); 334 } 335 else 336 { 337 currentMetaPath.append(ContentConstants.METADATA_PATH_SEPARATOR).append(currentPathSegment); 338 } 339 definition = definition.getMetadataDefinition(currentPathSegment); 340 } 341 } 342 343 if (addLast) 344 { 345 joinPaths.add(currentMetaPath.toString()); 346 } 347 348 return _getSearchField(definition, joinPaths); 349 } 350 351 private SearchField _getSearchField(MetadataDefinition def, List<String> joinPaths) 352 { 353 MetadataType type = def.getType(); 354 355 String metadataPath = joinPaths.remove(joinPaths.size() - 1); 356 return type != null ? getMetadataSearchField(joinPaths, metadataPath, type, isTitleMultilingual(def)) : null; 357 } 358 359 /** 360 * Determines if the given metadata definition represents a CONTENT metadata with contents with multilingual titles 361 * @param def The metadata definition 362 * @return <code>true</code> if the given metadata definition represents a CONTENT metadata with contents with multilingual titles 363 */ 364 public boolean isTitleMultilingual(MetadataDefinition def) 365 { 366 boolean isTitleMultilingual = Optional.ofNullable(def) 367 .map(MetadataDefinition::getContentType) 368 .map(_cTypeEP::getExtension) 369 .map(cType -> cType.getMetadataDefinition(DefaultContent.METADATA_TITLE)) 370 .map(MetadataDefinition::getType) 371 .map(MetadataType.MULTILINGUAL_STRING::equals) 372 .orElse(false); 373 return isTitleMultilingual; 374 } 375 376 /** 377 * Get metadata values by their path 378 * @param contentId The id of content 379 * @param metadataPath The path of metadata to retrieve 380 * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null. 381 * @return The metadata values 382 */ 383 @Callable 384 public Map<String, Object> getMetadataValues(String contentId, Collection<String> metadataPath, Locale defaultLocale) 385 { 386 Content content = _resolver.resolveById(contentId); 387 return getMetadataValues(content, metadataPath, defaultLocale); 388 } 389 390 /** 391 * Get metadata values by their path 392 * @param content The initial content 393 * @param metadataPaths The path of metadata to retrieve, slash-separated. 394 * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null. 395 * @return The metadata values 396 */ 397 public Map<String, Object> getMetadataValues(Content content, Collection<String> metadataPaths, Locale defaultLocale) 398 { 399 Map<String, Object> values = new HashMap<>(); 400 401 for (String path : metadataPaths) 402 { 403 Object value = getMetadataValue(content, path, defaultLocale); 404 values.put(path, value); 405 } 406 407 return values; 408 } 409 410 /** 411 * Get the value(s) of a metadata - transformed into JSONified value - of a content at given path. 412 * 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'. 413 * The returned value is typed. 414 * @param content The content 415 * @param metadataPath The path to the metadata, separated by '/' 416 * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null. 417 * @return The final value. 418 */ 419 public Object getMetadataValue(Content content, String metadataPath, Locale defaultLocale) 420 { 421 return getMetadataValue(content, metadataPath, defaultLocale, false); 422 } 423 424 /** 425 * Get the value(s) of a metadata - transformed into JSONified value - of a content at given path. 426 * 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'. 427 * @param content The content 428 * @param metadataPath The path to the metadata, separated by '/' 429 * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null. 430 * @param resolveReferences <code>true</code> true to resolve references 431 * @return The final value, tranformed for search. 432 */ 433 public Object getMetadataValue(Content content, String metadataPath, Locale defaultLocale, boolean resolveReferences) 434 { 435 MetadataDefinition metadataDefinition = _contentTypesHelper.getMetadataDefinition(metadataPath, content); 436 if (metadataDefinition != null && metadataDefinition instanceof RepeaterDefinition) 437 { 438 String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR); 439 440 MetadataDefinition definition = _contentTypesHelper.getMetadataDefinition(pathSegments[0], content); 441 if (definition != null) 442 { 443 return _getRepeaterValues(content, content.getMetadataHolder(), definition, "", metadataPath, defaultLocale, resolveReferences); 444 } 445 446 return null; 447 } 448 else 449 { 450 Object value = _contentHelper.getMetadataValue(content, metadataPath, defaultLocale, resolveReferences); 451 return _transformValue(content, metadataPath, value, null, defaultLocale); 452 } 453 } 454 455 /** 456 * Get the value(s) of a metadata - transformed into JSONified value - of a content at given path. 457 * 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'. 458 * @param content The initial content. 459 * @param fullMetadataPath The path to the metadata, separated by '/' 460 * @param type The metadata type. 461 * @param multiple <code>true</code> if the metadata is multiple. 462 * @param enumerator The metadata enumerator. Can be null. 463 * @param defaultLocale The default locale to resolve multilingual values if the content's language is null. Can be null. 464 * @param full <code>true</code> to generate full representation of metadata's values : the references will be resolved and the label of enumerated values will be returned. 465 * @return The final value, tranformed for search. 466 */ 467 public Object getMetadataValue(Content content, String fullMetadataPath, MetadataType type, boolean multiple, Enumerator enumerator, Locale defaultLocale, boolean full) 468 { 469 // If full, extract content or resource values as Maps. If not, the values are kept as content/resources IDs. 470 // If full, extract enumerated values as Maps of <key,label>. If not, the values are kept as entry IDs. 471 472 MetadataDefinition metadataDefinition = _contentTypesHelper.getMetadataDefinition(fullMetadataPath, content); 473 if (metadataDefinition != null && metadataDefinition instanceof RepeaterDefinition) 474 { 475 String[] pathSegments = fullMetadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR); 476 MetadataDefinition definition = pathSegments.length > 1 ? _contentTypesHelper.getMetadataDefinition(pathSegments[0], content) : metadataDefinition; 477 return _getRepeaterValues(content, content.getMetadataHolder(), definition, fullMetadataPath, fullMetadataPath, defaultLocale, full); 478 } 479 else 480 { 481 Object rawValue = _contentHelper.getMetadataValue(content, fullMetadataPath, defaultLocale, full); 482 483 if (!multiple && rawValue instanceof Collection<?>) 484 { 485 if (((Collection<?>) rawValue).iterator().hasNext()) 486 { 487 rawValue = ((Collection<?>) rawValue).iterator().next(); 488 } 489 else 490 { 491 rawValue = null; 492 } 493 } 494 495 return _transformValue(content, fullMetadataPath, rawValue, full ? enumerator : null, defaultLocale); 496 } 497 } 498 499 /** 500 * Transform the raw value into a JSONified value, understandable for search UI 501 * @param content the content 502 * @param metadataPath The path to the metadata, separated by '/' 503 * @param rawValue The typed value 504 * @param enumerator The enumerator. Can be null 505 * @param defaultLocale Locale to use for localized values if content's language is null. 506 * @return The transformed value 507 */ 508 public Object _transformValue(Content content, String metadataPath, Object rawValue, Enumerator enumerator, Locale defaultLocale) 509 { 510 if (rawValue == null) 511 { 512 return null; 513 } 514 else if (rawValue instanceof Resource) 515 { 516 return _transformValue((Resource) rawValue); 517 } 518 else if (rawValue instanceof Content) 519 { 520 return _transformValue(content, (Content) rawValue, defaultLocale); 521 } 522 else if (rawValue instanceof UserIdentity) 523 { 524 return _transformValue((UserIdentity) rawValue); 525 } 526 else if (rawValue instanceof BinaryMetadata) 527 { 528 return _transformValue(content, metadataPath, (BinaryMetadata) rawValue); 529 } 530 else if (rawValue instanceof RichText) 531 { 532 return _transformValue((RichText) rawValue); 533 } 534 else if (rawValue instanceof MultilingualString) 535 { 536 return _transformValue((MultilingualString) rawValue); 537 } 538 else if (rawValue instanceof Collection<?>) 539 { 540 List<Object> tranformedValues = new ArrayList<>(); 541 for (Object object : (Collection<?>) rawValue) 542 { 543 tranformedValues.add(_transformValue(content, metadataPath, object, enumerator, defaultLocale)); 544 } 545 return tranformedValues; 546 } 547 else if (rawValue instanceof Date) 548 { 549 return DateUtils.dateToString((Date) rawValue); 550 } 551 else if (enumerator != null) 552 { 553 Map<String, Object> tranformedValue = new HashMap<>(); 554 tranformedValue.put("value", rawValue); 555 556 try 557 { 558 tranformedValue.put("label", enumerator.getEntry(rawValue.toString())); 559 } 560 catch (Exception e) 561 { 562 tranformedValue.put("label", rawValue); 563 } 564 565 return tranformedValue; 566 } 567 else 568 { 569 return rawValue; 570 } 571 } 572 573 private Object _transformValue(Resource resource) 574 { 575 Map<String, Object> info = new HashMap<>(); 576 577 info.put("type", "explorer"); 578 info.put("id", resource.getId()); 579 info.put("filename", resource.getName()); 580 info.put("mime-type", resource.getMimeType()); 581 info.put("size", resource.getLength()); 582 info.put("lastModified", resource.getLastModified()); 583 info.put("viewUrl", ResolveURIComponent.resolve("explorer", resource.getId(), false)); 584 info.put("downloadUrl", ResolveURIComponent.resolve("explorer", resource.getId(), true)); 585 586 return info; 587 } 588 589 private Object _transformValue(Content content, Content value, Locale defaultLocale) 590 { 591 Map<String, Object> info = new HashMap<>(); 592 593 Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale; 594 info.put("id", value.getId()); 595 info.put("title", value.getTitle(locale)); 596 info.put("isSimple", _contentHelper.isSimple(value)); 597 598 return info; 599 } 600 601 private Object _transformValue(UserIdentity user) 602 { 603 return _userHelper.user2json(user, true); 604 } 605 606 private Object _transformValue(MultilingualString multilingualString) 607 { 608 return MultilingualStringHelper.toJson(multilingualString); 609 } 610 611 private Object _transformValue(Content content, String metadataPath, BinaryMetadata binary) 612 { 613 Map<String, Object> info = new HashMap<>(); 614 615 info.put("type", "metadata"); 616 info.put("id", EditContentFunction.UNTOUCHED_BINARY); 617 info.put("mime-type", binary.getMimeType()); 618 info.put("filename", binary.getFilename()); 619 info.put("size", binary.getLength()); 620 info.put("lastModified", binary.getLastModified()); 621 info.put("viewUrl", ResolveURIComponent.resolve("metadata", metadataPath + "?objectId=" + content.getId(), false)); 622 info.put("downloadUrl", ResolveURIComponent.resolve("metadata", metadataPath + "?objectId=" + content.getId(), true)); 623 624 return info; 625 } 626 627 private Object _transformValue(RichText richText) 628 { 629 Map<String, Object> info = new HashMap<>(); 630 631 info.put("type", "metadata"); 632 info.put("mime-type", richText.getMimeType()); 633 info.put("size", richText.getLength()); 634 info.put("lastModified", richText.getLastModified()); 635 636 SAXParser saxParser = null; 637 try (InputStream is = richText.getInputStream()) 638 { 639 RichTextHandler txtHandler = new RichTextHandler(100); 640 saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE); 641 saxParser.parse(new InputSource(is), txtHandler); 642 643 String excerpt = txtHandler.getValue(); 644 info.put("content", excerpt); 645 } 646 catch (Exception e) 647 { 648 getLogger().error("Cannot extract excerpt of a RichText value ", e); 649 info.put("content", ""); 650 } 651 finally 652 { 653 _manager.release(saxParser); 654 } 655 656 return info; 657 } 658 659 private Object _getRepeaterValues(Content content, CompositeMetadata metadataHolder, MetadataDefinition definition, String prefix, String metadataPath, Locale defaultLocale, boolean resolveReferences) 660 { 661 String[] pathSegments = metadataPath.split(ContentConstants.METADATA_PATH_SEPARATOR); 662 663 String metadataName = pathSegments[0]; 664 665 if (!metadataHolder.hasMetadata(metadataName) || definition == null) 666 { 667 return null; 668 } 669 670 MetadataType type = definition.getType(); 671 switch (type) 672 { 673 case COMPOSITE: 674 return _getCompositeRepeaterValues(content, metadataHolder, definition, defaultLocale, resolveReferences, pathSegments, prefix, metadataName); 675 676 case CONTENT: 677 return _getContentRepeaterValues(content, metadataHolder, definition, metadataPath, defaultLocale, resolveReferences, pathSegments, metadataName); 678 679 default: 680 if (pathSegments.length == 1) 681 { 682 Locale locale = content.getLanguage() != null ? new Locale(content.getLanguage()) : defaultLocale; 683 Object rawValue = _contentHelper.getSimpleMetadataValue(metadataHolder, definition, metadataName, locale, resolveReferences); 684 return _transformValue(content, prefix + ContentConstants.METADATA_PATH_SEPARATOR + metadataPath, rawValue, null, defaultLocale); 685 } 686 687 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)); 688 } 689 690 } 691 692 private Object _getContentRepeaterValues(Content content, CompositeMetadata metadataHolder, MetadataDefinition definition, String metadataPath, Locale defaultLocale, 693 boolean resolveReferences, String[] pathSegments, String metadataName) 694 { 695 if (pathSegments.length > 1) 696 { 697 if (definition.isMultiple()) 698 { 699 List<Object> values = new ArrayList<>(); 700 String[] refContentIds = metadataHolder.getStringArray(metadataName, new String[0]); 701 for (String refContentId : refContentIds) 702 { 703 Content refContent = _resolver.resolveById(refContentId); 704 Object remoteValue = getMetadataValue(refContent, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), defaultLocale, resolveReferences); 705 if (remoteValue != null && remoteValue instanceof Collection<?>) 706 { 707 values.addAll((Collection<?>) remoteValue); 708 } 709 else if (remoteValue != null) 710 { 711 values.add(remoteValue); 712 } 713 } 714 return values; 715 } 716 else 717 { 718 String refContentId = metadataHolder.getString(metadataName); 719 Content refContent = _resolver.resolveById(refContentId); 720 Object rawValue = getMetadataValue(refContent, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), defaultLocale, resolveReferences); 721 return _transformValue(content, metadataPath, rawValue, null, defaultLocale); 722 } 723 } 724 else 725 { 726 Object rawValue = _contentHelper.getSimpleMetadataValue(metadataHolder, definition, metadataName, defaultLocale, resolveReferences); 727 return _transformValue(content, metadataPath, rawValue, null, defaultLocale); 728 } 729 } 730 731 private Object _getCompositeRepeaterValues(Content content, CompositeMetadata metadataHolder, MetadataDefinition definition, Locale locale, boolean resolveReferences, String[] pathSegments, String prefix, String metadataName) 732 { 733 if (definition instanceof RepeaterDefinition) 734 { 735 return _getRepeaterRepeaterValue(content, metadataHolder, definition, locale, resolveReferences, pathSegments, prefix, metadataName); 736 } 737 else 738 { 739 // Composite. 740 CompositeMetadata subMetadataHolder = metadataHolder.getCompositeMetadata(metadataName); 741 if (pathSegments.length > 1) 742 { 743 MetadataDefinition subMetadataDef = definition.getMetadataDefinition(pathSegments[1]); 744 return _getRepeaterValues(content, subMetadataHolder, subMetadataDef, prefix, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences); 745 } 746 else 747 { 748 Map<String, Object> values = new HashMap<>(); 749 for (String subMetadataName : subMetadataHolder.getMetadataNames()) 750 { 751 MetadataDefinition submetadataDefinition = definition.getMetadataDefinition(subMetadataName); 752 Object entryValue = _getRepeaterValues(content, subMetadataHolder, submetadataDefinition, prefix, subMetadataName, locale, resolveReferences); 753 if (entryValue != null) 754 { 755 if (submetadataDefinition != null && submetadataDefinition.getType().equals(MetadataType.COMPOSITE) && !(submetadataDefinition instanceof RepeaterDefinition) && entryValue instanceof Map) 756 { 757 // add and prefix each entry of the composite 758 @SuppressWarnings("unchecked") 759 Map<String, Object> map = (Map<String, Object>) entryValue; 760 values.putAll(map.entrySet().stream().collect(Collectors.toMap(e -> metadataName + ContentConstants.METADATA_PATH_SEPARATOR + e.getKey(), e -> e.getValue()))); 761 } 762 else 763 { 764 values.put(metadataName + ContentConstants.METADATA_PATH_SEPARATOR + subMetadataName, entryValue); 765 } 766 } 767 } 768 return values; 769 } 770 } 771 } 772 773 @SuppressWarnings("unchecked") 774 private Object _getRepeaterRepeaterValue(Content content, CompositeMetadata metadataHolder, MetadataDefinition definition, Locale locale, boolean resolveReferences, String[] pathSegments, String prefix, String metadataName) 775 { 776 // Repeater: get and sort the entry names. 777 CompositeMetadata repeater = metadataHolder.getCompositeMetadata(metadataName); 778 String[] entries = repeater.getMetadataNames(); 779 Arrays.sort(entries, MetadataManager.REPEATER_ENTRY_COMPARATOR); 780 781 782 if (pathSegments.length > 1) 783 { 784 Map<String, Object> values = new HashMap<>(); 785 for (String entryName : entries) 786 { 787 CompositeMetadata entry = repeater.getCompositeMetadata(entryName); 788 789 Object entryValue = _getRepeaterValues(content, entry, definition.getMetadataDefinition(pathSegments[1]), prefix + ContentConstants.METADATA_PATH_SEPARATOR + entryName, StringUtils.join(pathSegments, ContentConstants.METADATA_PATH_SEPARATOR, 1, pathSegments.length), locale, resolveReferences); 790 if (entryValue != null) 791 { 792 values.put(_i18nUtils.translate(((RepeaterDefinition) definition).getLabel(), locale.getLanguage()), entryValue); 793 } 794 } 795 796 return values; 797 } 798 else 799 { 800 List<Map<String, Object>> values = new LinkedList<>(); 801 802 int i = 0; 803 for (String entryName : entries) 804 { 805 i++; 806 CompositeMetadata entry = repeater.getCompositeMetadata(entryName); 807 808 Map<String, Object> compositeValues = new HashMap<>(); 809 for (String subMetadataName : entry.getMetadataNames()) 810 { 811 MetadataDefinition submetadataDefinition = definition.getMetadataDefinition(subMetadataName); 812 Object entryValue = _getRepeaterValues(content, entry, submetadataDefinition, prefix + ContentConstants.METADATA_PATH_SEPARATOR + entryName, subMetadataName, locale, resolveReferences); 813 if (entryValue != null) 814 { 815 if (submetadataDefinition != null && submetadataDefinition.getType().equals(MetadataType.COMPOSITE) && !(submetadataDefinition instanceof RepeaterDefinition) && entryValue instanceof Map) 816 { 817 // if submetadata was a composite, add all composite child as a flat list of values 818 compositeValues.putAll((Map< ? extends String, ? extends Object>) entryValue); 819 } 820 else 821 { 822 compositeValues.put(subMetadataName, entryValue); 823 } 824 } 825 } 826 827 Map<String, Object> repeaterValues = new HashMap<>(); 828 repeaterValues.put("label", ((RepeaterDefinition) definition).getLabel()); 829 repeaterValues.put("position", i); 830 repeaterValues.put("values", compositeValues); 831 String headerLabel = ((RepeaterDefinition) definition).getHeaderLabel(); 832 if (headerLabel != null) 833 { 834 repeaterValues.put("header-label", headerLabel); 835 } 836 values.add(repeaterValues); 837 } 838 839 return values; 840 } 841 } 842}