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}