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