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