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.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.lang3.tuple.Pair;
033
034import org.ametys.cms.data.ContentValue;
035import org.ametys.cms.data.ametysobject.ModelAwareDataAwareAmetysObject;
036import org.ametys.cms.data.type.ModelItemTypeConstants;
037import org.ametys.cms.model.ContentElementDefinition;
038import org.ametys.cms.search.model.SystemProperty;
039import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
040import org.ametys.plugins.repository.data.holder.DataHolder;
041import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
042import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
043import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
044import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater;
045import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
046import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode;
047import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
048import org.ametys.plugins.repository.data.holder.values.UntouchedValue;
049import org.ametys.plugins.repository.model.CompositeDefinition;
050import org.ametys.plugins.repository.model.RepeaterDefinition;
051import org.ametys.runtime.model.DefinitionAndValue;
052import org.ametys.runtime.model.DefinitionContext;
053import org.ametys.runtime.model.Model;
054import org.ametys.runtime.model.ModelHelper;
055import org.ametys.runtime.model.ModelItem;
056import org.ametys.runtime.model.ModelItemAccessor;
057import org.ametys.runtime.model.ModelViewItem;
058import org.ametys.runtime.model.View;
059import org.ametys.runtime.model.ViewItemContainer;
060import org.ametys.runtime.model.disableconditions.DisableCondition;
061import org.ametys.runtime.model.exception.BadItemTypeException;
062import org.ametys.runtime.model.exception.UndefinedItemPathException;
063import org.ametys.runtime.model.type.ElementType;
064
065/**
066 * {@link DisableCondition} for model aware {@link DataHolder}
067 */
068public class DataHolderRelativeDisableConditionsHelper implements Component, Serviceable
069{
070    /** The component role. */
071    public static final String ROLE = DataHolderRelativeDisableConditionsHelper.class.getName();
072    
073    /** The contextual parameter key for synchronization context */
074    public static final String SYNCHRONIZATION_CONTEXT_PARAMETER_KEY = "synchronizationContext";
075    /** The JSON info containing exploded conditions when a condition points to an element that is in a distant content */
076    public static final String EXPLODED_DISABLE_CONDITIONS = "explodedConditions";
077    
078    /** The contextual parameter key for data holder */
079    protected static final String __DATA_HOLDER_PARAMETER_KEY = "dataHolder";
080    /** The contextual parameter key for old condition path */
081    protected static final String __OLD_CONDITION_PATH_PARAMETER_KEY = "oldCondtitionPath";
082    
083    private SystemPropertyExtensionPoint _systemPropertyExtensionPoint;
084    
085    public void service(ServiceManager manager) throws ServiceException
086    {
087        _systemPropertyExtensionPoint = (SystemPropertyExtensionPoint) manager.lookup(SystemPropertyExtensionPoint.ROLE);
088    }
089    
090    /**
091     * Retrieves the {@link SystemProperty} if the given data path represents one
092     * @param modelItemAccessor the relative accessor to the given data path
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(ModelItemAccessor modelItemAccessor, String dataPath)
097    {
098        String conditionPathLastSegment = dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR)
099                ? StringUtils.substring(dataPath, dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR))
100                : dataPath;
101        
102        return _systemPropertyExtensionPoint.hasExtension(conditionPathLastSegment)
103                ? _systemPropertyExtensionPoint.getExtension(conditionPathLastSegment)
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     * Check if the values {@link Map} contains a value for the condition
209     * @param modelItemAccessor the relative accessor to the given condition path
210     * @param conditionDataPath the absolute data path of the condition
211     * @param values the values {@link Map}
212     * @param contextualParameters the contextual parameters
213     * @return <code>true</code> if there is a value (even empty) for the condition in the values {@link Map}, <code>false</code> otherwise
214     * @throws BadItemTypeException if a value does not correspond to the model of given object
215     */
216    public boolean containsValue(ModelItemAccessor modelItemAccessor, String conditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
217    {
218        String[] conditionDataPathSegments = StringUtils.split(conditionDataPath, ModelItem.ITEM_PATH_SEPARATOR);
219
220        ModelItem modelItem = ModelHelper.getModelItem(conditionDataPathSegments[0], List.of(modelItemAccessor));
221        if (conditionDataPathSegments.length == 1)
222        {
223            return _containsElementValue(modelItem, conditionDataPathSegments[0], values, contextualParameters);
224        }
225        else
226        {
227            String subConditionDataPath = StringUtils.join(conditionDataPathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, conditionDataPathSegments.length);
228            if (modelItem instanceof CompositeDefinition compositeDefinition)
229            {
230                return _doesCompositeContainValue(compositeDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
231            }
232            else if (modelItem instanceof RepeaterDefinition repeaterDefinition)
233            {
234                return _doesRepeaterContainValue(repeaterDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
235            }
236            else if (modelItem instanceof ContentElementDefinition contentElementDefinition)
237            {
238                return _containsElementValue(contentElementDefinition, conditionDataPathSegments[0], values, contextualParameters);
239            }
240            else
241            {
242                throw new BadItemTypeException("Unable to retrieve the value at the given path. the the segment '" + conditionDataPathSegments[0] + "' is not a model item accessor");
243            }
244        }
245    }
246    
247    @SuppressWarnings("unchecked")
248    private boolean _doesCompositeContainValue(CompositeDefinition compositeDefinition, String compositeName, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters)
249    {
250        if (!values.containsKey(compositeName))
251        {
252            // There is no corresponding value in the map
253            return false;
254        }
255        
256        Object value = values.get(compositeName);
257        if (value == null)
258        {
259            // An empty value has been found
260            return true;
261        }
262        
263        if (value instanceof UntouchedValue)
264        {
265            // If value is untouched, we should check in stored values
266            return false;
267        }
268        
269        if (value instanceof Map)
270        {
271            return containsValue(compositeDefinition, subConditionDataPath, (Map<String, Object>) value, contextualParameters);
272        }
273        else
274        {
275            throw new BadItemTypeException("Unable to retrieve the value at the given path. the value of the segment '" + compositeName + "' for composite is not a Map");
276        }
277    }
278    
279    @SuppressWarnings("unchecked")
280    private boolean _doesRepeaterContainValue(RepeaterDefinition repeaterDefinition, String repeaterNameAndPositionSegment, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
281    {
282        if (!DataHolderHelper.isRepeaterEntryPath(repeaterNameAndPositionSegment))
283        {
284            // The condition is evaluated for new repeater entry, so there is no value
285            return false;
286        }
287        
288        Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(repeaterNameAndPositionSegment);
289        String repeaterName = repeaterNameAndEntryPosition.getLeft();
290        
291        if (!values.containsKey(repeaterName))
292        {
293            // There is no corresponding value in the map
294            return false;
295        }
296        
297        Object value = values.get(repeaterName);
298        if (value == null)
299        {
300            // An empty value has been found
301            return true;
302        }
303        
304        if (value instanceof UntouchedValue)
305        {
306            // If value is untouched, we should check in stored values
307            return false;
308        }
309        
310        if (value instanceof List || value instanceof SynchronizableRepeater)
311        {
312            Integer entryPosition = repeaterNameAndEntryPosition.getRight();
313            List<Map<String, Object>> entries = value instanceof List ? (List<Map<String, Object>>) value : ((SynchronizableRepeater) value).getEntries();
314            SynchronizableRepeater.Mode mode = value instanceof SynchronizableRepeater ? ((SynchronizableRepeater) value).getMode() : SynchronizableRepeater.Mode.REPLACE_ALL;
315            
316            if (mode == SynchronizableRepeater.Mode.REPLACE_ALL)
317            {
318                Map<String, Object> entry = entries.get(entryPosition - 1);
319                return containsValue(repeaterDefinition, subConditionDataPath, entry, contextualParameters);
320            }
321            else if (mode == SynchronizableRepeater.Mode.REPLACE)
322            {
323                return _doesReplaceModeRepeaterContainValue(repeaterDefinition, entryPosition, subConditionDataPath, (SynchronizableRepeater) value, entries, contextualParameters);
324            }
325            else // mode == SynchronizableRepeater.Mode.APPEND
326            {
327                return _doesAppendModeRepeaterContainValue(repeaterDefinition, entryPosition, (SynchronizableRepeater) value, contextualParameters);
328            }
329        }
330        else
331        {
332            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>");
333        }
334    }
335    
336    private boolean _doesReplaceModeRepeaterContainValue(RepeaterDefinition repeaterDefinition, Integer entryPosition, String subConditionDataPath, SynchronizableRepeater value, List<Map<String, Object>> entries, Map<String, Object> contextualParameters)
337    {
338        int indexOfEntryInReplaceEntries = value.getReplacePositions().indexOf(entryPosition);
339        if (indexOfEntryInReplaceEntries >= 0)
340        {
341            Map<String, Object> entry = entries.get(indexOfEntryInReplaceEntries);
342            return containsValue(repeaterDefinition, subConditionDataPath, entry, contextualParameters);
343        }
344        else
345        {
346            return false;
347        }
348    }
349    
350    private boolean _doesAppendModeRepeaterContainValue(RepeaterDefinition repeaterDefinition, Integer entryPosition, SynchronizableRepeater value, Map<String, Object> contextualParameters)
351    {
352        Set<Integer> removedEntries = value.getRemovedEntries();
353        if (removedEntries.contains(entryPosition))
354        {
355            // Non sense, we should not evaluate disable conditions on a data in a repeater entry that will be removed
356            return true;
357        }
358        
359        ModelAwareRepeater repeater = null;
360        if (contextualParameters.containsKey(__DATA_HOLDER_PARAMETER_KEY))
361        {
362            ModelAwareDataAwareAmetysObject ametysObject = (ModelAwareDataAwareAmetysObject) contextualParameters.get(__DATA_HOLDER_PARAMETER_KEY);
363            repeater = ametysObject.getRepeater(repeaterDefinition.getPath());
364        }
365        
366        int actualRepeaterSize = repeater != null ? repeater.getSize() : 0;
367        if (removedEntries.size() > actualRepeaterSize)
368        {
369            throw new IllegalArgumentException("Try to remove more entries than exist in repeater at path '" + repeaterDefinition.getPath() + "'.");
370        }
371        
372        int repeaterSizeAfterRemoving = actualRepeaterSize - removedEntries.size();
373        return entryPosition > repeaterSizeAfterRemoving;
374    }
375    
376    private boolean _containsElementValue(ModelItem definition, String dataName, Map<String, Object> values, Map<String, Object> contextualParameters)
377    {
378        if (!values.containsKey(dataName))
379        {
380            // There is no corresponding value in the map
381            return false;
382        }
383        
384        Object value = values.get(dataName);
385        if (value == null)
386        {
387            // An empty value has been found
388            return true;
389        }
390        
391        if (value instanceof UntouchedValue)
392        {
393            // If value is untouched, we should check in stored values
394            return false;
395        }
396        
397        SynchronizationContext synchronizationContext = (SynchronizationContext) contextualParameters.get(SYNCHRONIZATION_CONTEXT_PARAMETER_KEY);
398        return synchronizationContext == null
399                || !(_getValueFromSynchronizableValue(definition, value, synchronizationContext, contextualParameters) instanceof UntouchedValue);
400    }
401    
402    /**
403     * Retrieves the condition value from the values {@link Map}
404     * @param modelItemAccessor the relative accessor to the given condition path
405     * @param conditionDataPath the absolute data path of the condition
406     * @param values the values {@link Map}
407     * @param contextualParameters the contextual parameters
408     * @return the condition value found in the values {@link Map}
409     * @throws BadItemTypeException if a value does not correspond to the model of given object
410     */
411    public Object getValueFromMap(ModelItemAccessor modelItemAccessor, String conditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
412    {
413        String[] conditionDataPathSegments = StringUtils.split(conditionDataPath, ModelItem.ITEM_PATH_SEPARATOR);
414        
415        ModelItem modelItem = ModelHelper.getModelItem(conditionDataPathSegments[0], List.of(modelItemAccessor));
416        if (conditionDataPathSegments.length == 1)
417        {
418            return _getElementValueFromMap(modelItem, conditionDataPathSegments[0], values, contextualParameters);
419        }
420        else
421        {
422            String subConditionDataPath = StringUtils.join(conditionDataPathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, conditionDataPathSegments.length);
423            if (modelItem instanceof CompositeDefinition compositeDefinition)
424            {
425                return _getValueFromComposite(compositeDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
426            }
427            else if (modelItem instanceof RepeaterDefinition repeaterDefinition)
428            {
429                return _getValueFromRepeater(repeaterDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
430            }
431            else if (modelItem instanceof ContentElementDefinition contentElementDefinition)
432            {
433                return _getValueFromContentValue(contentElementDefinition, conditionDataPathSegments[0], subConditionDataPath, values, contextualParameters);
434            }
435            else
436            {
437                throw new BadItemTypeException("Unable to retrieve the value at the given path. the the segment '" + conditionDataPathSegments[0] + "' is not a model item accessor");
438            }
439        }
440    }
441    
442    @SuppressWarnings("unchecked")
443    private Object _getValueFromComposite(CompositeDefinition compositeDefinition, String compositeName, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
444    {
445        Object value = values.get(compositeName);
446        if (value == null)
447        {
448            return null;
449        }
450        
451        if (value instanceof Map)
452        {
453            return getValueFromMap(compositeDefinition, subConditionDataPath, (Map<String, Object>) value, contextualParameters);
454        }
455        else
456        {
457            throw new BadItemTypeException("Unable to retrieve the value at the given path. the value of the segment '" + compositeName + "' for composite is not a Map");
458        }
459    }
460    
461    @SuppressWarnings("unchecked")
462    private Object _getValueFromRepeater(RepeaterDefinition repeaterDefinition, String repeaterNameAndPositionSegment, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters) throws BadItemTypeException
463    {
464        Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(repeaterNameAndPositionSegment);
465        String repeaterName = repeaterNameAndEntryPosition.getLeft();
466        
467        Object value = values.get(repeaterName);
468        if (value == null)
469        {
470            return null;
471        }
472        
473        if (value instanceof List || value instanceof SynchronizableRepeater)
474        {
475            Integer entryPosition = repeaterNameAndEntryPosition.getRight();
476            List<Map<String, Object>> entries = value instanceof List ? (List<Map<String, Object>>) value : ((SynchronizableRepeater) value).getEntries();
477            SynchronizableRepeater.Mode mode = value instanceof SynchronizableRepeater ? ((SynchronizableRepeater) value).getMode() : SynchronizableRepeater.Mode.REPLACE_ALL;
478            
479            if (mode == SynchronizableRepeater.Mode.REPLACE_ALL)
480            {
481                Map<String, Object> entry = entries.get(entryPosition - 1);
482                return getValueFromMap(repeaterDefinition, subConditionDataPath, entry, contextualParameters);
483            }
484            else if (mode == SynchronizableRepeater.Mode.REPLACE)
485            {
486                int indexOfEntryInReplaceEntries = ((SynchronizableRepeater) value).getReplacePositions().indexOf(entryPosition);
487                Map<String, Object> entry = entries.get(indexOfEntryInReplaceEntries);
488                return getValueFromMap(repeaterDefinition, subConditionDataPath, entry, contextualParameters);
489            }
490            else // mode == SynchronizableRepeater.Mode.APPEND
491            {
492                Set<Integer> removedEntries = ((SynchronizableRepeater) value).getRemovedEntries();
493                if (removedEntries.contains(entryPosition))
494                {
495                    // Non sense, we should not evaluate disable conditions on a data in a repeater entry that will be removed
496                    return null;
497                }
498                
499                ModelAwareRepeater repeater = null;
500                if (contextualParameters.containsKey(__DATA_HOLDER_PARAMETER_KEY))
501                {
502                    ModelAwareDataAwareAmetysObject ametysObject = (ModelAwareDataAwareAmetysObject) contextualParameters.get(__DATA_HOLDER_PARAMETER_KEY);
503                    repeater = ametysObject.getRepeater(repeaterDefinition.getPath());
504                }
505                
506                int actualRepeaterSize = repeater != null ? repeater.getSize() : 0;
507                
508                int repeaterSizeAfterRemoving = actualRepeaterSize - removedEntries.size();
509                int positionInAppendedEntries = entryPosition - repeaterSizeAfterRemoving;
510                int indexInAppendedEntries = positionInAppendedEntries - 1;
511                Map<String, Object> entry = entries.get(indexInAppendedEntries);
512                return getValueFromMap(repeaterDefinition, subConditionDataPath, entry, contextualParameters);
513            }
514        }
515        else
516        {
517            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>");
518        }
519    }
520    
521    private Object _getValueFromContentValue(ContentElementDefinition contentElementDefinition, String dataName, String subConditionDataPath, Map<String, Object> values, Map<String, Object> contextualParameters)
522    {
523        ElementType<ContentValue> type = contentElementDefinition.getType();
524        Object contentValue = _getElementValueFromMap(contentElementDefinition, dataName, values, contextualParameters);
525        
526        if (contentValue == null)
527        {
528            return null;
529        }
530        
531        if (contentValue.getClass().isArray())
532        {
533            return Arrays.stream((Object[]) contentValue)
534                         .map(type::castValue)
535                         .map(ContentValue::getContent)
536                         .map(content -> content.getValue(subConditionDataPath))
537                         .toArray();
538        }
539
540        return Optional.of(contentValue)
541                       .map(type::castValue)
542                       .map(ContentValue::getContent)
543                       .map(content -> content.getValue(subConditionDataPath))
544                       .orElse(null);
545    }
546    
547    private Object _getElementValueFromMap(ModelItem definition, String dataName, Map<String, Object> values, Map<String, Object> contextualParameters)
548    {
549        Object value = values.get(dataName);
550        if (value == null)
551        {
552            return null;
553        }
554        
555        Object valueFromSyncValue = value;
556        SynchronizationContext synchronizationContext = (SynchronizationContext) contextualParameters.get(SYNCHRONIZATION_CONTEXT_PARAMETER_KEY);
557        if (synchronizationContext != null)
558        {
559            valueFromSyncValue = _getValueFromSynchronizableValue(definition, value, synchronizationContext, contextualParameters);
560        
561            if (valueFromSyncValue == null
562                || value instanceof SynchronizableValue syncValue && syncValue.getMode() == Mode.REMOVE)
563            {
564                return null;
565            }
566        }
567        
568        return valueFromSyncValue;
569    }
570    
571    private Object _getValueFromSynchronizableValue(ModelItem definition, Object value, SynchronizationContext synchronizationContext, Map<String, Object> contextualParameters)
572    {
573        Object valueFromSyncValue = value instanceof SynchronizableValue synchronizableValue ? synchronizableValue.getLocalValue() : value;
574        if (contextualParameters.containsKey(__DATA_HOLDER_PARAMETER_KEY))
575        {
576            ModelAwareDataAwareAmetysObject ametysObject = (ModelAwareDataAwareAmetysObject) contextualParameters.get(__DATA_HOLDER_PARAMETER_KEY);
577            @SuppressWarnings("unchecked")
578            Optional<String> oldConditionDataPath = (Optional<String>) contextualParameters.get(__OLD_CONDITION_PATH_PARAMETER_KEY);
579            valueFromSyncValue = DataHolderHelper.getValueFromSynchronizableValue(value, ametysObject, definition, oldConditionDataPath, synchronizationContext);
580        }
581        
582        return valueFromSyncValue;
583    }
584    
585    /**
586     * Check if the given disable condition is external
587     * 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
588     * @param condition the disable condition to check
589     * @param context the definition context
590     * @return <code>true</code> if the given disable condition is external, <code>false</code> otherwise
591     */
592    public boolean isExternal(DisableCondition condition, DefinitionContext context)
593    {
594        Optional<ModelItem> modelItem = context.getModelItem();
595        Optional<View> view = context.getView();
596        
597        if (modelItem.isPresent() && view.isPresent())
598        {
599            String absolutePath = ModelHelper.getDisableConditionAbsolutePath(condition, modelItem.get().getPath());
600            
601            // If the view item has not been found in the view, the condition is external
602            return !_hasViewItem(view.get(), absolutePath);
603        }
604        else
605        {
606            // No information permit to check if the condition is external or not, consider it as not external
607            return false;
608        }
609    }
610    
611    private boolean _hasViewItem(ViewItemContainer viewItemContainer, String path)
612    {
613        String[] pathSegments = StringUtils.split(path, ModelItem.ITEM_PATH_SEPARATOR);
614        
615        if (pathSegments.length == 1)
616        {
617            // Check if the referenced item is in the view
618            return viewItemContainer.hasModelViewItem(pathSegments[0]);
619        }
620        else
621        {
622            // Check first segment of the path
623            if (viewItemContainer.hasModelViewItem(pathSegments[0]))
624            {
625                ModelViewItem modelViewItem = viewItemContainer.getModelViewItem(pathSegments[0]);
626                if (modelViewItem instanceof ViewItemContainer subViewItemContainer)
627                {
628                    String subPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
629                    return _hasViewItem(subViewItemContainer, subPath);
630                }
631                else
632                {
633                    // If the first segment is an accessor but not a container (linked contents),
634                    // we do not check the other segments. The condition will be exploded
635                    return true;
636                }
637            }
638            else
639            {
640                // The item of the first path segment has not been found in the view
641                return false;
642            }
643        }
644    }
645}