001/*
002 *  Copyright 2022 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.data.holder.impl;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026
027import org.apache.avalon.framework.activity.Disposable;
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.xml.AttributesImpl;
033import org.apache.cocoon.xml.XMLUtils;
034import org.apache.commons.lang3.StringUtils;
035import org.apache.solr.common.SolrInputDocument;
036import org.xml.sax.ContentHandler;
037import org.xml.sax.SAXException;
038
039import org.ametys.cms.data.ametysobject.ModelAwareDataAwareAmetysObject;
040import org.ametys.cms.data.holder.IndexableDataHolder;
041import org.ametys.cms.data.holder.group.IndexableComposite;
042import org.ametys.cms.data.holder.group.IndexableRepeater;
043import org.ametys.cms.data.type.indexing.IndexableElementType;
044import org.ametys.cms.model.CMSDataContext;
045import org.ametys.cms.model.properties.Property;
046import org.ametys.cms.search.model.IndexationAwareElementDefinition;
047import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
048import org.ametys.core.util.JSONUtils;
049import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
050import org.ametys.plugins.repository.data.holder.DataHolder;
051import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
052import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite;
053import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
054import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
055import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
056import org.ametys.plugins.repository.data.type.RepositoryElementType;
057import org.ametys.plugins.repository.model.CompositeDefinition;
058import org.ametys.plugins.repository.model.RepeaterDefinition;
059import org.ametys.plugins.repository.model.RepositoryDataContext;
060import org.ametys.plugins.repository.model.ViewHelper;
061import org.ametys.runtime.model.DefinitionContext;
062import org.ametys.runtime.model.ElementDefinition;
063import org.ametys.runtime.model.ModelItem;
064import org.ametys.runtime.model.ModelViewItem;
065import org.ametys.runtime.model.View;
066import org.ametys.runtime.model.ViewItem;
067import org.ametys.runtime.model.ViewItemAccessor;
068import org.ametys.runtime.model.disableconditions.DefaultDisableConditionsEvaluator;
069import org.ametys.runtime.model.disableconditions.DisableCondition;
070import org.ametys.runtime.model.disableconditions.DisableConditions;
071import org.ametys.runtime.model.disableconditions.DisableConditionsEvaluator;
072import org.ametys.runtime.model.exception.BadItemTypeException;
073import org.ametys.runtime.model.exception.UndefinedItemPathException;
074import org.ametys.runtime.model.type.DataContext;
075import org.ametys.runtime.model.type.ElementType;
076import org.ametys.runtime.model.type.ModelItemType;
077
078/**
079 * Helper for implementations of indexable data holder
080 */
081public final class IndexableDataHolderHelper implements Component, Serviceable, Disposable
082{
083    /** The constant to use to send values of external disable conditions */
084    public static final String EXTERNAL_DISABLE_CONDITIONS_VALUES = "__externalDisableConditionsValues";
085    
086    private static JSONUtils _jsonUtils;
087    private static SystemPropertyExtensionPoint _contentSystemPropertyExtentionPoint;
088    private static DisableConditionsEvaluator _disableConditionsEvaluator;
089    
090    @Override
091    public void service(ServiceManager manager) throws ServiceException
092    {
093        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
094        _contentSystemPropertyExtentionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
095        _disableConditionsEvaluator = (DisableConditionsEvaluator) manager.lookup(DefaultDisableConditionsEvaluator.ROLE);
096    }
097    
098    public void dispose()
099    {
100        _jsonUtils = null;
101        _contentSystemPropertyExtentionPoint = null;
102    }
103    
104    /**
105     * Indexes data of the given {@link IndexableDataHolder}
106     * @param dataHolder the {@link IndexableDataHolder} to index
107     * @param viewItemAccessor the view item accessor to explore
108     * @param document the solr document representing this {@link IndexableDataHolder}
109     * @param rootDocument the solr document of the root object.
110     * @param solrFieldPrefix the prefix of the solr field
111     * @param context The context of the data to index
112     * @return additional solr documents that may have been created (ex: repeater entries)
113     * @throws BadItemTypeException if the saxed value's type does not matches the stored data
114     */
115    public static List<SolrInputDocument> indexData(IndexableDataHolder dataHolder, ViewItemAccessor viewItemAccessor, SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, CMSDataContext context) throws BadItemTypeException
116    {
117        ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor);
118
119        List<SolrInputDocument> additionalDocuments = new ArrayList<>();
120        
121        for (ViewItem viewItem : mergedViewItemAccessor.getViewItems())
122        {
123            additionalDocuments.addAll(_indexDataFromViewItem(dataHolder, viewItem, document, rootDocument, solrFieldPrefix, context));
124        }
125        
126        return additionalDocuments;
127    }
128
129    @SuppressWarnings("unchecked")
130    private static List<SolrInputDocument> _indexDataFromViewItem(IndexableDataHolder dataHolder, ViewItem viewItem, SolrInputDocument document, SolrInputDocument rootDocument, String solrFieldPrefix, CMSDataContext context)
131    {
132        if (viewItem instanceof ModelViewItem modelViewItem)
133        {
134            ModelItem modelItem = modelViewItem.getDefinition();
135            String dataName = modelItem.getName();
136            CMSDataContext newContext = context.cloneContext()
137                    .addSegmentToDataPath(dataName)
138                    .withModelItem(modelItem)
139                    .withViewItem(viewItem);
140
141            if (modelItem instanceof IndexationAwareElementDefinition indexationAwareElementDefinition)
142            {
143                indexationAwareElementDefinition.indexValue(document, getAmetysObjectFromContext(newContext), newContext);
144                return Collections.EMPTY_LIST;
145            }
146            else if (!(modelItem instanceof Property) && dataHolder.hasValue(dataName))
147            {
148                if (modelItem instanceof ElementDefinition definition)
149                {
150                    ElementType type = definition.getType();
151                    
152                    if (type instanceof IndexableElementType indexingType)
153                    {
154                        Object value = dataHolder.getValue(dataName);
155                        
156                        String solrFieldName = solrFieldPrefix + dataName;
157                        indexingType.indexValue(document, rootDocument, solrFieldName, value, newContext);
158                    }
159                    
160                    return Collections.EMPTY_LIST;
161                }
162                else if (modelItem instanceof CompositeDefinition)
163                {
164                    IndexableComposite composite = dataHolder.getValue(dataName);
165                    String newSolrFieldPrefix = solrFieldPrefix + dataName + ModelItem.ITEM_PATH_SEPARATOR;
166
167                    return indexData(composite, (ViewItemAccessor) viewItem, document, rootDocument, newSolrFieldPrefix, newContext);
168                }
169                else if (modelItem instanceof RepeaterDefinition)
170                {
171                    IndexableRepeater repeater = dataHolder.getValue(dataName);
172                    return repeater.indexData(document, rootDocument, solrFieldPrefix, newContext);
173                }
174            }
175        }
176        else if (viewItem instanceof ViewItemAccessor accessor)
177        {
178            return indexData(dataHolder, accessor, document, rootDocument, solrFieldPrefix, context);
179        }
180        return Collections.EMPTY_LIST;
181    }
182    
183    /**
184     * Generates SAX events for the data in the given view in the given {@link DataHolder}
185     * @param dataHolder the {@link ModelAwareDataHolder} to SAX
186     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
187     * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items for which generate SAX events
188     * @param context The context of the data to SAX
189     * @param isEdition <code>true</code> if SAX events are generated in edition mode, <code>false</code> otherwise
190     * @throws SAXException if an error occurs during the SAX events generation
191     * @throws BadItemTypeException if the saxed value's type does not matches the stored data
192     * @throws UndefinedItemPathException if an item in the view is not part of the model
193     */
194    public static void dataToSAX(ModelAwareDataHolder dataHolder, ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context, boolean isEdition) throws SAXException, BadItemTypeException, UndefinedItemPathException
195    {
196        ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor);
197        
198        for (ViewItem viewItem : mergedViewItemAccessor.getViewItems())
199        {
200            if (viewItem instanceof ModelViewItem modelViewItem)
201            {
202                ModelItem modelItem = modelViewItem.getDefinition();
203                String dataName = modelItem.getName();
204                DataContext newContext = context.cloneContext()
205                                                .addSegmentToDataPath(dataName)
206                                                .withViewItem(viewItem)
207                                                .withModelItem(modelItem);
208                
209                if (renderValue(dataHolder, dataName, newContext, isEdition))
210                {
211                    if (modelItem instanceof Property property)
212                    {
213                        if (hasValue(dataHolder, dataName, newContext))
214                        {
215                            _propertyToSAX(property, contentHandler, newContext, isEdition);
216                        }
217                    }
218                    else
219                    {
220                        ModelItemType type = modelItem.getType();
221        
222                        if (isEdition)
223                        {
224                            if (newContext instanceof RepositoryDataContext repositoryDataContext && repositoryDataContext.isDataExternalizable())
225                            {
226                                _saxExternalizableValuesAsJson(dataHolder, contentHandler, dataName, repositoryDataContext);
227                            }
228                            else if (hasValue(dataHolder, dataName, newContext))
229                            {
230                                Object value = dataHolder.getValue(dataName);
231                                type.valueToSAXForEdition(contentHandler, modelItem.getName(), value, newContext);
232                            }
233                        }
234                        else if (hasValue(dataHolder, dataName, newContext))
235                        {
236                            Object value = dataHolder.getValue(dataName);
237                            type.valueToSAX(contentHandler, modelItem.getName(), value, newContext);
238                        }
239                    }
240                }
241            }
242            else if (viewItem instanceof ViewItemAccessor accessor)
243            {
244                dataToSAX(dataHolder, contentHandler, accessor, context, isEdition);
245            }
246        }
247        
248        if (_isExternalDisableConditionsAware(mergedViewItemAccessor, isEdition))
249        {
250            externalDisableConditionsToSAX(dataHolder, contentHandler, mergedViewItemAccessor, context);
251        }
252    }
253    
254    @SuppressWarnings("unchecked")
255    private static void _propertyToSAX(Property property, ContentHandler contentHandler, DataContext context, boolean isEdition) throws SAXException
256    {
257        ModelAwareDataAwareAmetysObject ametysObject = getAmetysObjectFromContext(context);
258        if (isEdition)
259        {
260            throw new SAXException("Unable to generate SAX events for property '" + context.getDataPath() + "', for object '" + ametysObject + "' in edition mode: properties are not modifiables.");
261        }
262        else
263        {
264            property.valueToSAX(contentHandler, ametysObject, context);
265        }
266    }
267    
268    private static void _saxExternalizableValuesAsJson (ModelAwareDataHolder dataHolder, ContentHandler contentHandler, String dataName, RepositoryDataContext context) throws SAXException
269    {
270        Map<String, Object> values = externalizableValuesAsJson(dataHolder, dataName, context);
271        
272        if (!values.isEmpty())
273        {
274            String jsonString = _jsonUtils.convertObjectToJson(values);
275    
276            AttributesImpl attrs = new AttributesImpl();
277            attrs.addCDATAAttribute("json", "true");
278            XMLUtils.createElement(contentHandler, dataName, attrs, jsonString);
279        }
280    }
281    
282    /**
283     * Generates SAX events for external disable conditions of the items in the given view
284     * @param dataHolder the {@link ModelAwareDataHolder} to SAX
285     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
286     * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items for which generate SAX events
287     * @param dataContext The context of the data to SAX
288     * @throws SAXException if an error occurs during the SAX events generation
289     * @throws BadItemTypeException if the saxed value's type does not matches the stored data
290     * @throws UndefinedItemPathException if an item in the view is not part of the model
291     */
292    public static void externalDisableConditionsToSAX(ModelAwareDataHolder dataHolder, ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext dataContext) throws SAXException, BadItemTypeException, UndefinedItemPathException
293    {
294        Map<String, Boolean> externalDisableConditionsValues = getExternalDisableConditionsValues(dataHolder, viewItemAccessor, dataContext);
295        
296        if (!externalDisableConditionsValues.isEmpty())
297        {
298            XMLUtils.startElement(contentHandler, EXTERNAL_DISABLE_CONDITIONS_VALUES);
299            
300            for (Map.Entry<String, Boolean> externalDisableConditionValue : externalDisableConditionsValues.entrySet())
301            {
302                String conditionId = externalDisableConditionValue.getKey();
303                String conditionValue = externalDisableConditionValue.getValue().toString();
304                
305                AttributesImpl attributes = new AttributesImpl();
306                attributes.addCDATAAttribute("id", conditionId);
307                XMLUtils.createElement(contentHandler, "condition", attributes, conditionValue);
308            }
309            
310            XMLUtils.endElement(contentHandler, EXTERNAL_DISABLE_CONDITIONS_VALUES);
311        }
312    }
313    
314    /**
315     * Convert the data in the given view of the given {@link DataHolder}
316     * @param dataHolder the {@link ModelAwareDataHolder} containing the data to convert
317     * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items to convert
318     * @param context The context of the data to convert
319     * @param isEdition <code>true</code> to convert in edition mode, <code>false</code> otherwise
320     * @return The data of the given view as JSON
321     * @throws BadItemTypeException if the value's type does not matches the stored data
322     * @throws UndefinedItemPathException if an item in the view is not part of the model
323     */
324    public static Map<String, Object> dataToJSON(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext context, boolean isEdition) throws BadItemTypeException, UndefinedItemPathException
325    {
326        ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor);
327        
328        Map<String, Object> result = new HashMap<>();
329        for (ViewItem viewItem : mergedViewItemAccessor.getViewItems())
330        {
331            if (viewItem instanceof ModelViewItem modelViewItem)
332            {
333                ModelItem modelItem = modelViewItem.getDefinition();
334                String dataName = modelItem.getName();
335                DataContext newContext = context.cloneContext()
336                                                .addSegmentToDataPath(dataName)
337                                                .withViewItem(viewItem)
338                                                .withModelItem(modelItem);
339                
340                if (renderValue(dataHolder, dataName, newContext, isEdition))
341                {
342                    if (modelItem instanceof Property property)
343                    {
344                        if (hasValue(dataHolder, dataName, newContext))
345                        {
346                            Object json = _propertyToJSON(property, newContext, isEdition);
347                            result.put(dataName, json);
348                        }
349                    }
350                    else
351                    {
352                        ModelItemType type = modelItem.getType();
353        
354                        if (isEdition)
355                        {
356                            if (newContext instanceof RepositoryDataContext repositoryDataContext && repositoryDataContext.isDataExternalizable())
357                            {
358                                Map<String, Object> json = externalizableValuesAsJson(dataHolder, dataName, repositoryDataContext);
359                                if (!json.isEmpty())
360                                {
361                                    result.put(dataName, json);
362                                }
363                            }
364                            else if (hasValue(dataHolder, dataName, newContext))
365                            {
366                                Object value = dataHolder.getValue(dataName);
367                                Object json = type.valueToJSONForEdition(value, newContext);
368                                result.put(dataName, json);
369                            }
370                        }
371                        else if (hasValue(dataHolder, dataName, newContext))
372                        {
373                            Object value = dataHolder.getValue(dataName);
374                            Object json = type.valueToJSONForClient(value, newContext);
375                            result.put(dataName, json);
376                        }
377                    }
378                }
379            }
380            else if (viewItem instanceof ViewItemAccessor accessor)
381            {
382                result.putAll(dataToJSON(dataHolder, accessor, context, isEdition));
383            }
384        }
385        
386        if (_isExternalDisableConditionsAware(mergedViewItemAccessor, isEdition))
387        {
388            Map<String, Boolean> conditionsValues = getExternalDisableConditionsValues(dataHolder, mergedViewItemAccessor, context);
389            if (!conditionsValues.isEmpty())
390            {
391                result.put(IndexableDataHolderHelper.EXTERNAL_DISABLE_CONDITIONS_VALUES, conditionsValues);
392            }
393        }
394        
395        return result;
396    }
397    
398    @SuppressWarnings("unchecked")
399    private static Object _propertyToJSON(Property property, DataContext context, boolean isEdition) throws UndefinedItemPathException
400    {
401        ModelAwareDataAwareAmetysObject ametysObject = getAmetysObjectFromContext(context);
402        if (isEdition)
403        {
404            throw new UndefinedItemPathException("Unable to convert property '" + context.getDataPath() + "', for object '" + ametysObject + "' in edition mode: properties are not modifiables.");
405        }
406        else
407        {
408            return property.valueToJSON(ametysObject, context);
409        }
410    }
411    
412    private static boolean _isExternalDisableConditionsAware(ViewItemAccessor viewItemAccessor, boolean isEdition)
413    {
414        return isEdition
415                && (viewItemAccessor instanceof View
416                        || viewItemAccessor instanceof ModelViewItem modelViewItem && modelViewItem.getDefinition() instanceof RepeaterDefinition);
417    }
418    
419    /**
420     * Convert the externalizable data with the given name
421     * @param dataHolder the {@link ModelAwareDataHolder} containing the data to convert
422     * @param dataName the name of the data to convert
423     * @param context The context of the data to convert
424     * @return The data with the given name as JSON
425     */
426    public static Map<String, Object> externalizableValuesAsJson(ModelAwareDataHolder dataHolder, String dataName, RepositoryDataContext context)
427    {
428        Map<String, Object> result = new LinkedHashMap<>();
429
430        RepositoryElementType type = dataHolder.getType(dataName);
431        if (dataHolder.hasLocalValue(dataName)
432            || context.renderEmptyValues() && dataHolder.hasLocalValueOrEmpty(dataName))
433        {
434            Object localValue = dataHolder.getLocalValue(dataName);
435            
436            Object localValueAsJSON = type.externalizableValueToJSON(localValue, context.withStatus(ExternalizableDataStatus.LOCAL));
437            result.put("local", localValueAsJSON);
438        }
439
440        if (dataHolder.hasExternalValue(dataName)
441            || context.renderEmptyValues() && dataHolder.hasExternalValueOrEmpty(dataName))
442        {
443            Object externalValue = dataHolder.getExternalValue(dataName);
444            Object externalValueAsJSON = type.externalizableValueToJSON(externalValue, context.withStatus(ExternalizableDataStatus.EXTERNAL));
445            result.put("external", externalValueAsJSON);
446        }
447        
448        if (!result.isEmpty())
449        {
450            ExternalizableDataStatus status = dataHolder.getStatus(dataName);
451            result.put("status", status.name().toLowerCase());
452        }
453        
454        return result;
455    }
456    
457    /**
458     * Retrieves the values of the disable conditions of the items in the given view for the given data holder
459     * @param dataHolder the {@link ModelAwareDataHolder} containing the data
460     * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items
461     * @param dataContext The context of the data
462     * @return the disable conditions values
463     */
464    public static Map<String, Boolean> getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext dataContext)
465    {
466        DefinitionContext definitionContext = DefinitionContext.newInstance();
467        org.ametys.runtime.model.ViewHelper.getView(viewItemAccessor)
468                                           .ifPresent(definitionContext::withView);
469        
470        return _getExternalDisableConditionsValues(dataHolder, viewItemAccessor, StringUtils.EMPTY, definitionContext, dataContext);
471    }
472    
473    private static Map<String, Boolean> _getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, String prefix, DefinitionContext definitionContext, DataContext dataContext)
474    {
475        Map<String, Boolean> externalDisableConditionsValues = new HashMap<>();
476        
477        for (ViewItem viewItem : viewItemAccessor.getViewItems())
478        {
479            DataContext newDataContext = dataContext.cloneContext();
480            
481            if (viewItem instanceof ModelViewItem modelViewItem)
482            {
483                ModelItem modelItem = modelViewItem.getDefinition();
484                
485                newDataContext.addSegmentToDataPath(modelItem.getName())
486                              .withViewItem(viewItem)
487                              .withModelItem(modelItem);
488                
489                externalDisableConditionsValues.putAll(_getExternalDisableConditionsValues(dataHolder, modelItem, prefix, definitionContext, newDataContext));
490            }
491            
492            if (viewItem instanceof ViewItemAccessor group && !_isExternalDisableConditionsAware(group, true))
493            {
494                // Add external disable conditions of view items in groups
495                String newPrefix = prefix;
496                if (group instanceof ModelViewItem modelViewItem)
497                {
498                    newPrefix = prefix + modelViewItem.getDefinition().getName() + ModelItem.ITEM_PATH_SEPARATOR;
499                }
500                externalDisableConditionsValues.putAll(_getExternalDisableConditionsValues(dataHolder, group, newPrefix, definitionContext, newDataContext));
501            }
502        }
503        
504        return externalDisableConditionsValues;
505    }
506    
507    private static Map<String, Boolean> _getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ModelItem modelItem, String prefix, DefinitionContext definitionContext, DataContext dataContext)
508    {
509        Map<String, Boolean> externalDisableConditionsValues = new HashMap<>();
510        
511        DisableConditions disableConditions = modelItem.getDisableConditions();
512        if (disableConditions != null)
513        {
514            definitionContext.withModelItem(modelItem);
515            Collection<DisableCondition> externalDisableConditions = disableConditions.getExternalDisableConditions(definitionContext);
516            for (DisableCondition condition : externalDisableConditions)
517            {
518                boolean conditionValue = condition.evaluate(modelItem, dataContext.getDataPath(), Optional.empty(), Map.of(), Optional.of(dataHolder.getRootDataHolder()), new HashMap<>());
519                
520                String conditionId = StringUtils.join(new String[] {DisableCondition.EXTERNAL_CONDITION_ID_PREFIX, condition.getId(), condition.getName(), prefix + modelItem.getName()}, "_");
521                externalDisableConditionsValues.put(conditionId, conditionValue);
522            }
523        }
524        
525        return externalDisableConditionsValues;
526    }
527    
528    /**
529     * Returns all data of the given DataHolder as a typed-values Map.
530     * @param dataHolder the DataHolder to export
531     * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items to include in the resulting Map
532     * @param context The context of the data
533     * @return a Map containing all data.
534     */
535    @SuppressWarnings("unchecked")
536    public static Map<String, Object> dataToMap(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext context)
537    {
538        ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor);
539        
540        Map<String, Object> result = new HashMap<>();
541        
542        ViewHelper.visitView(mergedViewItemAccessor,
543            (element, definition) -> {
544                // simple element
545                String name = definition.getName();
546                DataContext newContext = context.cloneContext().addSegmentToDataPath(name);
547                
548                if (renderValue(dataHolder, name, newContext, false))
549                {
550                    if (definition instanceof Property property)
551                    {
552                        if (hasValue(dataHolder, name, newContext))
553                        {
554                            ModelAwareDataAwareAmetysObject ametysObject = getAmetysObjectFromContext(newContext);
555                            Object value = property.getValue(ametysObject);
556                            result.put(name, value);
557                        }
558                    }
559                    else
560                    {
561                        if (newContext instanceof RepositoryDataContext repositoryDataContext && repositoryDataContext.isDataExternalizable())
562                        {
563                            if (_hasExternalizableValue(dataHolder, name, newContext))
564                            {
565                                SynchronizableValue value = new SynchronizableValue(dataHolder.getLocalValue(name));
566                                value.setExternalValue(dataHolder.getExternalValue(name));
567                                value.setExternalizableStatus(dataHolder.getStatus(name));
568                                result.put(name, value);
569                            }
570                        }
571                        else if (hasValue(dataHolder, name, newContext))
572                        {
573                            Object value = dataHolder.getValue(name);
574                            result.put(name, value);
575                        }
576                    }
577                }
578            },
579            (group, definition) -> {
580                // composite
581                String name = definition.getName();
582                DataContext newContext = context.cloneContext().addSegmentToDataPath(name);
583                if (renderValue(dataHolder, name, newContext, false) && hasValue(dataHolder, name, newContext))
584                {
585                    ModelAwareComposite value = dataHolder.getValue(name);
586                    result.put(name, value == null ? null : value.dataToMap(group, newContext));
587                }
588            },
589            (group, definition) -> {
590                // repeater
591                String name = definition.getName();
592                DataContext repeaterContext = context.cloneContext().addSegmentToDataPath(name);
593                
594                if (renderValue(dataHolder, name, repeaterContext, false) && hasValue(dataHolder, name, context))
595                {
596                    ModelAwareRepeater repeater = dataHolder.getValue(name);
597                    List<Map<String, Object>> entries = null;
598                    if (repeater != null)
599                    {
600                        entries = new ArrayList<>();
601                        for (ModelAwareRepeaterEntry entry : repeater.getEntries())
602                        {
603                            DataContext entryContext = repeaterContext.cloneContext().addSuffixToLastSegment("[" + entry.getPosition() + "]");
604                            entries.add(entry.dataToMap(group, entryContext));
605                        }
606                    }
607                    result.put(name, entries);
608                }
609            },
610            group -> result.putAll(dataToMap(dataHolder, group, context)));
611        
612        return result;
613    }
614    
615    /**
616     * Retrieves the {@link ModelAwareDataAwareAmetysObject} from the given {@link DataContext}
617     * @param context the context containing the ametys object
618     * @return the ametys object, or <code>null</code> if there is no object id in the context
619     */
620    public static ModelAwareDataAwareAmetysObject getAmetysObjectFromContext(DataContext context)
621    {
622        RepositoryDataContext repoContext = context instanceof RepositoryDataContext rc ? rc : RepositoryDataContext.newInstance(context);
623        
624        return repoContext.getObject()
625                          .filter(ModelAwareDataAwareAmetysObject.class::isInstance)
626                          .map(ModelAwareDataAwareAmetysObject.class::cast)
627                          .orElse(null);
628    }
629    
630    /**
631     * Check if the value at the given path should be rendered
632     * @param dataHolder the data holder containing the data to check
633     * @param dataPath the path of the data to check
634     * @param context the data context
635     * @param isEdition <code>true</code> if values are rendered in edition mode, <code>false</code> otherwise
636     * @return <code>true</code> if the value has to be rendered, <code>false</code> otherwise
637     */
638    public static boolean renderValue(ModelAwareDataHolder dataHolder, String dataPath, DataContext context, boolean isEdition)
639    {
640        if (!isEdition && !context.renderDisabledValues())
641        {
642            ModelItem modelItem = dataHolder.getDefinition(dataPath);
643            String absoluteDataPath = context.getDataPath();
644            return !_disableConditionsEvaluator.evaluateDisableConditions(modelItem, absoluteDataPath, dataHolder.getRootDataHolder());
645        }
646        else
647        {
648            return true;
649        }
650    }
651    
652    /**
653     * Check if there is a value to render at the given path
654     * @param dataHolder the data holder containing the data to check
655     * @param dataPath the path of the data to check
656     * @param context the data context
657     * @return <code>true</code> if there is a value to render, <code>false</code> otherwise
658     */
659    public static boolean hasValue(ModelAwareDataHolder dataHolder, String dataPath, DataContext context)
660    {
661        return dataHolder.hasValue(dataPath)
662               || context.renderEmptyValues() && dataHolder.hasValueOrEmpty(dataPath);
663    }
664    
665    private static boolean _hasExternalizableValue(ModelAwareDataHolder dataHolder, String dataName, DataContext context)
666    {
667        return dataHolder.hasLocalValue(dataName)
668                || dataHolder.hasExternalValue(dataName)
669                || context.renderEmptyValues()
670                    && (dataHolder.hasLocalValueOrEmpty(dataName) || dataHolder.hasExternalValueOrEmpty(dataName));
671    }
672    
673    /**
674     * Retrieves the {@link SystemPropertyExtensionPoint} for content system properties
675     * @return the {@link SystemPropertyExtensionPoint} for content system properties
676     */
677    public static SystemPropertyExtensionPoint getContentSystemPropertyExtensionPoint()
678    {
679        return _contentSystemPropertyExtentionPoint;
680    }
681    
682    private static ViewItemAccessor _mergeDuplicatedItemsInView(ViewItemAccessor viewItemAccessor)
683    {
684        return viewItemAccessor instanceof View
685                ? org.ametys.runtime.model.ViewHelper.mergeDuplicatedItems(viewItemAccessor)
686                : viewItemAccessor;
687    }
688}