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