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