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