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