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}