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