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.util.ArrayList; 019import java.util.Collection; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Locale; 023import java.util.Map; 024import java.util.Optional; 025import java.util.Set; 026 027import org.apache.avalon.framework.component.Component; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.commons.lang3.StringUtils; 032 033import org.ametys.cms.data.ContentValue; 034import org.ametys.cms.data.ametysobject.IndexableAmetysObject; 035import org.ametys.cms.data.holder.impl.IndexableDataHolderHelper; 036import org.ametys.cms.data.type.ModelItemTypeConstants; 037import org.ametys.cms.model.CMSDataContext; 038import org.ametys.cms.model.properties.Property; 039import org.ametys.cms.repository.Content; 040import org.ametys.cms.search.model.SearchModel; 041import org.ametys.cms.search.model.SystemProperty; 042import org.ametys.cms.search.ui.model.SearchUIColumn; 043import org.ametys.cms.search.ui.model.SearchUIColumnHelper; 044import org.ametys.cms.search.ui.model.impl.RepeaterSearchUIColumn; 045import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 046import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint; 047import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 048import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite; 049import org.ametys.plugins.repository.data.holder.group.Repeater; 050import org.ametys.plugins.repository.data.holder.group.RepeaterEntry; 051import org.ametys.plugins.repository.data.holder.impl.DataHolderExportHelper; 052import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 053import org.ametys.plugins.repository.model.CompositeDefinition; 054import org.ametys.plugins.repository.model.RepeaterDefinition; 055import org.ametys.plugins.repository.model.RepeaterViewItem; 056import org.ametys.plugins.repository.model.RepositoryDataContext; 057import org.ametys.plugins.repository.model.ViewHelper; 058import org.ametys.runtime.model.ElementDefinition; 059import org.ametys.runtime.model.Model; 060import org.ametys.runtime.model.ModelHelper; 061import org.ametys.runtime.model.ModelItem; 062import org.ametys.runtime.model.ModelItemGroup; 063import org.ametys.runtime.model.ModelViewItem; 064import org.ametys.runtime.model.ModelViewItemGroup; 065import org.ametys.runtime.model.View; 066import org.ametys.runtime.model.ViewElement; 067import org.ametys.runtime.model.ViewElementAccessor; 068import org.ametys.runtime.model.ViewItemAccessor; 069import org.ametys.runtime.model.ViewItemContainer; 070import org.ametys.runtime.model.exception.BadItemTypeException; 071import org.ametys.runtime.model.exception.UndefinedItemPathException; 072import org.ametys.runtime.model.type.DataContext; 073import org.ametys.runtime.model.type.ModelItemType; 074import org.ametys.runtime.plugin.component.AbstractLogEnabled; 075 076/** 077 * Component creating content values extractors from {@link SearchModel}s or {@link View}s. 078 */ 079public class ContentValuesExtractorFactory extends AbstractLogEnabled implements Component, Serviceable 080{ 081 /** The component role. */ 082 public static final String ROLE = ContentValuesExtractorFactory.class.getName(); 083 084 /** The content search helper. */ 085 protected ContentSearchHelper _searchHelper; 086 087 /** To determine the externalizable status */ 088 protected ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP; 089 090 /** The helper to export data holder values as JSON */ 091 protected DataHolderExportHelper _dataHolderExportHelper; 092 093 @Override 094 public void service(ServiceManager manager) throws ServiceException 095 { 096 _searchHelper = (ContentSearchHelper) manager.lookup(ContentSearchHelper.ROLE); 097 _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE); 098 _dataHolderExportHelper = (DataHolderExportHelper) manager.lookup(DataHolderExportHelper.ROLE); 099 } 100 101 /** 102 * Create a ContentValuesExtractor from a search model. 103 * @param searchModel The reference search model. 104 * @return a ContentValuesExtractor backed by the given search model. 105 */ 106 public SearchModelContentValuesExtractor create(SearchModel searchModel) 107 { 108 return new SearchModelContentValuesExtractor(searchModel); 109 } 110 111 /** 112 * Create a simple {@link ContentValuesExtractor} from a view 113 * @param view The view. 114 * @return The created {@link ContentValuesExtractor} 115 */ 116 public SimpleContentValuesExtractor create(View view) 117 { 118 return new SimpleContentValuesExtractor(view); 119 } 120 121 /** 122 * A ContentValuesExtractor 123 */ 124 public interface ContentValuesExtractor 125 { 126 /** 127 * Get the values from the given content. 128 * @param content The content. 129 * @param defaultLocale The default locale for localized values if the content's language is null. Can be null. 130 * @param contextualParameters The search contextual parameters. 131 * @return the extracted values. 132 */ 133 public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters); 134 135 /** 136 * Get the default values of external disable conditions for repeaters in the extractor's items 137 * These default values are used to initialize disable status of new repeater entries 138 * @param content The content. 139 * @param contextualParameters The search contextual parameters. 140 * @return the default values of external disable conditions for repeaters 141 */ 142 public Map<String, Object> getRepeatersDefaultExternalDisableConditionValues(Content content, Map<String, Object> contextualParameters); 143 } 144 145 /** 146 * An abstract implementation of ContentValuesExtractor 147 */ 148 public abstract class AbstractContentValuesExtractor implements ContentValuesExtractor 149 { 150 public Map<String, Object> getValues(Content content, Locale defaultLocale, Map<String, Object> contextualParameters) 151 { 152 CMSDataContext context = CMSDataContext.newInstance() 153 .withRichTextMaxLength(100) 154 .withObject(content) 155 .withLocale(defaultLocale) 156 .withEmptyValues(false); 157 158 boolean handleExternalizable = (boolean) contextualParameters.getOrDefault("externalizable", true); 159 if (handleExternalizable) 160 { 161 Set<String> externalizableData = _externalizableDataProviderEP.getExternalizableDataPaths(content); 162 context.withExternalizableData(externalizableData); 163 } 164 165 ViewItemContainer viewItemContainer = _getResultItems(content.getModel(), contextualParameters); 166 Map<String, Object> json = _dataToJSON(content, viewItemContainer, context, StringUtils.EMPTY, new HashMap<>()); 167 168 return json; 169 } 170 171 public Map<String, Object> getRepeatersDefaultExternalDisableConditionValues(Content content, Map<String, Object> contextualParameters) 172 { 173 DataContext context = RepositoryDataContext.newInstance() 174 .withObject(content); 175 176 ViewItemContainer viewItemContainer = _getResultItems(content.getModel(), contextualParameters); 177 Map<String, Object> json = _getRepeatersDefaultExternalDisableConditionValues(content, viewItemContainer, context, StringUtils.EMPTY); 178 179 return json; 180 } 181 182 /** 183 * Retrieves the view to use to extract values 184 * @param model the model 185 * @param contextualParameters The search contextual parameters. 186 * @return the {@link View} to use to extract values 187 */ 188 protected abstract ViewItemContainer _getResultItems(Collection<? extends Model> model, Map<String, Object> contextualParameters); 189 } 190 191 /** 192 * A ContentValuesExtractor backed by a {@link SearchModel}. 193 */ 194 public class SearchModelContentValuesExtractor extends AbstractContentValuesExtractor 195 { 196 private SearchModel _searchModel; 197 198 /** 199 * Build a ContentValuesExtractor referencing a {@link SearchModel}. 200 * @param searchModel the {@link SearchModel}. 201 */ 202 public SearchModelContentValuesExtractor(SearchModel searchModel) 203 { 204 _searchModel = searchModel; 205 } 206 207 @Override 208 protected ViewItemContainer _getResultItems(Collection< ? extends Model> model, Map<String, Object> contextualParameters) 209 { 210 return _searchModel.getResultItems(contextualParameters); 211 } 212 } 213 214 /** 215 * A simple ContentValuesExtractor on a list of content types. 216 */ 217 public class SimpleContentValuesExtractor extends AbstractContentValuesExtractor 218 { 219 private ViewItemContainer _resultItems; 220 221 /** 222 * Build a simple {@link ContentValuesExtractor} from the given result items 223 * @param resultItems The result items 224 * @throws IllegalArgumentException if the given result items contain a composite as leaf, or a {@link SystemProperty} that is not displayable 225 */ 226 public SimpleContentValuesExtractor(ViewItemContainer resultItems) throws IllegalArgumentException 227 { 228 _resultItems = resultItems; 229 } 230 231 @Override 232 protected ViewItemContainer _getResultItems(Collection< ? extends Model> model, Map<String, Object> contextualParameters) 233 { 234 return _resultItems; 235 } 236 } 237 238 private Map<String, Object> _getRepeatersDefaultExternalDisableConditionValues(Content content, ViewItemAccessor viewItemAccessor, DataContext context, String prefix) throws BadItemTypeException, UndefinedItemPathException 239 { 240 Map<String, Object> result = new HashMap<>(); 241 242 ViewHelper.visitView(viewItemAccessor, 243 (element, definition) -> { 244 // simple element - Do nothing 245 }, 246 (group, definition) -> { 247 // composite 248 String name = definition.getName(); 249 String newDataPath = prefix + name; 250 251 DataContext newContext = CMSDataContext.newInstance(context) 252 .addSegmentToDataPath(name) 253 .withViewItem(group) 254 .withModelItem(definition); 255 256 result.putAll(_getRepeatersDefaultExternalDisableConditionValues(content, group, newContext, newDataPath + ModelItem.ITEM_PATH_SEPARATOR)); 257 }, 258 (group, definition) -> { 259 // repeater 260 String name = definition.getName(); 261 String newDataPath = prefix + name; 262 263 DataContext repeaterContext = CMSDataContext.newInstance(context) 264 .addSegmentToDataPath(name) 265 .withViewItem(group) 266 .withModelItem(definition); 267 268 Map<String, Boolean> conditionsValues = DataHolderHelper.getExternalDisableConditionsValues(content, group, repeaterContext); 269 if (!conditionsValues.isEmpty()) 270 { 271 result.put(newDataPath, conditionsValues); 272 } 273 274 result.putAll(_getRepeatersDefaultExternalDisableConditionValues(content, group, repeaterContext, newDataPath + ModelItem.ITEM_PATH_SEPARATOR)); 275 }, 276 group -> result.putAll(_getRepeatersDefaultExternalDisableConditionValues(content, group, context, prefix))); 277 278 return result; 279 } 280 281 @SuppressWarnings("unchecked") 282 private Map<String, Object> _dataToJSON(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext context, String prefix, Map<String, Content> resolvedContents) throws BadItemTypeException 283 { 284 Map<String, Object> result = new HashMap<>(); 285 286 ViewHelper.visitView(viewItemAccessor, 287 (element, definition) -> { 288 // simple element 289 String name = definition.getName(); 290 String newDataPath = prefix + name; 291 292 DataContext newContext = CMSDataContext.newInstance(context) 293 .addSegmentToDataPath(name) 294 .withViewItem(element) 295 .withModelItem(definition); 296 297 result.putAll(_elementToJSON(dataHolder, element, definition, newDataPath, newContext, resolvedContents)); 298 }, 299 (group, definition) -> { 300 // composite 301 String name = definition.getName(); 302 String newDataPath = prefix + name; 303 304 DataContext newContext = CMSDataContext.newInstance(context) 305 .addSegmentToDataPath(name) 306 .withViewItem(group) 307 .withModelItem(definition); 308 309 result.putAll(_compositeToJSON(dataHolder, group, definition, newDataPath, newContext, resolvedContents)); 310 }, 311 (group, definition) -> { 312 // repeater 313 String name = definition.getName(); 314 String newDataPath = prefix + name; 315 316 DataContext repeaterContext = CMSDataContext.newInstance(context) 317 .addSegmentToDataPath(name) 318 .withViewItem(group) 319 .withModelItem(definition); 320 321 result.putAll(_repeaterToJSON(dataHolder, group, definition, newDataPath, repeaterContext, resolvedContents)); 322 }, 323 group -> result.putAll(_dataToJSON(dataHolder, group, context, prefix, resolvedContents))); 324 325 326 Map<String, Boolean> conditionsValues = DataHolderHelper.getExternalDisableConditionsValues(dataHolder, viewItemAccessor, context); 327 if (!conditionsValues.isEmpty()) 328 { 329 result.put(DataHolderHelper.EXTERNAL_DISABLE_CONDITIONS_VALUES, conditionsValues); 330 } 331 332 return result; 333 } 334 335 private Map<String, Object> _elementToJSON(ModelAwareDataHolder dataHolder, ViewElement viewElement, ElementDefinition definition, String dataPath, DataContext context, Map<String, Content> resolvedContents) 336 { 337 Map<String, Object> result = new HashMap<>(); 338 String name = definition.getName(); 339 340 if (viewElement instanceof ViewElementAccessor viewElementAccessor 341 && !viewElementAccessor.getViewItems().isEmpty()) 342 { 343 // The view item is an accessor -> convert all its children to JSON 344 if (dataHolder.hasValue(name) && ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(definition.getType().getId())) 345 { 346 Map<String, Object> contentJSON = definition.isMultiple() 347 ? _multipleContentToJSON(dataHolder, viewElementAccessor, name, dataPath, context, resolvedContents) 348 : _singleContentToJSON(dataHolder, viewElementAccessor, name, dataPath, context, resolvedContents); 349 350 result.putAll(contentJSON); 351 } 352 } 353 else if (definition instanceof Property property) 354 { 355 IndexableAmetysObject ametysObject = IndexableDataHolderHelper.getAmetysObjectFromContext(context); 356 @SuppressWarnings("unchecked") 357 Object json = property.valueToJSON(ametysObject, context); 358 result.put(dataPath, json); 359 } 360 else 361 { 362 if (((CMSDataContext) context).isDataExternalizable()) 363 { 364 Map<String, Object> json = _dataHolderExportHelper.externalizableValuesAsJson(dataHolder, name, (CMSDataContext) context); 365 if (json.isEmpty()) 366 { 367 // Always send externalizable data status for grids 368 ExternalizableDataStatus status = dataHolder.getStatus(name); 369 json.put("status", status.name().toLowerCase()); 370 } 371 372 result.put(dataPath, json); 373 } 374 else if (dataHolder.hasValue(name)) 375 { 376 Object value = dataHolder.getValue(name); 377 if (ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(definition.getType().getId())) 378 { 379 // Remove view item from context because children must be processed by the current algorithm, not by the type's one 380 context.withViewItem(null); 381 382 // Resolve contents here to use resolved contents cache 383 value = _resolveContentValues(definition, value, resolvedContents); 384 } 385 386 ModelItemType type = definition.getType(); 387 Object json = type.valueToJSONForClient(value, context); 388 result.put(dataPath, json); 389 } 390 } 391 392 return result; 393 } 394 395 @SuppressWarnings("unchecked") 396 private Map<String, Object> _multipleContentToJSON(ModelAwareDataHolder dataHolder, ViewElementAccessor viewElementAccessor, String modelItemName, String dataPath, DataContext context, Map<String, Content> resolvedContents) 397 { 398 Map<String, List<Object>> allDataJSON = new HashMap<>(); // JSON for all data except repeaters 399 Map<String, Object> repeatersJSON = new HashMap<>(); // JSON for repeaters that are leaves 400 401 ContentValue[] conntentValues = dataHolder.getValue(modelItemName); 402 for (ContentValue contentValue : conntentValues) 403 { 404 Content linkedContent = _getResolvedContent(contentValue, resolvedContents); 405 if (linkedContent != null) 406 { 407 DataContext contentContext = CMSDataContext.newInstance(context) 408 .withObject(linkedContent) 409 .withDataPath(StringUtils.EMPTY); 410 411 Map<String, Object> contentJSON = _dataToJSON(linkedContent, viewElementAccessor, contentContext, dataPath + ModelItem.ITEM_PATH_SEPARATOR, resolvedContents); 412 for (String childDataPath : contentJSON.keySet()) 413 { 414 String definitionPath = ModelHelper.getDefinitionPathFromDataPath(childDataPath); 415 Object childJSON = contentJSON.get(childDataPath); 416 if (_isRepeaterJSON(childJSON)) 417 { 418 // The child is a repeater -> merge the entries 419 _mergeRepeaterEntriesJSON(repeatersJSON, (Map<String, Object>) childJSON, definitionPath); 420 } 421 else 422 { 423 // Merge data of all contents 424 List<Object> dataJSON = allDataJSON.computeIfAbsent(definitionPath, __ -> new ArrayList<>()); 425 if (childJSON instanceof List jsonList) 426 { 427 dataJSON.addAll(jsonList); 428 } 429 else 430 { 431 dataJSON.add(childJSON); 432 } 433 } 434 } 435 } 436 } 437 438 Map<String, Object> result = new HashMap<>(); 439 result.putAll(allDataJSON); 440 result.putAll(repeatersJSON); 441 442 return result; 443 } 444 445 private Map<String, Object> _singleContentToJSON(ModelAwareDataHolder dataHolder, ViewElementAccessor viewElementAccessor, String modelItemName, String dataPath, DataContext context, Map<String, Content> resolvedContents) 446 { 447 Map<String, Object> result = new HashMap<>(); 448 449 ContentValue contentValue = dataHolder.getValue(modelItemName); 450 Content linkedContent = _getResolvedContent(contentValue, resolvedContents); 451 452 if (linkedContent != null) 453 { 454 DataContext contentContext = CMSDataContext.newInstance(context) 455 .withObject(linkedContent) 456 .withDataPath(StringUtils.EMPTY); 457 458 Map<String, Object> contentJSON = _dataToJSON(linkedContent, viewElementAccessor, contentContext, dataPath + ModelItem.ITEM_PATH_SEPARATOR, resolvedContents); 459 result.putAll(contentJSON); 460 } 461 462 return result; 463 } 464 465 private Object _resolveContentValues(ElementDefinition definition, Object value, Map<String, Content> resolvedContents) 466 { 467 Object result = value; 468 469 if (definition.isMultiple()) 470 { 471 ContentValue[] contentValues = (ContentValue[]) value; 472 List<Content> contents = new ArrayList<>(); 473 for (ContentValue contentValue : contentValues) 474 { 475 Content content = _getResolvedContent(contentValue, resolvedContents); 476 if (content != null) 477 { 478 contents.add(content); 479 } 480 } 481 482 result = contents.toArray(new Content[contents.size()]); 483 } 484 else 485 { 486 ContentValue contentValue = (ContentValue) value; 487 Content content = _getResolvedContent(contentValue, resolvedContents); 488 result = content != null ? content : result; 489 } 490 491 return result; 492 } 493 494 private Content _getResolvedContent(ContentValue contentValue, Map<String, Content> resolvedContents) 495 { 496 return resolvedContents.computeIfAbsent(contentValue.getContentId(), 497 id -> contentValue.getContentIfExists().orElse(null)); 498 } 499 500 private Map<String, Object> _compositeToJSON(ModelAwareDataHolder dataHolder, ModelViewItemGroup<CompositeDefinition> compositeViewItem, CompositeDefinition compositeDefinition, String dataPath, DataContext context, Map<String, Content> resolvedContents) 501 { 502 Map<String, Object> result = new HashMap<>(); 503 String name = compositeDefinition.getName(); 504 505 if (compositeViewItem.getViewItems().isEmpty()) 506 { 507 throw new IllegalArgumentException("Attribute at path '" + dataPath + "' is a composite: can not invoke #getAttributeValue"); 508 } 509 510 if (dataHolder.hasValue(name)) 511 { 512 ModelAwareComposite value = dataHolder.getValue(name); 513 Map<String, Object> json = _dataToJSON(value, compositeViewItem, context, dataPath + ModelItem.ITEM_PATH_SEPARATOR, resolvedContents); 514 result.putAll(json); 515 } 516 517 return result; 518 } 519 520 private Map<String, Object> _repeaterToJSON(ModelAwareDataHolder dataHolder, ModelViewItemGroup<RepeaterDefinition> repeaterViewItem, RepeaterDefinition repeaterDefinition, String repeaterPath, DataContext context, Map<String, Content> resolvedContents) 521 { 522 Map<String, Object> result = new HashMap<>(); 523 String name = repeaterDefinition.getName(); 524 525 if (dataHolder.hasValue(name)) 526 { 527 Repeater repeater = dataHolder.getValue(name); 528 529 if (repeaterViewItem instanceof SearchUIColumn || repeaterViewItem.getViewItems().isEmpty()) 530 { 531 ModelViewItemGroup<RepeaterDefinition> repeaterLeaf = _getRepeaterLeafViewItem(repeaterViewItem, repeaterDefinition); 532 Map<String, Object> repeaterLeafJSON = _repeaterLeafToJSON(repeater, repeaterLeaf, context, resolvedContents); 533 result.put(repeaterPath, repeaterLeafJSON); 534 } 535 else 536 { 537 Map<String, Object> repeaterJSON = _repeaterToJSON(repeater, repeaterViewItem, repeaterPath, context, resolvedContents); 538 result.putAll(repeaterJSON); 539 } 540 } 541 542 return result; 543 } 544 545 private Map<String, Object> _repeaterLeafToJSON(Repeater repeater, ModelViewItemGroup<RepeaterDefinition> repeaterViewItem, DataContext context, Map<String, Content> resolvedContents) 546 { 547 List<Map<String, Object>> entriesJSON = new ArrayList<>(); 548 for (RepeaterEntry entry : repeater.getEntries()) 549 { 550 DataContext entryContext = CMSDataContext.newInstance(context) 551 .addSuffixToLastSegment("[" + entry.getPosition() + "]"); 552 553 Map<String, Object> entryValuesJson = _dataToJSON(entry, repeaterViewItem, entryContext, StringUtils.EMPTY, resolvedContents); 554 555 Map<String, Object> entryJSON = new HashMap<>(); 556 entryJSON.put("values", entryValuesJson); 557 entryJSON.put("position", entry.getPosition()); 558 559 entriesJSON.add(entryJSON); 560 } 561 562 Map<String, Object> json = new HashMap<>(); 563 json.put("type", org.ametys.plugins.repository.data.type.ModelItemTypeConstants.REPEATER_TYPE_ID); 564 json.put("entries", entriesJSON); 565 json.put("label", repeaterViewItem.getDefinition().getLabel()); 566 567 Optional.ofNullable(repeaterViewItem.getDefinition().getHeaderLabel()) 568 .ifPresent(headerLabel -> json.put("header-label", headerLabel)); 569 570 Map<String, Boolean> conditionsValues = DataHolderHelper.getExternalDisableConditionsValues(repeater.getParentDataHolder(), repeaterViewItem, context); 571 if (!conditionsValues.isEmpty()) 572 { 573 json.put(DataHolderHelper.EXTERNAL_DISABLE_CONDITIONS_VALUES, conditionsValues); 574 } 575 576 return json; 577 } 578 579 private ModelViewItemGroup<RepeaterDefinition> _getRepeaterLeafViewItem(ModelViewItemGroup<RepeaterDefinition> group, RepeaterDefinition definition) 580 { 581 if (group.getViewItems().isEmpty()) 582 { 583 boolean useColumns = group instanceof SearchUIColumn; 584 ModelViewItemGroup<RepeaterDefinition> viewItemGroup = useColumns ? new RepeaterSearchUIColumn() : new RepeaterViewItem(); 585 viewItemGroup.setDefinition(definition); 586 viewItemGroup.setParent(group.getParent()); 587 588 for (ModelItem child : definition.getChildren()) 589 { 590 _addViewItemForRepeaterLeaf(child, viewItemGroup, useColumns); 591 } 592 593 return viewItemGroup; 594 } 595 else 596 { 597 return group; 598 } 599 } 600 601 @SuppressWarnings("unchecked") 602 private void _addViewItemForRepeaterLeaf(ModelItem modelItem, ModelViewItemGroup group, boolean isColumns) 603 { 604 ModelViewItem viewItem; 605 if (modelItem instanceof CompositeDefinition compositeDefinition) 606 { 607 viewItem = new ModelViewItemGroup<>(); 608 609 // Add children only for composites, children of type repeater do not need to have children 610 for (ModelItem child : compositeDefinition.getChildren()) 611 { 612 _addViewItemForRepeaterLeaf(child, (ModelViewItemGroup) viewItem, isColumns); 613 } 614 } 615 else 616 { 617 viewItem = isColumns 618 ? SearchUIColumnHelper.createModelItemColumn(modelItem) 619 : modelItem instanceof ModelItemGroup 620 ? new RepeaterViewItem() 621 : new ViewElement(); 622 } 623 624 viewItem.setDefinition(modelItem); 625 group.addViewItem(viewItem); 626 } 627 628 @SuppressWarnings("unchecked") 629 private Map<String, Object> _repeaterToJSON(Repeater repeater, ModelViewItemGroup<RepeaterDefinition> group, String repeaterPath, DataContext repeaterContext, Map<String, Content> resolvedContents) 630 { 631 Map<String, List<Object>> singleDataJSON = new HashMap<>(); 632 Map<String, Object> repeatersJSON = new HashMap<>(); 633 634 for (RepeaterEntry entry : repeater.getEntries()) 635 { 636 DataContext entryContext = CMSDataContext.newInstance(repeaterContext) 637 .addSuffixToLastSegment("[" + entry.getPosition() + "]"); 638 639 Map<String, Object> entryJson = _dataToJSON(entry, group, entryContext, repeaterPath + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR, resolvedContents); 640 641 for (String entryPath : entryJson.keySet()) 642 { 643 String definitionPath = ModelHelper.getDefinitionPathFromDataPath(entryPath); 644 Object dataJSON = entryJson.get(entryPath); 645 if (_isRepeaterJSON(dataJSON)) 646 { 647 // The child is a repeater -> merge the entries 648 _mergeRepeaterEntriesJSON(repeatersJSON, (Map<String, Object>) dataJSON, definitionPath); 649 } 650 else 651 { 652 // Merge data of all data 653 List<Object> dataJsonList = singleDataJSON.computeIfAbsent(definitionPath, __ -> new ArrayList<>()); 654 if (dataJSON instanceof List jsonList) 655 { 656 dataJsonList.addAll(jsonList); 657 } 658 else 659 { 660 dataJsonList.add(dataJSON); 661 } 662 } 663 } 664 } 665 666 Map<String, Object> result = new HashMap<>(); 667 result.putAll(singleDataJSON); 668 result.putAll(repeatersJSON); 669 670 return result; 671 } 672 673 private boolean _isRepeaterJSON(Object json) 674 { 675 return json instanceof Map map && org.ametys.plugins.repository.data.type.ModelItemTypeConstants.REPEATER_TYPE_ID.equals(map.get("type")); 676 } 677 678 @SuppressWarnings("unchecked") 679 private void _mergeRepeaterEntriesJSON(Map<String, Object> repeatersJSON, Map<String, Object> json, String definitionPath) 680 { 681 if (repeatersJSON.containsKey(definitionPath)) 682 { 683 Map<String, Object> repeaterJSON = (Map<String, Object>) repeatersJSON.get(definitionPath); 684 List<Object> allEntries = (List<Object>) repeaterJSON.get("entries"); 685 List<Object> entries = (List<Object>) json.get("entries"); 686 allEntries.addAll(entries); 687 } 688 else 689 { 690 repeatersJSON.put(definitionPath, json); 691 } 692 } 693}