001/*
002 *  Copyright 2025 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;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.configuration.ConfigurationException;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.lang3.tuple.Pair;
034
035import org.ametys.cms.data.ContentValue;
036import org.ametys.cms.data.ametysobject.IndexableAmetysObject;
037import org.ametys.cms.data.type.ModelItemTypeConstants;
038import org.ametys.cms.model.ContentElementDefinition;
039import org.ametys.cms.search.model.SystemProperty;
040import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
041import org.ametys.plugins.repository.data.holder.DataHolder;
042import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
043import org.ametys.plugins.repository.data.holder.group.Repeater;
044import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
045import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater;
046import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
047import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode;
048import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
049import org.ametys.plugins.repository.data.holder.values.UntouchedValue;
050import org.ametys.plugins.repository.model.CompositeDefinition;
051import org.ametys.plugins.repository.model.RepeaterDefinition;
052import org.ametys.runtime.model.DefinitionAndValue;
053import org.ametys.runtime.model.DefinitionContext;
054import org.ametys.runtime.model.ElementDefinition;
055import org.ametys.runtime.model.Model;
056import org.ametys.runtime.model.ModelHelper;
057import org.ametys.runtime.model.ModelItem;
058import org.ametys.runtime.model.ModelItemAccessor;
059import org.ametys.runtime.model.ModelViewItem;
060import org.ametys.runtime.model.View;
061import org.ametys.runtime.model.ViewItemContainer;
062import org.ametys.runtime.model.disableconditions.DisableCondition;
063import org.ametys.runtime.model.exception.BadItemTypeException;
064import org.ametys.runtime.model.exception.UndefinedItemPathException;
065import org.ametys.runtime.model.type.ElementType;
066
067/**
068 * {@link DisableCondition} for model aware {@link DataHolder}
069 */
070public class DataHolderRelativeDisableConditionsHelper implements Component, Serviceable
071{
072    /** The component role. */
073    public static final String ROLE = DataHolderRelativeDisableConditionsHelper.class.getName();
074    
075    /** The contextual parameter key for synchronization context */
076    public static final String SYNCHRONIZATION_CONTEXT_PARAMETER_KEY = "synchronizationContext";
077    /** The JSON info containing exploded conditions when a condition points to an element that is in a distant content */
078    public static final String EXPLODED_DISABLE_CONDITIONS = "explodedConditions";
079    
080    /** The contextual parameter key for data holder */
081    protected static final String __DATA_HOLDER_PARAMETER_KEY = "dataHolder";
082    /** The contextual parameter key for old condition path */
083    protected static final String __OLD_CONDITION_PATH_PARAMETER_KEY = "oldCondtitionPath";
084    
085    private SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
086    
087    public void service(ServiceManager manager) throws ServiceException
088    {
089        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
090    }
091    
092    /**
093     * Retrieves the {@link SystemProperty} if the given data path represents one
094     * @param dataPath the data path of the system property to retrieve
095     * @return the system property or <code>null</code> if the given data path does not represent a system property
096     */
097    public SystemProperty getSystemProperty(String dataPath)
098    {
099        String dataPathLastSegment = dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR)
100                ? StringUtils.substring(dataPath, dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR))
101                : dataPath;
102        
103        return _systemPropertyExtensionPoint.hasExtension(dataPathLastSegment)
104                ? _systemPropertyExtensionPoint.getExtension(dataPathLastSegment)
105                : null;
106    }
107    
108    /**
109     * Retrieves all model items of the given relative path to the system property
110     * @param condition the disable condition
111     * @param conditionPath the absolute path of the disable condition
112     * @param model the model containing the definition
113     * @param definition the model item defining this disable condition
114     * @param systemProperty the system property targeted by the condition
115     * @return all model items of the given relative path
116     * @throws UndefinedItemPathException if there is no item defined at the given path
117     * @throws IllegalArgumentException if the given path is null or empty
118     */
119    public List<ModelItem> getAllModelItemsInPathToSystemProperty(DisableCondition condition, String conditionPath, Model model, ModelItem definition, SystemProperty systemProperty) throws UndefinedItemPathException, IllegalArgumentException
120    {
121        List<ModelItem> allModelItemsInPath = new ArrayList<>();
122            
123        if (conditionPath.contains(ModelItem.ITEM_PATH_SEPARATOR))
124        {
125            String conditionParentPath = StringUtils.substring(conditionPath, 0, conditionPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR));
126            allModelItemsInPath.addAll(ModelHelper.getAllModelItemsInPath(conditionParentPath, List.of(model)));
127
128            ModelItem parentModelItem = allModelItemsInPath.get(allModelItemsInPath.size() - 1);
129            if (!(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(parentModelItem.getType().getId())))
130            {
131                String message = String.format("Disable conditions: the disable condition '%s' on model item '%s' in model '%s' references a system property on a non content element '%s'", condition.getName(), definition.getPath(), model.getId(), conditionParentPath);
132                throw new IllegalArgumentException(message);
133            }
134        }
135        
136        allModelItemsInPath.add(systemProperty);
137        return allModelItemsInPath;
138    }
139
140    /**
141     * Check a segment of the condition path
142     * @param condition the disable condition
143     * @param model the model containing the definition
144     * @param definition the model item defining this disable condition
145     * @param segmentModelItem the model item concerned by the segment
146     * @param segmentIndex the index of the segment in the path
147     * @param conditionPathSize the number of segments in the condition path
148     * @throws ConfigurationException if the segment can not be used in the condition path
149     */
150    public void checkModelItemPathSegment(DisableCondition condition, Model model, ModelItem definition, ModelItem segmentModelItem, int segmentIndex, int conditionPathSize) throws ConfigurationException
151    {
152        if (segmentModelItem instanceof ContentElementDefinition && segmentIndex < conditionPathSize - 2)
153        {
154            // A reference to a content attribute must specify an item at root of this content, groups inside the distant content and multiple joins are not allowed
155            String message = String.format("Disable conditions: the disable condition '%s' on model item '%s' in model '%s' references an item that is not at root of the linked content", condition.getName(), definition.getPath(), model.getId());
156            throw new ConfigurationException(message);
157        }
158        
159        if (segmentModelItem instanceof RepeaterDefinition repeaterDefinition && !(definition.getPath().startsWith(repeaterDefinition.getPath())))
160        {
161            // the definition holding the condition must be in the same repeater
162            String message = String.format("Disable conditions: the disable condition '%s' on model item '%s' in model '%s' references an item that is in a repeater", condition.getName(), definition.getPath(), model.getId());
163            throw new ConfigurationException(message);
164        }
165    }
166    
167    /**
168     * Retrieves the additional contextual parameters to use to extract values
169     * @param condition the disable condition
170     * @param oldDataPath the old path of the evaluated data. Needed to get stored value if the data has been moved
171     * @param dataHolder the data holder
172     * @return the {@link DefinitionAndValue} corresponding to the condition identifier
173     */
174    public Map<String, Object> getAdditionalContextualParameters(DisableCondition condition, Optional<String> oldDataPath, ModelAwareDataHolder dataHolder)
175    {
176        Map<String, Object> contextualParameters = new HashMap<>();
177
178        contextualParameters.put(__DATA_HOLDER_PARAMETER_KEY, dataHolder);
179            
180        Optional<String> oldConditionAbsoluteDataPath = oldDataPath.map(path -> ModelHelper.getDisableConditionAbsolutePath(condition, path));
181        contextualParameters.put(__OLD_CONDITION_PATH_PARAMETER_KEY, oldConditionAbsoluteDataPath);
182
183        return contextualParameters;
184    }
185    
186    /**
187     * Check if the condition concerns a item in a repeater but with no specified entry position
188     * This is the case of external conditions evaluated for new repeater entries
189     * @param dataHolder the data holder
190     * @param conditionDataPath the absolute data path of the condition
191     * @return <code>true</code> if the condition is evaluated for a new repeater entry, <code>false</code> otherwise
192     */
193    public boolean doesEvaluateConditionForNewRepeaterEntry(ModelAwareDataHolder dataHolder, String conditionDataPath)
194    {
195        ModelItem definition = dataHolder.getDefinition(conditionDataPath);
196        if (definition.getParent() != null && definition.getParent() instanceof RepeaterDefinition)
197        {
198            String[] pathSegments = StringUtils.split(conditionDataPath, ModelItem.ITEM_PATH_SEPARATOR);
199            String repeaterPathSegment = pathSegments[pathSegments.length - 2];
200            return !DataHolderHelper.isRepeaterEntryPath(repeaterPathSegment);
201        }
202        else
203        {
204            return false;
205        }
206    }
207    
208    /**
209     * Retrieves the model item accessors relative to the disable condition
210     * @param <T> Type of object holding the data to evaluate
211     * @param object the object holding the data to evaluate and the condition value
212     * @return the model item accessors relative to the disable condition
213     */
214    public <T> Optional<Collection<? extends ModelItemAccessor>> getModelItemAccessors(Optional<T> object)
215    {
216        return object.filter(ModelAwareDataHolder.class::isInstance)
217                     .map(ModelAwareDataHolder.class::cast)
218                     .map(ModelAwareDataHolder::getModel);
219    }
220    
221    /**
222     * Check if the values {@link Map} contains a value for the condition
223     * @param modelItemAccessors the relative accessors to the given condition path
224     * @param conditionDataPath the absolute data path of the condition
225     * @param values the values {@link Map}
226     * @param contextualParameters the contextual parameters
227     * @return <code>true</code> if there is a value (even empty) for the condition in the values {@link Map}, <code>false</code> otherwise
228     * @throws BadItemTypeException if a value does not correspond to the model of given object
229     */
230    public boolean containsValue(Collection<? extends ModelItemAccessor> modelItemAccessors, String conditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
231    {
232        String[] conditionDataPathSegments = StringUtils.split(conditionDataPath, ModelItem.ITEM_PATH_SEPARATOR);
233
234        ModelItem modelItem = ModelHelper.getModelItem(conditionDataPathSegments[0], modelItemAccessors);
235        if (conditionDataPathSegments.length == 1)
236        {
237            return _containsElementValue(modelItem, conditionDataPathSegments[0], values, contextualParameters);
238        }
239        else
240        {
241            String subConditionDataPath = StringUtils.join(conditionDataPathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, conditionDataPathSegments.length);
242            if (modelItem instanceof CompositeDefinition compositeDefinition)
243            {
244                return _doesCompositeContainValue(compositeDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
245            }
246            else if (modelItem instanceof RepeaterDefinition repeaterDefinition)
247            {
248                return _doesRepeaterContainValue(repeaterDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
249            }
250            else if (modelItem instanceof ContentElementDefinition contentElementDefinition)
251            {
252                return _containsElementValue(contentElementDefinition, conditionDataPathSegments[0], values, contextualParameters);
253            }
254            else
255            {
256                throw new BadItemTypeException("Unable to retrieve the value at the given path. the the segment '" + conditionDataPathSegments[0] + "' is not a model item accessor");
257            }
258        }
259    }
260    
261    @SuppressWarnings("unchecked")
262    private boolean _doesCompositeContainValue(CompositeDefinition compositeDefinition, String compositeName, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters)
263    {
264        if (!values.containsKey(compositeName))
265        {
266            // There is no corresponding value in the map
267            return false;
268        }
269        
270        Object value = values.get(compositeName);
271        if (value == null)
272        {
273            // An empty value has been found
274            return true;
275        }
276        
277        if (value instanceof UntouchedValue)
278        {
279            // If value is untouched, we should check in stored values
280            return false;
281        }
282        
283        if (value instanceof Map)
284        {
285            return containsValue(List.of(compositeDefinition), subConditionDataPath, (Map<String, Object>) value, contextualParameters);
286        }
287        else
288        {
289            throw new BadItemTypeException("Unable to retrieve the value at the given path. the value of the segment '" + compositeName + "' for composite is not a Map");
290        }
291    }
292    
293    @SuppressWarnings("unchecked")
294    private boolean _doesRepeaterContainValue(RepeaterDefinition repeaterDefinition, String repeaterNameAndPositionSegment, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
295    {
296        if (!DataHolderHelper.isRepeaterEntryPath(repeaterNameAndPositionSegment))
297        {
298            // The condition is evaluated for new repeater entry, so there is no value
299            return false;
300        }
301        
302        Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(repeaterNameAndPositionSegment);
303        String repeaterName = repeaterNameAndEntryPosition.getLeft();
304        
305        if (!values.containsKey(repeaterName))
306        {
307            // There is no corresponding value in the map
308            return false;
309        }
310        
311        Object value = values.get(repeaterName);
312        if (value == null)
313        {
314            // An empty value has been found
315            return true;
316        }
317        
318        if (value instanceof UntouchedValue)
319        {
320            // If value is untouched, we should check in stored values
321            return false;
322        }
323        
324        if (value instanceof List || value instanceof SynchronizableRepeater)
325        {
326            Integer entryPosition = repeaterNameAndEntryPosition.getRight();
327            List<Map<String, Object>> entries = value instanceof List ? (List<Map<String, Object>>) value : ((SynchronizableRepeater) value).getEntries();
328            SynchronizableRepeater.Mode mode = value instanceof SynchronizableRepeater ? ((SynchronizableRepeater) value).getMode() : SynchronizableRepeater.Mode.REPLACE_ALL;
329            
330            if (mode == SynchronizableRepeater.Mode.REPLACE_ALL)
331            {
332                Map<String, Object> entry = entries.get(entryPosition - 1);
333                return containsValue(List.of(repeaterDefinition), subConditionDataPath, entry, contextualParameters);
334            }
335            else if (mode == SynchronizableRepeater.Mode.REPLACE)
336            {
337                return _doesReplaceModeRepeaterContainValue(repeaterDefinition, entryPosition, subConditionDataPath, (SynchronizableRepeater) value, entries, contextualParameters);
338            }
339            else // mode == SynchronizableRepeater.Mode.APPEND
340            {
341                return _doesAppendModeRepeaterContainValue(repeaterDefinition, entryPosition, (SynchronizableRepeater) value, contextualParameters);
342            }
343        }
344        else
345        {
346            throw new BadItemTypeException("Unable to retrieve the value at the given path. the value of the segment '" + repeaterNameAndPositionSegment + "' for repeater is not a List<Map>");
347        }
348    }
349    
350    private boolean _doesReplaceModeRepeaterContainValue(RepeaterDefinition repeaterDefinition, Integer entryPosition, String subConditionDataPath, SynchronizableRepeater value, List<Map<String, Object>> entries, Map<String, Object> contextualParameters)
351    {
352        int indexOfEntryInReplaceEntries = value.getReplacePositions().indexOf(entryPosition);
353        if (indexOfEntryInReplaceEntries >= 0)
354        {
355            Map<String, Object> entry = entries.get(indexOfEntryInReplaceEntries);
356            return containsValue(List.of(repeaterDefinition), subConditionDataPath, entry, contextualParameters);
357        }
358        else
359        {
360            return false;
361        }
362    }
363    
364    private boolean _doesAppendModeRepeaterContainValue(RepeaterDefinition repeaterDefinition, Integer entryPosition, SynchronizableRepeater value, Map<String, Object> contextualParameters)
365    {
366        Set<Integer> removedEntries = value.getRemovedEntries();
367        if (removedEntries.contains(entryPosition))
368        {
369            // Non sense, we should not evaluate disable conditions on a data in a repeater entry that will be removed
370            return true;
371        }
372        
373        Repeater repeater = null;
374        if (contextualParameters.containsKey(__DATA_HOLDER_PARAMETER_KEY))
375        {
376            IndexableAmetysObject ametysObject = (IndexableAmetysObject) contextualParameters.get(__DATA_HOLDER_PARAMETER_KEY);
377            repeater = ametysObject.getRepeater(repeaterDefinition.getPath());
378        }
379        
380        int actualRepeaterSize = repeater != null ? repeater.getSize() : 0;
381        if (removedEntries.size() > actualRepeaterSize)
382        {
383            throw new IllegalArgumentException("Try to remove more entries than exist in repeater at path '" + repeaterDefinition.getPath() + "'.");
384        }
385        
386        int repeaterSizeAfterRemoving = actualRepeaterSize - removedEntries.size();
387        return entryPosition > repeaterSizeAfterRemoving;
388    }
389    
390    private boolean _containsElementValue(ModelItem definition, String dataName, Map<String, Object> values, Map<String, Object> contextualParameters)
391    {
392        if (!values.containsKey(dataName))
393        {
394            // There is no corresponding value in the map
395            return false;
396        }
397        
398        Object value = values.get(dataName);
399        if (value == null)
400        {
401            // An empty value has been found
402            return true;
403        }
404        
405        if (value instanceof UntouchedValue)
406        {
407            // If value is untouched, we should check in stored values
408            return false;
409        }
410        
411        SynchronizationContext synchronizationContext = (SynchronizationContext) contextualParameters.get(SYNCHRONIZATION_CONTEXT_PARAMETER_KEY);
412        return synchronizationContext == null
413                || !(_getValueFromSynchronizableValue(definition, value, synchronizationContext, contextualParameters) instanceof UntouchedValue);
414    }
415    
416    /**
417     * Retrieves the condition value from the values {@link Map}
418     * @param modelItemAccessors the relative accessors to the given condition path
419     * @param conditionDataPath the absolute data path of the condition
420     * @param values the values {@link Map}
421     * @param contextualParameters the contextual parameters
422     * @return the condition value found in the values {@link Map}
423     * @throws BadItemTypeException if a value does not correspond to the model of given object
424     */
425    public Object getValueFromMap(Collection<? extends ModelItemAccessor> modelItemAccessors, String conditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
426    {
427        String[] conditionDataPathSegments = StringUtils.split(conditionDataPath, ModelItem.ITEM_PATH_SEPARATOR);
428        
429        ModelItem modelItem = ModelHelper.getModelItem(conditionDataPathSegments[0], modelItemAccessors);
430        if (conditionDataPathSegments.length == 1)
431        {
432            if (modelItem instanceof ElementDefinition definition)
433            {
434                return _getElementValueFromMap(definition, conditionDataPathSegments[0], values, contextualParameters);
435            }
436            else
437            {
438                throw new BadItemTypeException("Unable to retrieve the value at the given path. the path '" + conditionDataPathSegments[0] + "' is not an element");
439            }
440        }
441        else
442        {
443            String subConditionDataPath = StringUtils.join(conditionDataPathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, conditionDataPathSegments.length);
444            if (modelItem instanceof CompositeDefinition compositeDefinition)
445            {
446                return _getValueFromComposite(compositeDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
447            }
448            else if (modelItem instanceof RepeaterDefinition repeaterDefinition)
449            {
450                return _getValueFromRepeater(repeaterDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
451            }
452            else if (modelItem instanceof ContentElementDefinition contentElementDefinition)
453            {
454                return _getValueFromContentValue(contentElementDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
455            }
456            else
457            {
458                throw new BadItemTypeException("Unable to retrieve the value at the given path. the segment '" + conditionDataPathSegments[0] + "' is not a model item accessor");
459            }
460        }
461    }
462    
463    @SuppressWarnings("unchecked")
464    private Object _getValueFromComposite(CompositeDefinition compositeDefinition, String compositeName, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
465    {
466        Object value = values.get(compositeName);
467        if (value == null)
468        {
469            return null;
470        }
471        
472        if (value instanceof Map)
473        {
474            return getValueFromMap(List.of(compositeDefinition), subConditionDataPath, (Map<String, Object>) value, contextualParameters);
475        }
476        else
477        {
478            throw new BadItemTypeException("Unable to retrieve the value at the given path. the value of the segment '" + compositeName + "' for composite is not a Map");
479        }
480    }
481    
482    @SuppressWarnings("unchecked")
483    private Object _getValueFromRepeater(RepeaterDefinition repeaterDefinition, String repeaterNameAndPositionSegment, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
484    {
485        Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(repeaterNameAndPositionSegment);
486        String repeaterName = repeaterNameAndEntryPosition.getLeft();
487        
488        Object value = values.get(repeaterName);
489        if (value == null)
490        {
491            return null;
492        }
493        
494        if (value instanceof List || value instanceof SynchronizableRepeater)
495        {
496            Integer entryPosition = repeaterNameAndEntryPosition.getRight();
497            List<Map<String, Object>> entries = value instanceof List ? (List<Map<String, Object>>) value : ((SynchronizableRepeater) value).getEntries();
498            SynchronizableRepeater.Mode mode = value instanceof SynchronizableRepeater ? ((SynchronizableRepeater) value).getMode() : SynchronizableRepeater.Mode.REPLACE_ALL;
499            
500            if (mode == SynchronizableRepeater.Mode.REPLACE_ALL)
501            {
502                Map<String, Object> entry = entries.get(entryPosition - 1);
503                return getValueFromMap(List.of(repeaterDefinition), subConditionDataPath, entry, contextualParameters);
504            }
505            else if (mode == SynchronizableRepeater.Mode.REPLACE)
506            {
507                int indexOfEntryInReplaceEntries = ((SynchronizableRepeater) value).getReplacePositions().indexOf(entryPosition);
508                Map<String, Object> entry = entries.get(indexOfEntryInReplaceEntries);
509                return getValueFromMap(List.of(repeaterDefinition), subConditionDataPath, entry, contextualParameters);
510            }
511            else // mode == SynchronizableRepeater.Mode.APPEND
512            {
513                Set<Integer> removedEntries = ((SynchronizableRepeater) value).getRemovedEntries();
514                if (removedEntries.contains(entryPosition))
515                {
516                    // Non sense, we should not evaluate disable conditions on a data in a repeater entry that will be removed
517                    return null;
518                }
519                
520                Repeater repeater = null;
521                if (contextualParameters.containsKey(__DATA_HOLDER_PARAMETER_KEY))
522                {
523                    IndexableAmetysObject ametysObject = (IndexableAmetysObject) contextualParameters.get(__DATA_HOLDER_PARAMETER_KEY);
524                    repeater = ametysObject.getRepeater(repeaterDefinition.getPath());
525                }
526                
527                int actualRepeaterSize = repeater != null ? repeater.getSize() : 0;
528                
529                int repeaterSizeAfterRemoving = actualRepeaterSize - removedEntries.size();
530                int positionInAppendedEntries = entryPosition - repeaterSizeAfterRemoving;
531                int indexInAppendedEntries = positionInAppendedEntries - 1;
532                Map<String, Object> entry = entries.get(indexInAppendedEntries);
533                return getValueFromMap(List.of(repeaterDefinition), subConditionDataPath, entry, contextualParameters);
534            }
535        }
536        else
537        {
538            throw new BadItemTypeException("Unable to retrieve the value at the given path. the value of the segment '" + repeaterNameAndPositionSegment + "' for repeater is not a List<Map>");
539        }
540    }
541    
542    private Object _getValueFromContentValue(ContentElementDefinition contentElementDefinition, String dataName, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters)
543    {
544        ElementType<ContentValue> type = contentElementDefinition.getType();
545        Object contentValue = _getElementValueFromMap(contentElementDefinition, dataName, values, contextualParameters);
546        
547        if (contentValue == null)
548        {
549            return null;
550        }
551        
552        if (contentValue.getClass().isArray())
553        {
554            return Arrays.stream((Object[]) contentValue)
555                         .map(type::castValue)
556                         .map(ContentValue::getContent)
557                         .map(content -> content.getValue(subConditionDataPath))
558                         .toArray();
559        }
560
561        return Optional.of(contentValue)
562                       .map(type::castValue)
563                       .map(ContentValue::getContent)
564                       .map(content -> content.getValue(subConditionDataPath))
565                       .orElse(null);
566    }
567    
568    private Object _getElementValueFromMap(ElementDefinition definition, String dataName, Map<String, Object> values, Map<String, Object> contextualParameters)
569    {
570        Object value = values.get(dataName);
571        if (value == null)
572        {
573            return null;
574        }
575        
576        SynchronizationContext synchronizationContext = (SynchronizationContext) contextualParameters.get(SYNCHRONIZATION_CONTEXT_PARAMETER_KEY);
577        if (synchronizationContext != null)
578        {
579            Object valueFromSyncValue = _getValueFromSynchronizableValue(definition, value, synchronizationContext, contextualParameters);
580        
581            return valueFromSyncValue == null
582                || value instanceof SynchronizableValue syncValue && syncValue.getMode() == Mode.REMOVE
583                    ? null
584                    : valueFromSyncValue;
585        }
586        
587        ElementType type = definition.getType();
588        if (value.getClass().isArray())
589        {
590            return Arrays.stream((Object[]) value)
591                         .map(type::castValue)
592                         .toArray();
593        }
594        
595        return type.castValue(value);
596    }
597    
598    private Object _getValueFromSynchronizableValue(ModelItem definition, Object value, SynchronizationContext synchronizationContext, Map<String, Object> contextualParameters)
599    {
600        Object valueFromSyncValue = value instanceof SynchronizableValue synchronizableValue ? synchronizableValue.getLocalValue() : value;
601        if (contextualParameters.containsKey(__DATA_HOLDER_PARAMETER_KEY))
602        {
603            IndexableAmetysObject ametysObject = (IndexableAmetysObject) contextualParameters.get(__DATA_HOLDER_PARAMETER_KEY);
604            @SuppressWarnings("unchecked")
605            Optional<String> oldConditionDataPath = (Optional<String>) contextualParameters.get(__OLD_CONDITION_PATH_PARAMETER_KEY);
606            valueFromSyncValue = DataHolderHelper.getValueFromSynchronizableValue(value, ametysObject, definition, oldConditionDataPath, synchronizationContext);
607        }
608        
609        return valueFromSyncValue;
610    }
611    
612    /**
613     * Check if the given disable condition is external
614     * The condition is external if there is a view in the context and  the relative element pointed by the condition is not in the view
615     * @param condition the disable condition to check
616     * @param context the definition context
617     * @return <code>true</code> if the given disable condition is external, <code>false</code> otherwise
618     */
619    public boolean isExternal(DisableCondition condition, DefinitionContext context)
620    {
621        Optional<ModelItem> modelItem = context.getModelItem();
622        Optional<View> view = context.getView();
623        
624        if (modelItem.isPresent() && view.isPresent())
625        {
626            String absolutePath = ModelHelper.getDisableConditionAbsolutePath(condition, modelItem.get().getPath());
627            
628            // If the view item has not been found in the view, the condition is external
629            return !_hasViewItem(view.get(), absolutePath);
630        }
631        else
632        {
633            // No information permit to check if the condition is external or not, consider it as not external
634            return false;
635        }
636    }
637    
638    private boolean _hasViewItem(ViewItemContainer viewItemContainer, String path)
639    {
640        String[] pathSegments = StringUtils.split(path, ModelItem.ITEM_PATH_SEPARATOR);
641        
642        if (pathSegments.length == 1)
643        {
644            // Check if the referenced item is in the view
645            return viewItemContainer.hasModelViewItem(pathSegments[0]);
646        }
647        else
648        {
649            // Check first segment of the path
650            if (viewItemContainer.hasModelViewItem(pathSegments[0]))
651            {
652                ModelViewItem modelViewItem = viewItemContainer.getModelViewItem(pathSegments[0]);
653                if (modelViewItem instanceof ViewItemContainer subViewItemContainer)
654                {
655                    String subPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
656                    return _hasViewItem(subViewItemContainer, subPath);
657                }
658                else
659                {
660                    // If the first segment is an accessor but not a container (linked contents),
661                    // we do not check the other segments. The condition will be exploded
662                    return true;
663                }
664            }
665            else
666            {
667                // The item of the first path segment has not been found in the view
668                return false;
669            }
670        }
671    }
672}