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, newContext);
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, DataContext 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, newContext);
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, DataContext 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            Object localValueAsJSON = type.externalizableValueToJSON(localValue, context);
436            result.put("local", localValueAsJSON);
437        }
438
439        if (dataHolder.hasExternalValue(dataName)
440            || context.renderEmptyValues() && dataHolder.hasExternalValueOrEmpty(dataName))
441        {
442            Object externalValue = dataHolder.getExternalValue(dataName);
443            Object externalValueAsJSON = type.externalizableValueToJSON(externalValue, context);
444            result.put("external", externalValueAsJSON);
445        }
446        
447        if (!result.isEmpty())
448        {
449            ExternalizableDataStatus status = dataHolder.getStatus(dataName);
450            result.put("status", status.name().toLowerCase());
451        }
452        
453        return result;
454    }
455    
456    /**
457     * Retrieves the values of the disable conditions of the items in the given view for the given data holder
458     * @param dataHolder the {@link ModelAwareDataHolder} containing the data
459     * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items
460     * @param dataContext The context of the data
461     * @return the disable conditions values
462     */
463    public static Map<String, Boolean> getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext dataContext)
464    {
465        DefinitionContext definitionContext = DefinitionContext.newInstance();
466        org.ametys.runtime.model.ViewHelper.getView(viewItemAccessor)
467                                           .ifPresent(definitionContext::withView);
468        
469        return _getExternalDisableConditionsValues(dataHolder, viewItemAccessor, StringUtils.EMPTY, definitionContext, dataContext);
470    }
471    
472    private static Map<String, Boolean> _getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, String prefix, DefinitionContext definitionContext, DataContext dataContext)
473    {
474        Map<String, Boolean> externalDisableConditionsValues = new HashMap<>();
475        
476        for (ViewItem viewItem : viewItemAccessor.getViewItems())
477        {
478            DataContext newDataContext = dataContext.cloneContext();
479            
480            if (viewItem instanceof ModelViewItem modelViewItem)
481            {
482                ModelItem modelItem = modelViewItem.getDefinition();
483                
484                newDataContext.addSegmentToDataPath(modelItem.getName())
485                              .withViewItem(viewItem)
486                              .withModelItem(modelItem);
487                
488                externalDisableConditionsValues.putAll(_getExternalDisableConditionsValues(dataHolder, modelItem, prefix, definitionContext, newDataContext));
489            }
490            
491            if (viewItem instanceof ViewItemAccessor group && !_isExternalDisableConditionsAware(group, true))
492            {
493                // Add external disable conditions of view items in groups
494                String newPrefix = prefix;
495                if (group instanceof ModelViewItem modelViewItem)
496                {
497                    newPrefix = prefix + modelViewItem.getDefinition().getName() + ModelItem.ITEM_PATH_SEPARATOR;
498                }
499                externalDisableConditionsValues.putAll(_getExternalDisableConditionsValues(dataHolder, group, newPrefix, definitionContext, newDataContext));
500            }
501        }
502        
503        return externalDisableConditionsValues;
504    }
505    
506    private static Map<String, Boolean> _getExternalDisableConditionsValues(ModelAwareDataHolder dataHolder, ModelItem modelItem, String prefix, DefinitionContext definitionContext, DataContext dataContext)
507    {
508        Map<String, Boolean> externalDisableConditionsValues = new HashMap<>();
509        
510        DisableConditions disableConditions = modelItem.getDisableConditions();
511        if (disableConditions != null)
512        {
513            definitionContext.withModelItem(modelItem);
514            Collection<DisableCondition> externalDisableConditions = disableConditions.getExternalDisableConditions(definitionContext);
515            for (DisableCondition condition : externalDisableConditions)
516            {
517                boolean conditionValue = condition.evaluate(modelItem, dataContext.getDataPath(), Optional.empty(), Map.of(), Optional.of(dataHolder.getRootDataHolder()), new HashMap<>());
518                
519                String conditionId = StringUtils.join(new String[] {DisableCondition.EXTERNAL_CONDITION_ID_PREFIX, condition.getId(), condition.getName(), prefix + modelItem.getName()}, "_");
520                externalDisableConditionsValues.put(conditionId, conditionValue);
521            }
522        }
523        
524        return externalDisableConditionsValues;
525    }
526    
527    /**
528     * Returns all data of the given DataHolder as a typed-values Map.
529     * @param dataHolder the DataHolder to export
530     * @param viewItemAccessor the {@link ViewItemAccessor} referencing the items to include in the resulting Map
531     * @param context The context of the data
532     * @return a Map containing all data.
533     */
534    @SuppressWarnings("unchecked")
535    public static Map<String, Object> dataToMap(ModelAwareDataHolder dataHolder, ViewItemAccessor viewItemAccessor, DataContext context)
536    {
537        ViewItemAccessor mergedViewItemAccessor = _mergeDuplicatedItemsInView(viewItemAccessor);
538        
539        Map<String, Object> result = new HashMap<>();
540        
541        ViewHelper.visitView(mergedViewItemAccessor,
542            (element, definition) -> {
543                // simple element
544                String name = definition.getName();
545                DataContext newContext = context.cloneContext().addSegmentToDataPath(name);
546                
547                if (renderValue(dataHolder, name, newContext, false))
548                {
549                    if (definition instanceof Property property)
550                    {
551                        if (hasValue(dataHolder, name, newContext))
552                        {
553                            ModelAwareDataAwareAmetysObject ametysObject = getAmetysObjectFromContext(newContext);
554                            Object value = property.getValue(ametysObject);
555                            result.put(name, value);
556                        }
557                    }
558                    else
559                    {
560                        if (newContext instanceof RepositoryDataContext repositoryDataContext && repositoryDataContext.isDataExternalizable())
561                        {
562                            if (_hasExternalizableValue(dataHolder, name, newContext))
563                            {
564                                SynchronizableValue value = new SynchronizableValue(dataHolder.getLocalValue(name));
565                                value.setExternalValue(dataHolder.getExternalValue(name));
566                                value.setExternalizableStatus(dataHolder.getStatus(name));
567                                result.put(name, value);
568                            }
569                        }
570                        else if (hasValue(dataHolder, name, newContext))
571                        {
572                            Object value = dataHolder.getValue(name);
573                            result.put(name, value);
574                        }
575                    }
576                }
577            },
578            (group, definition) -> {
579                // composite
580                String name = definition.getName();
581                DataContext newContext = context.cloneContext().addSegmentToDataPath(name);
582                if (renderValue(dataHolder, name, newContext, false) && hasValue(dataHolder, name, newContext))
583                {
584                    ModelAwareComposite value = dataHolder.getValue(name);
585                    result.put(name, value == null ? null : value.dataToMap(group, newContext));
586                }
587            },
588            (group, definition) -> {
589                // repeater
590                String name = definition.getName();
591                DataContext repeaterContext = context.cloneContext().addSegmentToDataPath(name);
592                
593                if (renderValue(dataHolder, name, repeaterContext, false) && hasValue(dataHolder, name, context))
594                {
595                    ModelAwareRepeater repeater = dataHolder.getValue(name);
596                    List<Map<String, Object>> entries = null;
597                    if (repeater != null)
598                    {
599                        entries = new ArrayList<>();
600                        for (ModelAwareRepeaterEntry entry : repeater.getEntries())
601                        {
602                            DataContext entryContext = repeaterContext.cloneContext().addSuffixToLastSegment("[" + entry.getPosition() + "]");
603                            entries.add(entry.dataToMap(group, entryContext));
604                        }
605                    }
606                    result.put(name, entries);
607                }
608            },
609            group -> result.putAll(dataToMap(dataHolder, group, context)));
610        
611        return result;
612    }
613    
614    /**
615     * Retrieves the {@link ModelAwareDataAwareAmetysObject} from the given {@link DataContext}
616     * @param context the context containing the ametys object
617     * @return the ametys object, or <code>null</code> if there is no object id in the context
618     */
619    public static ModelAwareDataAwareAmetysObject getAmetysObjectFromContext(DataContext context)
620    {
621        RepositoryDataContext repoContext = context instanceof RepositoryDataContext rc ? rc : RepositoryDataContext.newInstance(context);
622        
623        return repoContext.getObject()
624                          .filter(ModelAwareDataAwareAmetysObject.class::isInstance)
625                          .map(ModelAwareDataAwareAmetysObject.class::cast)
626                          .orElse(null);
627    }
628    
629    /**
630     * Check if the value at the given path should be rendered
631     * @param dataHolder the data holder containing the data to check
632     * @param dataPath the path of the data to check
633     * @param context the data context
634     * @param isEdition <code>true</code> if values are rendered in edition mode, <code>false</code> otherwise
635     * @return <code>true</code> if the value has to be rendered, <code>false</code> otherwise
636     */
637    public static boolean renderValue(ModelAwareDataHolder dataHolder, String dataPath, DataContext context, boolean isEdition)
638    {
639        if (!isEdition && !context.renderDisabledValues())
640        {
641            ModelItem modelItem = dataHolder.getDefinition(dataPath);
642            String absoluteDataPath = context.getDataPath();
643            return !_disableConditionsEvaluator.evaluateDisableConditions(modelItem, absoluteDataPath, dataHolder.getRootDataHolder());
644        }
645        else
646        {
647            return true;
648        }
649    }
650    
651    /**
652     * Check if there is a value to render at the given path
653     * @param dataHolder the data holder containing the data to check
654     * @param dataPath the path of the data to check
655     * @param context the data context
656     * @return <code>true</code> if there is a value to render, <code>false</code> otherwise
657     */
658    public static boolean hasValue(ModelAwareDataHolder dataHolder, String dataPath, DataContext context)
659    {
660        return dataHolder.hasValue(dataPath)
661               || context.renderEmptyValues() && dataHolder.hasValueOrEmpty(dataPath);
662    }
663    
664    private static boolean _hasExternalizableValue(ModelAwareDataHolder dataHolder, String dataName, DataContext context)
665    {
666        return dataHolder.hasLocalValue(dataName)
667                || dataHolder.hasExternalValue(dataName)
668                || context.renderEmptyValues()
669                    && (dataHolder.hasLocalValueOrEmpty(dataName) || dataHolder.hasExternalValueOrEmpty(dataName));
670    }
671    
672    /**
673     * Retrieves the {@link SystemPropertyExtensionPoint} for content system properties
674     * @return the {@link SystemPropertyExtensionPoint} for content system properties
675     */
676    public static SystemPropertyExtensionPoint getContentSystemPropertyExtensionPoint()
677    {
678        return _contentSystemPropertyExtentionPoint;
679    }
680    
681    private static ViewItemAccessor _mergeDuplicatedItemsInView(ViewItemAccessor viewItemAccessor)
682    {
683        return viewItemAccessor instanceof View
684                ? org.ametys.runtime.model.ViewHelper.mergeDuplicatedItems(viewItemAccessor)
685                : viewItemAccessor;
686    }
687}