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