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