001/*
002 *  Copyright 2022 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.data.holder.impl;
017
018import java.lang.reflect.Array;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026
027import org.apache.commons.lang3.StringUtils;
028import org.apache.commons.lang3.tuple.Pair;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031import org.xml.sax.ContentHandler;
032import org.xml.sax.SAXException;
033
034import org.ametys.cms.data.ContentValue;
035import org.ametys.cms.data.ametysobject.ModelAwareDataAwareAmetysObject;
036import org.ametys.cms.data.holder.IndexableDataHolder;
037import org.ametys.cms.data.holder.group.IndexableComposite;
038import org.ametys.cms.data.holder.group.IndexableRepeater;
039import org.ametys.cms.data.holder.group.impl.DefaultModelAwareComposite;
040import org.ametys.cms.data.holder.group.impl.DefaultModelAwareRepeater;
041import org.ametys.cms.model.ContentElementDefinition;
042import org.ametys.cms.model.properties.Property;
043import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
044import org.ametys.plugins.repository.data.ametysobject.DataAwareAmetysObject;
045import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
046import org.ametys.plugins.repository.data.holder.DataHolder;
047import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
048import org.ametys.plugins.repository.data.holder.group.ModelAwareComposite;
049import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
050import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
051import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
052import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater;
053import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
054import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
055import org.ametys.plugins.repository.data.holder.values.UntouchedValue;
056import org.ametys.plugins.repository.data.holder.values.ValueContext;
057import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
058import org.ametys.plugins.repository.data.type.RepositoryElementType;
059import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType;
060import org.ametys.plugins.repository.data.type.RepositoryModelItemType;
061import org.ametys.plugins.repository.model.CompositeDefinition;
062import org.ametys.plugins.repository.model.RepeaterDefinition;
063import org.ametys.plugins.repository.model.RepositoryDataContext;
064import org.ametys.runtime.model.DefinitionContext;
065import org.ametys.runtime.model.ElementDefinition;
066import org.ametys.runtime.model.ModelHelper;
067import org.ametys.runtime.model.ModelItem;
068import org.ametys.runtime.model.ModelItemContainer;
069import org.ametys.runtime.model.ModelViewItem;
070import org.ametys.runtime.model.ModelViewItemGroup;
071import org.ametys.runtime.model.View;
072import org.ametys.runtime.model.ViewElement;
073import org.ametys.runtime.model.ViewHelper;
074import org.ametys.runtime.model.ViewItem;
075import org.ametys.runtime.model.ViewItemAccessor;
076import org.ametys.runtime.model.exception.BadDataPathCardinalityException;
077import org.ametys.runtime.model.exception.BadItemTypeException;
078import org.ametys.runtime.model.exception.UndefinedItemPathException;
079import org.ametys.runtime.model.type.DataContext;
080import org.ametys.runtime.model.type.ElementType;
081import org.ametys.runtime.model.type.ModelItemType;
082
083/**
084 * Default implementation for data holder with model
085 */
086public class DefaultModelAwareDataHolder implements IndexableDataHolder
087{
088    private static final Logger __LOGGER = LoggerFactory.getLogger(ModelAwareDataHolder.class);
089    
090    /** Repository data to use to store data in the repository */
091    protected RepositoryData _repositoryData;
092    
093    /** Parent of the current {@link DataHolder} */
094    protected Optional<? extends IndexableDataHolder> _parent;
095    
096    /** Root {@link DataHolder} */
097    protected IndexableDataHolder _root;
098    
099    /** Model containers to use to get information about definitions */
100    protected Collection<? extends ModelItemContainer> _itemContainers;
101    
102    /**
103     * Creates a default model aware data holder
104     * @param repositoryData the repository data to use
105     * @param itemContainer the model container to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types.
106     */
107    public DefaultModelAwareDataHolder(RepositoryData repositoryData, ModelItemContainer itemContainer)
108    {
109        this(repositoryData, itemContainer, Optional.empty(), Optional.empty());
110    }
111    
112    /**
113     * Creates a default model aware data holder
114     * @param repositoryData the repository data to use
115     * @param itemContainer the model container to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types.
116     * @param parent the optional parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder}
117     * @param root the root {@link DataHolder}
118     */
119    public DefaultModelAwareDataHolder(RepositoryData repositoryData, ModelItemContainer itemContainer, Optional<? extends IndexableDataHolder> parent, Optional<? extends IndexableDataHolder> root)
120    {
121        this(repositoryData, List.of(itemContainer), parent, root);
122    }
123    
124    /**
125     * Creates a default model aware data holder
126     * @param repositoryData the repository data to use
127     * @param itemContainers the model containers to use to get information about definitions. Must match the given repository data. A repository data can have several item containers. For example, a content can have several content types.
128     * @param parent the parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder}
129     * @param root the root {@link DataHolder}
130     */
131    public DefaultModelAwareDataHolder(RepositoryData repositoryData, Collection<? extends ModelItemContainer> itemContainers, Optional<? extends IndexableDataHolder> parent, Optional<? extends IndexableDataHolder> root)
132    {
133        _repositoryData = repositoryData;
134        _itemContainers = itemContainers;
135        _ensureNonNullItemContainers();
136        
137        _parent = parent;
138        _root = root.map(IndexableDataHolder.class::cast)
139                    .or(() -> _parent.map(IndexableDataHolder::getRootDataHolder)) // if no root is specified but a parent, the root is the parent's root
140                    .orElse(this); // if no root or parent is specified, the root is the current DataHolder
141    }
142    
143    private void _ensureNonNullItemContainers()
144    {
145        for (ModelItemContainer itemContainer : _itemContainers)
146        {
147            if (itemContainer == null)
148            {
149                throw new NullPointerException(String.format("Invalid item containers for creating DefaultModelAwareDataHolder, one of them is null: %s", _itemContainers));
150            }
151        }
152    }
153    
154    public IndexableComposite getComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
155    {
156        Object value = getValue(compositePath);
157        return _getCompositeFromValue(value, compositePath);
158    }
159    
160    public IndexableComposite getLocalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
161    {
162        Object value = getLocalValue(compositePath);
163        return _getCompositeFromValue(value, compositePath);
164    }
165    
166    public IndexableComposite getExternalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
167    {
168        Object value = getExternalValue(compositePath);
169        return _getCompositeFromValue(value, compositePath);
170    }
171    
172    private IndexableComposite _getCompositeFromValue(Object value, String compositePath)
173    {
174        if (value == null)
175        {
176            return null;
177        }
178        else if (value instanceof IndexableComposite composite)
179        {
180            return composite;
181        }
182        else
183        {
184            throw new BadItemTypeException("The item at path '" + compositePath + "' is not a composite.");
185        }
186    }
187
188    public IndexableRepeater getRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
189    {
190        Object value = getValue(repeaterPath);
191        return _getRepeaterFromValue(value, repeaterPath);
192    }
193    
194    public IndexableRepeater getLocalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
195    {
196        Object value = getLocalValue(repeaterPath);
197        return _getRepeaterFromValue(value, repeaterPath);
198    }
199    
200    public IndexableRepeater getExternalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
201    {
202        Object value = getExternalValue(repeaterPath);
203        return _getRepeaterFromValue(value, repeaterPath);
204    }
205    
206    private IndexableRepeater _getRepeaterFromValue(Object value, String repeaterPath)
207    {
208        if (value == null)
209        {
210            return null;
211        }
212        else if (value instanceof IndexableRepeater repeater)
213        {
214            return repeater;
215        }
216        else
217        {
218            throw new BadItemTypeException("The data at path '" + repeaterPath + "' is not a repeater.");
219        }
220    }
221    
222    public <T> T getValue(String dataPath, boolean allowMultiValuedPathSegments) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
223    {
224        return _getValue(dataPath, allowMultiValuedPathSegments, Optional.empty());
225    }
226    
227    public <T> T getLocalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
228    {
229        return _getValue(dataPath, false, Optional.of(ExternalizableDataStatus.LOCAL));
230    }
231    
232    public <T> T getExternalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
233    {
234        return _getValue(dataPath, false, Optional.of(ExternalizableDataStatus.EXTERNAL));
235    }
236    
237    private <T> T _getValue(String dataPath, boolean allowMultiValuedPathSegments, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
238    {
239        _checkDefinition(dataPath, status.isPresent(), "Unable to retrieve the value at path '" + dataPath + "'.");
240    
241        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
242        
243        if (pathSegments == null || pathSegments.length < 1)
244        {
245            throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty.");
246        }
247        else if (pathSegments.length == 1)
248        {
249            // Simple path => get the value
250            ModelItem modelItem = getDefinition(dataPath);
251            String dataName = _getFinalDataName(dataPath, status);
252            
253            if (modelItem instanceof Property property)
254            {
255                return _getPropertyValue(property);
256            }
257            else if (modelItem instanceof ElementDefinition elementDefinition)
258            {
259                return _getElementValue(elementDefinition, dataName);
260            }
261            else
262            {
263                return _getGroupValue(modelItem, dataName);
264            }
265        }
266        else
267        {
268            if (isMultiple(pathSegments[0]))
269            {
270                if (allowMultiValuedPathSegments)
271                {
272                    return _getMultipleValues(dataPath);
273                }
274                else
275                {
276                    // Multiple items are allowed only at the last segment of the data path
277                    throw new BadDataPathCardinalityException("Unable to retrieve the value at path '" + dataPath + "'. The segment '" + pathSegments[0] + "' refers to a multiple data and can not be used inside the data path.");
278                }
279            }
280            else
281            {
282                // Path where first part is a data holder
283                ModelAwareDataHolder dataHolder = getValue(pathSegments[0]);
284                if (dataHolder == null)
285                {
286                    return null;
287                }
288                else
289                {
290                    String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
291                    return status.isPresent()
292                            ? ExternalizableDataStatus.EXTERNAL.equals(status.get())
293                                    ? dataHolder.getExternalValue(subDataPath)
294                                    : dataHolder.getLocalValue(subDataPath)
295                            : dataHolder.getValue(subDataPath, allowMultiValuedPathSegments);
296                }
297            }
298        }
299    }
300    
301    public ExternalizableDataStatus getStatus(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadDataPathCardinalityException
302    {
303        _checkDefinition(dataPath, true, "Unable to retrieve the value at path '" + dataPath + "'.");
304        
305        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
306        
307        if (pathSegments == null || pathSegments.length < 1)
308        {
309            throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty.");
310        }
311        else if (pathSegments.length == 1)
312        {
313            if (_repositoryData.hasValue(dataPath + STATUS_SUFFIX))
314            {
315                String status = _repositoryData.getString(dataPath + STATUS_SUFFIX);
316                return ExternalizableDataStatus.valueOf(status.toUpperCase());
317            }
318            else
319            {
320                return ExternalizableDataStatus.LOCAL;
321            }
322        }
323        else
324        {
325            if (isMultiple(pathSegments[0]))
326            {
327                // Multiple items are allowed only at the last segment of the data path
328                throw new BadDataPathCardinalityException("Unable to retrieve the value at path '" + dataPath + "'. The segment '" + pathSegments[0] + "' refers to a multiple data and can not be used inside the data path.");
329            }
330            else
331            {
332                // Path where first part is a data holder
333                ModelAwareDataHolder dataHolder = getValue(pathSegments[0]);
334                if (dataHolder == null)
335                {
336                    return null;
337                }
338                else
339                {
340                    String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
341                    return dataHolder.getStatus(subDataPath);
342                }
343            }
344        }
345    }
346    
347    @SuppressWarnings("unchecked")
348    private <T> T _getPropertyValue(Property property)
349    {
350        return getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametyObject ? (T) property.getValue(ametyObject) : null;
351    }
352    
353    @SuppressWarnings("unchecked")
354    private <T> T _getElementValue(ElementDefinition definition, String dataName)
355    {
356        RepositoryElementType type = (RepositoryElementType) definition.getType();
357        Object value = type.read(_repositoryData, dataName);
358
359        if (definition.isMultiple() && type.getManagedClass().isInstance(value))
360        {
361            // The value is single but should be an array. Create the array with the single value
362            T arrayValue = (T) Array.newInstance(type.getManagedClass(), 1);
363            Array.set(arrayValue, 0, value);
364            return arrayValue;
365        }
366        else if (!definition.isMultiple() && type.getManagedClassArray().isInstance(value))
367        {
368            // The value is multiple but should be single. Retrieve the first value of the array
369            return Array.getLength(value) > 0 ? (T) Array.get(value, 0) : null;
370        }
371        else
372        {
373            return (T) value;
374        }
375    }
376    
377    @SuppressWarnings("unchecked")
378    private <T> T _getGroupValue(ModelItem modelItem, String dataName)
379    {
380        if (modelItem instanceof RepeaterDefinition)
381        {
382            Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataName);
383            if (repeaterNameAndEntryPosition != null)
384            {
385                return (T) DataHolderHelper.getRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight());
386            }
387            else
388            {
389                return (T) _getRepeater(dataName, (RepeaterDefinition) modelItem);
390            }
391        }
392        else
393        {
394            return (T) _getComposite(dataName, (CompositeDefinition) modelItem);
395        }
396    }
397
398    @SuppressWarnings("unchecked")
399    private <T> T _getMultipleValues(String dataPath)
400    {
401        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
402        String subDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
403        Class managedClass = _getManagedClass(this, dataPath);
404        
405        Object segmentValue = getValue(pathSegments[0]);
406        if (segmentValue == null)
407        {
408            return (T) Array.newInstance(managedClass, 0);
409        }
410
411        if (segmentValue instanceof ModelAwareRepeater)
412        {
413            ModelAwareRepeater repeater = (ModelAwareRepeater) segmentValue;
414            return DataHolderHelper.aggregateMultipleValues(repeater.getEntries(), subDataPath, managedClass);
415        }
416        else
417        {
418            ModelAwareDataHolder[] dataHolders = (ModelAwareDataHolder[]) segmentValue;
419            return DataHolderHelper.aggregateMultipleValues(Arrays.asList(dataHolders), subDataPath, managedClass);
420        }
421    }
422
423    private Class _getManagedClass(ModelAwareDataHolder dataHolder, String dataPath)
424    {
425        Class managedClass;
426        ModelItem modelItem = dataHolder.getDefinition(dataPath);
427        if (modelItem instanceof ElementDefinition)
428        {
429            managedClass = ((ElementDefinition) modelItem).getType().getManagedClass();
430        }
431        else
432        {
433            if (modelItem instanceof RepeaterDefinition)
434            {
435                Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath);
436                managedClass = repeaterNameAndEntryPosition != null ? _getRepeaterEntryClass() : _getRepeaterClass();
437            }
438            else
439            {
440                managedClass = _getCompositeClass();
441            }
442        }
443        return managedClass;
444    }
445    
446    /**
447     * Retrieves the class of the managed repeater entries
448     * @return the class of the managed repeater entries
449     */
450    protected Class _getRepeaterEntryClass()
451    {
452        return ModelAwareRepeaterEntry.class;
453    }
454    
455    /**
456     * Retrieves the class of the managed repeaters
457     * @return the class of the managed repeaters
458     */
459    protected Class _getRepeaterClass()
460    {
461        return ModelAwareRepeater.class;
462    }
463    
464    /**
465     * Retrieves the class of the managed composites
466     * @return the class of the managed composites
467     */
468    protected Class _getCompositeClass()
469    {
470        return ModelAwareComposite.class;
471    }
472    
473    public <T> T getValue(String dataPath, boolean useDefaultFromModel, T defaultValue) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
474    {
475        _checkDefinition(dataPath, "Unable to retrieve the value at path '" + dataPath + "'.");
476        
477        if (hasValue(dataPath))
478        {
479            return  getValue(dataPath);
480        }
481        
482        if (useDefaultFromModel)
483        {
484            ModelItem modelItem = getDefinition(dataPath);
485     
486            if (modelItem instanceof ElementDefinition)
487            {
488                @SuppressWarnings("unchecked")
489                T defaultFromModel = (T) ((ElementDefinition) modelItem).getDefaultValue();
490                if (defaultFromModel != null)
491                {
492                    return defaultFromModel;
493                }
494            }
495        }
496
497        return defaultValue;
498    }
499    
500    /**
501     * Retrieves the composite with the given name
502     * @param name name of the composite to retrieve
503     * @param compositeDefinition the definition of the composite to retrieve
504     * @return the composite
505     * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite
506     */
507    protected ModelAwareComposite _getComposite(String name, CompositeDefinition compositeDefinition) throws BadItemTypeException
508    {
509        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) compositeDefinition.getType();
510        RepositoryData compositeRepositoryData = type.read(_repositoryData, name);
511        
512        if (compositeRepositoryData != null)
513        {
514            return new DefaultModelAwareComposite(compositeRepositoryData, compositeDefinition, this, _root);
515        }
516        else
517        {
518            return null;
519        }
520    }
521    
522    /**
523     * Retrieves the repeater with the given name
524     * @param name name of the repeater to retrieve
525     * @param repeaterDefinition the definition of the repeater to retrieve
526     * @return the repeater
527     * @throws BadItemTypeException if the value stored in the repository with the given name is not a repeater
528     */
529    protected ModelAwareRepeater _getRepeater(String name, RepeaterDefinition repeaterDefinition) throws BadItemTypeException
530    {
531        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) repeaterDefinition.getType();
532        RepositoryData repeaterRepositoryData = type.read(_repositoryData, name);
533        
534        if (repeaterRepositoryData != null)
535        {
536            return new DefaultModelAwareRepeater(repeaterRepositoryData, repeaterDefinition, this, _root);
537        }
538        else
539        {
540            return null;
541        }
542    }
543    
544    public boolean hasValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
545    {
546        return _hasValue(dataPath, Optional.empty());
547    }
548    
549    public boolean hasLocalValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
550    {
551        return _hasValue(dataPath, Optional.of(ExternalizableDataStatus.LOCAL));
552    }
553    
554    public boolean hasExternalValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
555    {
556        return _hasValue(dataPath, Optional.of(ExternalizableDataStatus.EXTERNAL));
557    }
558    
559    @SuppressWarnings("unchecked")
560    private boolean _hasValue(String dataPath, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, BadDataPathCardinalityException
561    {
562        if (!hasDefinition(dataPath))
563        {
564            return false;
565        }
566        
567        if (StringUtils.isEmpty(dataPath))
568        {
569            throw new IllegalArgumentException("Unable to check if there is a non empty value at the given path. This path is empty.");
570        }
571        else if (!dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR))
572        {
573            if (DataHolderHelper.isRepeaterEntryPath(dataPath))
574            {
575                Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath);
576                return DataHolderHelper.hasNonEmptyRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight());
577            }
578            else
579            {
580                if (getDefinition(dataPath) instanceof Property property)
581                {
582                    if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject)
583                    {
584                        return property.getValue(ametysObject) != null;
585                    }
586                    else
587                    {
588                        return false;
589                    }
590                }
591                else
592                {
593                    RepositoryModelItemType type = getType(dataPath);
594                    String dataName = _getFinalDataName(dataPath, status);
595                        
596                    try
597                    {
598                        return type.hasNonEmptyValue(_repositoryData, dataName);
599                    }
600                    catch (BadItemTypeException e)
601                    {
602                        return false;
603                    }
604                }
605            }
606        }
607        else
608        {
609            String parentPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
610            
611            // Multiple items are allowed only at the last segment of the data path
612            if (isMultiple(parentPath))
613            {
614                throw new BadDataPathCardinalityException("Unable to check if there is a value at path '" + dataPath + "'. The segment '" + parentPath + "' refers to a multiple data and can not be used inside the data path.");
615            }
616            
617            try
618            {
619                ModelAwareDataHolder parent = getValue(parentPath);
620                if (parent == null)
621                {
622                    return false;
623                }
624                else
625                {
626                    String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
627                    return status.isPresent()
628                            ? ExternalizableDataStatus.EXTERNAL.equals(status.get())
629                                    ? parent.hasExternalValue(childName)
630                                    : parent.hasLocalValue(childName)
631                            : parent.hasValue(childName);
632                }
633            }
634            catch (BadItemTypeException e)
635            {
636                return false;
637            }
638        }
639    }
640
641    public boolean hasValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
642    {
643        return _hasValueOrEmpty(dataPath, Optional.empty());
644    }
645    
646    public boolean hasLocalValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
647    {
648        return _hasValueOrEmpty(dataPath, Optional.of(ExternalizableDataStatus.LOCAL));
649    }
650    
651    public boolean hasExternalValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
652    {
653        return _hasValueOrEmpty(dataPath, Optional.of(ExternalizableDataStatus.EXTERNAL));
654    }
655    
656    @SuppressWarnings("unchecked")
657    private boolean _hasValueOrEmpty(String dataPath, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, BadDataPathCardinalityException
658    {
659        if (!hasDefinition(dataPath))
660        {
661            return false;
662        }
663        
664        if (StringUtils.isEmpty(dataPath))
665        {
666            throw new IllegalArgumentException("Unable to check if there is a value at the given path. This path is empty.");
667        }
668        else if (!dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR))
669        {
670            if (DataHolderHelper.isRepeaterEntryPath(dataPath))
671            {
672                Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath);
673                return DataHolderHelper.hasRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight());
674            }
675            else
676            {
677                if (getDefinition(dataPath) instanceof Property property)
678                {
679                    if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject)
680                    {
681                        return property.getValue(ametysObject) != null;
682                    }
683                    else
684                    {
685                        return false;
686                    }
687                }
688                else
689                {
690                    RepositoryModelItemType type = getType(dataPath);
691                    String dataName = _getFinalDataName(dataPath, status);
692                    return type.hasValue(_repositoryData, dataName);
693                }
694            }
695        }
696        else
697        {
698            String parentPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
699            
700            // Multiple items are allowed only at the last segment of the data path
701            if (isMultiple(parentPath))
702            {
703                throw new BadDataPathCardinalityException("Unable to check if there is a value at path '" + dataPath + "'. The segment '" + parentPath + "' refers to a multiple data and can not be used inside the data path.");
704            }
705            
706            try
707            {
708                ModelAwareDataHolder parent = getValue(parentPath);
709                if (parent == null)
710                {
711                    return false;
712                }
713                else
714                {
715                    String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
716                    return status.isPresent()
717                            ? ExternalizableDataStatus.EXTERNAL.equals(status.get())
718                                    ? parent.hasExternalValueOrEmpty(childName)
719                                    : parent.hasLocalValueOrEmpty(childName)
720                            : parent.hasValueOrEmpty(childName);
721                }
722            }
723            catch (BadItemTypeException e)
724            {
725                return false;
726            }
727        }
728    }
729    
730    /**
731     * Retrieves the name of the data according to the given status
732     * @param dataName the name of the data
733     * @param status the status
734     * @return the final name of the data
735     */
736    protected String _getFinalDataName(String dataName, Optional<ExternalizableDataStatus> status)
737    {
738        if (status.isPresent() && getStatus(dataName) != status.get())
739        {
740            return dataName + ALTERNATIVE_SUFFIX;
741        }
742        
743        return dataName;
744    }
745    
746    public Collection< ? extends ModelItemContainer> getModel()
747    {
748        return _itemContainers;
749    }
750    
751    public ModelItem getDefinition(String path) throws IllegalArgumentException, UndefinedItemPathException
752    {
753        try
754        {
755            ModelItem definition = IndexableDataHolder.super.getDefinition(path);
756            
757            // A definition has been found, ok
758            return definition;
759        }
760        catch (UndefinedItemPathException e)
761        {
762            // Look for system properties
763            if (StringUtils.contains(path, ModelItem.ITEM_PATH_SEPARATOR))
764            {
765                String parentDataPath = StringUtils.substringBeforeLast(path, ModelItem.ITEM_PATH_SEPARATOR);
766                ModelItem modelItem = getDefinition(parentDataPath);
767                if (modelItem instanceof ContentElementDefinition)
768                {
769                    SystemPropertyExtensionPoint contentSystemPropertyExtensionPoint = IndexableDataHolderHelper.getContentSystemPropertyExtensionPoint();
770                    String propertyName = StringUtils.substringAfterLast(path, ModelItem.ITEM_PATH_SEPARATOR);
771                    if (contentSystemPropertyExtensionPoint.hasExtension(propertyName))
772                    {
773                        return contentSystemPropertyExtensionPoint.getExtension(propertyName);
774                    }
775                }
776            }
777            else if (getParentDataHolder().isEmpty()
778                    && getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject
779                    && ametysObject.getSystemPropertyExtensionPoint().isPresent())
780            {
781                SystemPropertyExtensionPoint systemPropertyExtensionPoint = ametysObject.getSystemPropertyExtensionPoint().get();
782                if (systemPropertyExtensionPoint.hasExtension(path))
783                {
784                    return systemPropertyExtensionPoint.getExtension(path);
785                }
786            }
787            
788            // No system property has been found, throw the UndefinedItemPathException
789            throw e;
790        }
791    }
792
793    public Collection<String> getDataNames()
794    {
795        return ModelHelper.getModelItems(getModel())
796                          .stream()
797                          .map(ModelItem::getName)
798                          .filter(this::hasValueOrEmpty)
799                          .toList();
800    }
801    
802    @SuppressWarnings("unchecked")
803    public void dataToSAX(ContentHandler contentHandler, String dataPath, DataContext context) throws SAXException
804    {
805        _checkDefinition(dataPath, "Unable to generate SAX events for the data at path '" + dataPath + "'.");
806        
807        DataContext newContext = _addObjectInfoToContext(context);
808        if (StringUtils.isBlank(newContext.getDataPath()))
809        {
810            newContext.withDataPath(dataPath);
811        }
812        
813        if (IndexableDataHolderHelper.renderValue(this, dataPath, newContext, false) && IndexableDataHolderHelper.hasValue(this, dataPath, newContext))
814        {
815            ModelItem modelItem = getDefinition(dataPath);
816            if (modelItem instanceof Property property)
817            {
818                ModelAwareDataAwareAmetysObject ametysObject = _getPropertysAmetysObject(dataPath);
819                property.valueToSAX(contentHandler, ametysObject, newContext);
820            }
821            else
822            {
823                ModelItemType type = modelItem.getType();
824                Object value = getValue(dataPath);
825                
826                type.valueToSAX(contentHandler, modelItem.getName(), value, newContext);
827            }
828        }
829    }
830    
831    public void dataToSAX(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException
832    {
833        DataContext newContext = _addObjectInfoToContext(context);
834        IndexableDataHolderHelper.dataToSAX(this, contentHandler, viewItemAccessor, newContext, false);
835    }
836    
837    public void dataToSAXForEdition(ContentHandler contentHandler, ViewItemAccessor viewItemAccessor, DataContext context) throws SAXException, BadItemTypeException
838    {
839        DataContext newContext = _addObjectInfoToContext(context);
840        IndexableDataHolderHelper.dataToSAX(this, contentHandler, viewItemAccessor, newContext, true);
841        
842        if (viewItemAccessor instanceof View view)
843        {
844            DefinitionContext definitionContext = DefinitionContext.newInstance()
845                                                                   .withView(view);
846            IndexableDataHolderHelper.externalDisableConditionsToSAX(this, contentHandler, viewItemAccessor, definitionContext, newContext);
847        }
848    }
849    
850    @SuppressWarnings("unchecked")
851    public Object dataToJSON(String dataPath, DataContext context)
852    {
853        _checkDefinition(dataPath, "Unable to convert the data at path '" + dataPath + "' to JSON.");
854        
855        DataContext newContext = _addObjectInfoToContext(context)
856                                .withDataPath(dataPath);
857        
858        if (IndexableDataHolderHelper.renderValue(this, dataPath, newContext, false) && IndexableDataHolderHelper.hasValue(this, dataPath, newContext))
859        {
860            ModelItem modelItem = getDefinition(dataPath);
861            if (modelItem instanceof Property property)
862            {
863                ModelAwareDataAwareAmetysObject ametysObject = _getPropertysAmetysObject(dataPath);
864                return property.valueToJSON(ametysObject, newContext);
865            }
866            else
867            {
868                ModelItemType type = modelItem.getType();
869                Object value = getValue(dataPath);
870                
871                return type.valueToJSONForClient(value, newContext);
872            }
873        }
874        else
875        {
876            return null;
877        }
878    }
879    
880    /**
881     * Retrieves the ametys object containing the property at the given path
882     * @param dataPath the path of the property
883     * @return the ametys object containing the property
884     * @throws UndefinedItemPathException if the given path does not represent a property
885     */
886    protected ModelAwareDataAwareAmetysObject _getPropertysAmetysObject(String dataPath) throws UndefinedItemPathException
887    {
888        if (StringUtils.contains(dataPath, ModelItem.ITEM_PATH_SEPARATOR))
889        {
890            String parentDataPath = StringUtils.substringBeforeLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
891            ContentValue value = getValue(parentDataPath);
892            return value.getContent();
893        }
894        else if (getRootDataHolder() instanceof ModelAwareDataAwareAmetysObject ametysObject)
895        {
896            return ametysObject;
897        }
898        else
899        {
900            throw new UndefinedItemPathException("There is no property at path '" + dataPath + "'");
901        }
902    }
903    
904    public Map<String, Object> dataToJSON(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException
905    {
906        DataContext newContext = _addObjectInfoToContext(context);
907        return IndexableDataHolderHelper.dataToJSON(this, viewItemAccessor, newContext, false);
908    }
909    
910    public Map<String, Object> dataToJSONForEdition(ViewItemAccessor viewItemAccessor, DataContext context) throws BadItemTypeException
911    {
912        DataContext newContext = _addObjectInfoToContext(context);
913        Map<String, Object> json = IndexableDataHolderHelper.dataToJSON(this, viewItemAccessor, newContext, true);
914        
915        if (viewItemAccessor instanceof View view)
916        {
917            DefinitionContext definitionContext = DefinitionContext.newInstance()
918                                                                   .withView(view);
919            
920            Map<String, Boolean> conditionsValues = IndexableDataHolderHelper.getExternalDisableConditionsValues(this, viewItemAccessor, definitionContext, newContext);
921            if (!conditionsValues.isEmpty())
922            {
923                json.put(IndexableDataHolderHelper.EXTERNAL_DISABLE_CONDITIONS_VALUES, conditionsValues);
924            }
925        }
926        
927        return json;
928    }
929    
930    public Map<String, Object> dataToMap(ViewItemAccessor viewItemAccessor, DataContext context)
931    {
932        DataContext newContext = _addObjectInfoToContext(context);
933        return IndexableDataHolderHelper.dataToMap(this, viewItemAccessor, newContext);
934    }
935    
936    public boolean hasDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values) throws UndefinedItemPathException, BadItemTypeException
937    {
938        return hasDifferences(viewItemAccessor, values, _createSynchronizationContextInstance());
939    }
940    
941    public boolean hasDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException
942    {
943        return _hasDifferences(viewItemAccessor, values, context);
944    }
945    
946    /**
947     * Check if there are differences between the given values and the current ones
948     * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check
949     * @param values the values to check
950     * @param context the context of the synchronization
951     * @return <code>true</code> if there are differences, <code>false</code> otherwise
952     * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model
953     * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value
954     */
955    protected boolean _hasDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException
956    {
957        for (ViewItem viewItem : viewItemAccessor.getViewItems())
958        {
959            if (viewItem instanceof ModelViewItem)
960            {
961                if (viewItem instanceof ModelViewItemGroup group && _hasDifferencesInGroup(group, values, context)
962                    || viewItem instanceof ViewElement element && _hasDifferencesInElement(element, values, context))
963                {
964                    return true;
965                }
966            }
967            else if (viewItem instanceof ViewItemAccessor accessor && _hasDifferences(accessor, values, context))
968            {
969                return true;
970            }
971        }
972        
973        // No difference has been found
974        return false;
975    }
976
977    public Collection<ModelItem> getDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values) throws UndefinedItemPathException, BadItemTypeException
978    {
979        return getDifferences(viewItemAccessor, values, _createSynchronizationContextInstance());
980    }
981    
982    public Collection<ModelItem> getDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException
983    {
984        return _getDifferences(viewItemAccessor, values, context);
985    }
986    
987    /**
988     * Get the collection of model items where there are differences between the given values and the current ones
989     * @param viewItemAccessor The {@link ViewItemAccessor} for all items to check
990     * @param values the values to check
991     * @param context the context of the synchronization
992     * @return a collection of model items with differences
993     * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model
994     * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value
995     */
996    protected Collection<ModelItem> _getDifferences(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException
997    {
998        Set<ModelItem> modelItems = new HashSet<>();
999        
1000        for (ViewItem viewItem : viewItemAccessor.getViewItems())
1001        {
1002            if (viewItem instanceof ModelViewItem)
1003            {
1004                if (viewItem instanceof ModelViewItemGroup group)
1005                {
1006                    modelItems.addAll(_getDifferencesInGroup(group, values, context));
1007                }
1008                else if (viewItem instanceof ViewElement element)
1009                {
1010                    if (_hasDifferencesInElement(element, values, context))
1011                    {
1012                        modelItems.add(element.getDefinition());
1013                    }
1014                }
1015            }
1016            else if (viewItem instanceof ViewItemAccessor accessor)
1017            {
1018                modelItems.addAll(_getDifferences(accessor, values, context));
1019            }
1020        }
1021        
1022        return modelItems;
1023    }
1024    
1025    /**
1026     * Check if there are differences between the given values and the given group's ones
1027     * @param modelViewItemGroup the group
1028     * @param values the values to check
1029     * @param synchronizationContext the context of the synchronization
1030     * @return <code>true</code> if there are differences, <code>false</code> otherwise
1031     * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model
1032     * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value
1033     */
1034    @SuppressWarnings("unchecked")
1035    protected boolean _hasDifferencesInGroup(ModelViewItemGroup modelViewItemGroup, Map<String, Object> values, SynchronizationContext synchronizationContext) throws UndefinedItemPathException, BadItemTypeException
1036    {
1037        ModelItem modelItem = modelViewItemGroup.getDefinition();
1038        String dataName = modelItem.getName();
1039        Object value = values.get(dataName);
1040        if (value instanceof UntouchedValue)
1041        {
1042            if (__LOGGER.isDebugEnabled())
1043            {
1044                String viewItemPath = ViewHelper.getModelViewItemPath(modelViewItemGroup);
1045                __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath);
1046            }
1047            return false;
1048        }
1049        else if (value == null)
1050        {
1051            ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext);
1052            return DataHolderHelper.hasValueOrEmpty(this, dataName, valueContext);
1053        }
1054        else if (modelItem instanceof RepeaterDefinition)
1055        {
1056            if (value instanceof SynchronizableRepeater || value instanceof List)
1057            {
1058                ModelAwareRepeater repeater = getRepeater(dataName);
1059                SynchronizableRepeater repeaterValues = value instanceof SynchronizableRepeater ? (SynchronizableRepeater) value : SynchronizableRepeater.replaceAll((List<Map<String, Object>>) value, null);
1060
1061                if (repeater == null)
1062                {
1063                    if (__LOGGER.isDebugEnabled())
1064                    {
1065                        String viewItemPath = ViewHelper.getModelViewItemPath(modelViewItemGroup);
1066                        __LOGGER.debug("#hasDifferences[{}] differences detected: repeater will be created", viewItemPath);
1067                    }
1068                    return true;
1069                }
1070                else
1071                {
1072                    return repeater.hasDifferences(modelViewItemGroup, repeaterValues, synchronizationContext);
1073                }
1074            }
1075            else
1076            {
1077                throw new BadItemTypeException("Unable to check differences for the repeater named '" + dataName + "': the given value should be a list containing its entries");
1078            }
1079        }
1080        else
1081        {
1082            if (value instanceof Map)
1083            {
1084                ModelAwareComposite composite = getComposite(dataName);
1085                if (composite == null)
1086                {
1087                    if (__LOGGER.isDebugEnabled())
1088                    {
1089                        String viewItemPath = ViewHelper.getModelViewItemPath(modelViewItemGroup);
1090                        __LOGGER.debug("#hasDifferences[{}] differences detected: composite will be created", viewItemPath);
1091                    }
1092                    return true;
1093                }
1094                else
1095                {
1096                    return composite.hasDifferences(modelViewItemGroup, (Map<String, Object>) value, synchronizationContext);
1097                }
1098            }
1099            else
1100            {
1101                throw new BadItemTypeException("Unable to synchronize the composite named '" + dataName + "': the given value should be a map containing values of all of its items");
1102            }
1103        }
1104    }
1105    
1106    /**
1107     * Get the collection of model items where there are differences between the given values and the given group's ones
1108     * @param modelViewItemGroup the group
1109     * @param values the values to check
1110     * @param synchronizationContext the context of the synchronization
1111     * @return a collection of model items with differences
1112     * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model
1113     * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value
1114     */
1115    @SuppressWarnings("unchecked")
1116    protected Collection<ModelItem> _getDifferencesInGroup(ModelViewItemGroup modelViewItemGroup, Map<String, Object> values, SynchronizationContext synchronizationContext) throws UndefinedItemPathException, BadItemTypeException
1117    {
1118        Set<ModelItem> modelItems = new HashSet<>();
1119        
1120        ModelItem modelItem = modelViewItemGroup.getDefinition();
1121        String dataName = modelItem.getName();
1122        Object value = values.get(dataName);
1123        if (value instanceof UntouchedValue)
1124        {
1125            return modelItems;
1126        }
1127        else if (value == null)
1128        {
1129            ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext);
1130            if (DataHolderHelper.hasValueOrEmpty(this, dataName, valueContext))
1131            {
1132                modelItems.addAll(ViewHelper.getModelItems(modelViewItemGroup));
1133            }
1134        }
1135        else if (modelItem instanceof RepeaterDefinition)
1136        {
1137            if (value instanceof SynchronizableRepeater || value instanceof List)
1138            {
1139                ModelAwareRepeater repeater = getRepeater(dataName);
1140                SynchronizableRepeater repeaterValues = value instanceof SynchronizableRepeater ? (SynchronizableRepeater) value : SynchronizableRepeater.replaceAll((List<Map<String, Object>>) value, null);
1141
1142                if (repeater == null)
1143                {
1144                    modelItems.addAll(ViewHelper.getModelItems(modelViewItemGroup));
1145                }
1146                else
1147                {
1148                    modelItems.addAll(repeater.getDifferences(modelViewItemGroup, repeaterValues, synchronizationContext));
1149                }
1150            }
1151            else
1152            {
1153                throw new BadItemTypeException("Unable to check differences for the repeater named '" + dataName + "': the given value should be a list containing its entries");
1154            }
1155        }
1156        else
1157        {
1158            if (value instanceof Map)
1159            {
1160                ModelAwareComposite composite = getComposite(dataName);
1161                if (composite == null)
1162                {
1163                    modelItems.addAll(ViewHelper.getModelItems(modelViewItemGroup));
1164                }
1165                else
1166                {
1167                    modelItems.addAll(composite.getDifferences(modelViewItemGroup, (Map<String, Object>) value, synchronizationContext));
1168                }
1169            }
1170            else
1171            {
1172                throw new BadItemTypeException("Unable to synchronize the composite named '" + dataName + "': the given value should be a map containing values of all of its items");
1173            }
1174        }
1175        
1176        return modelItems;
1177    }
1178    
1179    /**
1180     * Check if there are differences between the given values and the given element's ones
1181     * @param viewElement the element
1182     * @param values the values to check
1183     * @param synchronizationContext the context of the synchronization
1184     * @return <code>true</code> if there are differences, <code>false</code> otherwise
1185     * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model
1186     * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value
1187     */
1188    protected boolean _hasDifferencesInElement(ViewElement viewElement, Map<String, Object> values, SynchronizationContext synchronizationContext) throws UndefinedItemPathException, BadItemTypeException
1189    {
1190        ElementDefinition definition = viewElement.getDefinition();
1191        String dataName = definition.getName();
1192        
1193        Object valueFromMap = values.get(dataName);
1194        ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext);
1195        
1196        SynchronizableValue syncValue = valueFromMap instanceof SynchronizableValue ? (SynchronizableValue) valueFromMap : new SynchronizableValue(valueFromMap, valueContext.getStatus().orElse(null));
1197        Object value = syncValue.getValue(valueContext.getStatus());
1198
1199        if (!(value instanceof UntouchedValue))
1200        {
1201            Object defaultValue = definition.getDefaultValue();
1202            if (value == null && synchronizationContext.useDefaultFromModel() && defaultValue != null)
1203            {
1204                if (_checkElementDifferences(viewElement, new SynchronizableValue(defaultValue, valueContext.getStatus().orElse(null)), valueContext))
1205                {
1206                    return true;
1207                }
1208            }
1209            else
1210            {
1211                if (values.containsKey(dataName))
1212                {
1213                    if (_checkElementDifferences(viewElement, syncValue, valueContext))
1214                    {
1215                        return true;
1216                    }
1217                }
1218                else if (DataHolderHelper.hasValueOrEmpty(this, dataName, valueContext))
1219                {
1220                    if (__LOGGER.isDebugEnabled())
1221                    {
1222                        String viewItemPath = ViewHelper.getModelViewItemPath(viewElement);
1223                        __LOGGER.debug("#hasDifferences[{}] differences detected: value will be removed", viewItemPath);
1224                    }
1225                    return true;
1226                }
1227            }
1228        }
1229        
1230        if (_checkStatusDifferences(definition, syncValue, synchronizationContext, values.containsKey(dataName)))
1231        {
1232            if (__LOGGER.isDebugEnabled())
1233            {
1234                String viewItemPath = ViewHelper.getModelViewItemPath(viewElement);
1235                __LOGGER.debug("#hasDifferences[{}] differences detected: status will change", viewItemPath);
1236            }
1237            return true;
1238        }
1239        
1240        // No difference has been found
1241        if (__LOGGER.isDebugEnabled())
1242        {
1243            String viewItemPath = ViewHelper.getModelViewItemPath(viewElement);
1244            __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath);
1245        }
1246        return false;
1247    }
1248    
1249    /**
1250     * Check if there are differences between the given value and the given view element's value
1251     * @param viewElement the element
1252     * @param value the value to check
1253     * @param context context of the data to check
1254     * @return <code>true</code> if there are differences, <code>false</code> otherwise
1255     * @throws IllegalArgumentException if the given data name is null or empty
1256     * @throws UndefinedItemPathException if the given data name is not defined by the model
1257     * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set
1258     */
1259    protected boolean _checkElementDifferences(ViewElement viewElement, SynchronizableValue value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException
1260    {
1261        ElementDefinition definition = viewElement.getDefinition();
1262        String dataName = definition.getName();
1263        
1264        if (SynchronizableValue.Mode.REMOVE.equals(value.getMode()))
1265        {
1266            return _checkElementDifferencesInRemoveMode(viewElement, value, context);
1267        }
1268        
1269        if (SynchronizableValue.Mode.APPEND.equals(value.getMode()) && isMultiple(dataName))
1270        {
1271            Object valuesToAppend = DataHolderHelper.getArrayValuesFromSynchronizableValue(value, context);
1272            boolean hasValuesToAppend = Array.getLength(valuesToAppend) > 0;
1273            
1274            if (__LOGGER.isDebugEnabled())
1275            {
1276                String viewItemPath = ViewHelper.getModelViewItemPath(viewElement);
1277                if (hasValuesToAppend)
1278                {
1279                    __LOGGER.debug("#hasDifferences[{}] differences detected: values {} will be appended", viewItemPath, valuesToAppend);
1280                }
1281                else
1282                {
1283                    __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath);
1284                }
1285            }
1286            
1287            return hasValuesToAppend;
1288        }
1289        
1290        boolean hasValueOrEmpty = DataHolderHelper.hasValueOrEmpty(this, dataName, context);
1291        boolean hasEmptyValue = hasValueOrEmpty && !DataHolderHelper.hasValue(this, dataName, context);
1292        Object newValue = value.getValue(context.getStatus());
1293        
1294        if (newValue == null && hasEmptyValue)
1295        {
1296            if (__LOGGER.isDebugEnabled())
1297            {
1298                String viewItemPath = ViewHelper.getModelViewItemPath(viewElement);
1299                __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath);
1300            }
1301            
1302            return false;
1303        }
1304        else if (hasValueOrEmpty)
1305        {
1306            Object oldValue = DataHolderHelper.getValue(this, dataName, context);
1307            ElementType type = ((ElementDefinition) getDefinition(dataName)).getType();
1308            
1309            // Check if there are differences between old and new value
1310            boolean hasDiff = type.compareValues(newValue, oldValue).count() > 0;
1311
1312            if (__LOGGER.isDebugEnabled())
1313            {
1314                String viewItemPath = ViewHelper.getModelViewItemPath(viewElement);
1315                if (hasDiff)
1316                {
1317                    __LOGGER.debug("#hasDifferences[{}] differences detected: {} will replace {}", viewItemPath, newValue, oldValue);
1318                }
1319                else
1320                {
1321                    __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath);
1322                }
1323            }
1324
1325            return hasDiff;
1326        }
1327        else
1328        {
1329            // There was no values at all, one should be set (even empty)
1330
1331            if (__LOGGER.isDebugEnabled())
1332            {
1333                String viewItemPath = ViewHelper.getModelViewItemPath(viewElement);
1334                __LOGGER.debug("#hasDifferences[{}] differences detected: {} will replace emty value", viewItemPath, newValue);
1335            }
1336
1337            return true;
1338        }
1339    }
1340    
1341    /**
1342     * Check if there are differences between the given value and the given view element's value
1343     * @param viewElement the element
1344     * @param value the value to check
1345     * @param context context of the data to check
1346     * @return <code>true</code> if there are differences, <code>false</code> otherwise
1347     * @throws IllegalArgumentException if the given data name is null or empty
1348     * @throws UndefinedItemPathException if the given data name is not defined by the model
1349     * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set
1350     */
1351    protected boolean _checkElementDifferencesInRemoveMode(ViewElement viewElement, SynchronizableValue value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException
1352    {
1353        ElementDefinition definition = viewElement.getDefinition();
1354        String dataName = definition.getName();
1355        
1356        boolean hasValue = DataHolderHelper.hasValueOrEmpty(this, dataName, context);
1357        
1358        if (hasValue && isMultiple(dataName))
1359        {
1360            Object oldValues = DataHolderHelper.getValue(this, dataName, context);
1361            Object valuesToRemove = DataHolderHelper.getArrayValuesFromSynchronizableValue(value, context);
1362            ElementType type = ((ElementDefinition) getDefinition(dataName)).getType();
1363
1364            // Remove the given values from the existent ones
1365            Object newValues = DataHolderHelper.removeValuesInArray(oldValues, valuesToRemove, type);
1366
1367            boolean hasDiff = Array.getLength(oldValues) > Array.getLength(newValues);
1368            
1369            if (__LOGGER.isDebugEnabled())
1370            {
1371                String viewItemPath = ViewHelper.getModelViewItemPath(viewElement);
1372                if (hasDiff)
1373                {
1374                    __LOGGER.debug("#hasDifferences[{}] differences detected: some values of {} will be removed from {}", viewItemPath, valuesToRemove, oldValues);
1375                }
1376                else
1377                {
1378                    __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath);
1379                }
1380            }
1381            
1382            return hasDiff;
1383        }
1384        else
1385        {
1386            if (__LOGGER.isDebugEnabled())
1387            {
1388                String viewItemPath = ViewHelper.getModelViewItemPath(viewElement);
1389                if (hasValue)
1390                {
1391                    Object oldValue = DataHolderHelper.getValue(this, dataName, context);
1392                    __LOGGER.debug("#hasDifferences[{}] differences detected: value {} will be removed", viewItemPath, oldValue);
1393                }
1394                else
1395                {
1396                    __LOGGER.debug("#hasDifferences[{}] no difference detected.", viewItemPath);
1397                }
1398            }
1399            
1400            return hasValue;
1401        }
1402    }
1403    
1404    /**
1405     * Check if the data's status will have to be changed
1406     * @param definition definition of the data
1407     * @param value the value
1408     * @param synchronizationContext the context of the synchronization
1409     * @param doValuesContainData <code>true</code> if the values contain the data, <code>false</code> otherwise
1410     * @return <code>true</code> if the status has changed, <code>false</code> otherwise
1411     */
1412    protected boolean _checkStatusDifferences(ElementDefinition definition, SynchronizableValue value, SynchronizationContext synchronizationContext, boolean doValuesContainData)
1413    {
1414        if (DataHolderHelper.getExternalizableDataProviderExtensionPoint().isDataExternalizable(getRootDataHolder(), definition))
1415        {
1416            String dataName = definition.getName();
1417            ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext);
1418        
1419            ExternalizableDataStatus oldStatus = null;
1420            if (_repositoryData.hasValue(dataName + STATUS_SUFFIX))
1421            {
1422                String status = _repositoryData.getString(dataName + STATUS_SUFFIX);
1423                oldStatus = ExternalizableDataStatus.valueOf(status.toUpperCase());
1424            }
1425            
1426            ExternalizableDataStatus newStatus = value.getExternalizableStatus();
1427            
1428            return synchronizationContext.forceStatusIfNotPresent() && oldStatus == null && newStatus == null && doValuesContainData && valueContext.getStatus().isPresent()
1429                || newStatus != null && !newStatus.equals(oldStatus);
1430        }
1431        else
1432        {
1433            return false;
1434        }
1435    }
1436    
1437    /**
1438     * Creates an instance of {@link SynchronizationContext}
1439     * @param <T> the type of the {@link SynchronizationContext}
1440     * @return the created {@link SynchronizationContext}
1441     */
1442    @SuppressWarnings("unchecked")
1443    protected <T extends SynchronizationContext> T _createSynchronizationContextInstance()
1444    {
1445        return (T) SynchronizationContext.newInstance();
1446    }
1447    
1448    private DataContext _addObjectInfoToContext(DataContext context)
1449    {
1450        ModelAwareDataHolder root = getRootDataHolder();
1451        
1452        RepositoryDataContext newContext = RepositoryDataContext.newInstance(context);
1453        if (root instanceof DataAwareAmetysObject ametysObject)
1454        {
1455            newContext.withObject(ametysObject);
1456        }
1457        
1458        return newContext;
1459    }
1460    
1461    public RepositoryData getRepositoryData()
1462    {
1463        return _repositoryData;
1464    }
1465    
1466    public Optional<? extends IndexableDataHolder> getParentDataHolder()
1467    {
1468        return _parent;
1469    }
1470    
1471    public IndexableDataHolder getRootDataHolder()
1472    {
1473        return _root;
1474    }
1475    
1476    /**
1477     * Check definition for data path
1478     * @param dataPath the data path
1479     * @param errorMsg the error message to throw
1480     */
1481    protected void _checkDefinition(String dataPath, String errorMsg)
1482    {
1483        _checkDefinition(dataPath, false, errorMsg);
1484    }
1485    
1486    /**
1487     * Check definition for data path
1488     * @param dataPath the data path
1489     * @param checkStatusAvailable <code>true</code> to check if the definition supports externalizable data status
1490     * @param errorMsg the error message to throw
1491     */
1492    protected void _checkDefinition(String dataPath, boolean checkStatusAvailable, String errorMsg)
1493    {
1494        // Check if the model exists
1495        if (_itemContainers.isEmpty())
1496        {
1497            throw new UndefinedItemPathException(errorMsg + " No model is defined for this object [" + getRootDataHolder() + "]");
1498        }
1499        
1500        // Check that there is an item at the given path
1501        if (!hasDefinition(dataPath))
1502        {
1503            throw new UndefinedItemPathException(errorMsg + " There is no such item defined by the model.");
1504        }
1505        
1506        // Externalizable data status is not available for properties
1507        if (checkStatusAvailable && getDefinition(dataPath) instanceof Property)
1508        {
1509            throw new UndefinedItemPathException(errorMsg + " A property can't have an externalizable status.");
1510        }
1511    }
1512}