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