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