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