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