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