001/*
002 *  Copyright 2018 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.Collection;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023
024import org.apache.commons.lang3.StringUtils;
025import org.apache.commons.lang3.tuple.Pair;
026
027import org.ametys.cms.data.holder.ModifiableIndexableDataHolder;
028import org.ametys.cms.data.holder.group.ModifiableIndexableComposite;
029import org.ametys.cms.data.holder.group.ModifiableIndexableRepeater;
030import org.ametys.cms.data.holder.group.impl.DefaultModifiableModelAwareComposite;
031import org.ametys.cms.data.holder.group.impl.DefaultModifiableModelAwareRepeater;
032import org.ametys.core.util.DateUtils;
033import org.ametys.plugins.repository.RepositoryConstants;
034import org.ametys.plugins.repository.data.DataComment;
035import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus;
036import org.ametys.plugins.repository.data.holder.DataHolder;
037import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
038import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareComposite;
039import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeater;
040import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry;
041import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
042import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater;
043import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
044import org.ametys.plugins.repository.data.holder.values.SynchronizationContext;
045import org.ametys.plugins.repository.data.holder.values.SynchronizationResult;
046import org.ametys.plugins.repository.data.holder.values.UntouchedValue;
047import org.ametys.plugins.repository.data.holder.values.ValueContext;
048import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
049import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
050import org.ametys.plugins.repository.data.type.RepositoryElementType;
051import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType;
052import org.ametys.plugins.repository.data.type.RepositoryModelItemType;
053import org.ametys.plugins.repository.lock.LockableAmetysObject;
054import org.ametys.plugins.repository.model.CompositeDefinition;
055import org.ametys.plugins.repository.model.RepeaterDefinition;
056import org.ametys.runtime.model.ElementDefinition;
057import org.ametys.runtime.model.ModelItem;
058import org.ametys.runtime.model.ModelItemContainer;
059import org.ametys.runtime.model.ModelViewItem;
060import org.ametys.runtime.model.ModelViewItemGroup;
061import org.ametys.runtime.model.ViewElement;
062import org.ametys.runtime.model.ViewItem;
063import org.ametys.runtime.model.ViewItemAccessor;
064import org.ametys.runtime.model.exception.BadDataPathCardinalityException;
065import org.ametys.runtime.model.exception.BadItemTypeException;
066import org.ametys.runtime.model.exception.UndefinedItemPathException;
067import org.ametys.runtime.model.type.ElementType;
068
069/**
070 * Default implementation for modifiable data holder with model
071 */
072public class DefaultModifiableModelAwareDataHolder extends DefaultModelAwareDataHolder implements ModifiableIndexableDataHolder
073{
074    private static final String __TEMP_SUFFIX = "__temp";
075    
076    /** Repository data to use to store data in the repository */
077    protected ModifiableRepositoryData _modifiableRepositoryData;
078    
079    /** Ametys object that can be locked on data modification */
080    protected Optional<LockableAmetysObject> _lockableAmetysObject;
081    
082    /** Parent of the current {@link DataHolder} */
083    protected Optional<? extends ModifiableIndexableDataHolder> _modifiableParent;
084    
085    /** Root {@link DataHolder} */
086    protected ModifiableIndexableDataHolder _modifiableRoot;
087    
088    /**
089     * Creates a modifiable default model aware data holder
090     * @param repositoryData the repository data to use
091     * @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.
092     */
093    public DefaultModifiableModelAwareDataHolder(ModifiableRepositoryData repositoryData, ModelItemContainer itemContainer)
094    {
095        this(repositoryData, itemContainer, Optional.empty(), Optional.empty(), Optional.empty());
096    }
097    
098    /**
099     * Creates a modifiable default model aware data holder
100     * @param repositoryData the repository data to use
101     * @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.
102     * @param lockableAmetysObject the ametys object that can be locked on data modification
103     */
104    public DefaultModifiableModelAwareDataHolder(ModifiableRepositoryData repositoryData, ModelItemContainer itemContainer, Optional<LockableAmetysObject> lockableAmetysObject)
105    {
106        this(repositoryData, itemContainer, lockableAmetysObject, Optional.empty(), Optional.empty());
107    }
108    
109    /**
110     * Creates a modifiable default model aware data holder
111     * @param repositoryData the repository data to use
112     * @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.
113     * @param lockableAmetysObject the ametys object that can be locked on data modification
114     * @param parent the 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 DefaultModifiableModelAwareDataHolder(ModifiableRepositoryData repositoryData, ModelItemContainer itemContainer, Optional<LockableAmetysObject> lockableAmetysObject, Optional<? extends ModifiableIndexableDataHolder> parent, Optional<? extends ModifiableIndexableDataHolder> root)
118    {
119        this(repositoryData, List.of(itemContainer), lockableAmetysObject, parent, root);
120    }
121    
122    
123    /**
124     * Creates a modifiable default model aware data holder
125     * @param repositoryData the repository data to use
126     * @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.
127     * @param lockableAmetysObject the ametys object that can be locked on data modification
128     * @param parent the parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder}
129     * @param root the root {@link DataHolder}
130     */
131    public DefaultModifiableModelAwareDataHolder(ModifiableRepositoryData repositoryData, Collection<? extends ModelItemContainer> itemContainers, Optional<LockableAmetysObject> lockableAmetysObject, Optional<? extends ModifiableIndexableDataHolder> parent, Optional<? extends ModifiableIndexableDataHolder> root)
132    {
133        super(repositoryData, itemContainers, parent, root);
134        _modifiableRepositoryData = repositoryData;
135        _lockableAmetysObject = lockableAmetysObject;
136        
137        _modifiableParent = parent;
138        _modifiableRoot = root.map(ModifiableIndexableDataHolder.class::cast)
139                .or(() -> _modifiableParent.map(ModifiableIndexableDataHolder::getRootDataHolder)) // if no root is specified but a parent, the root is the parent's root
140                .orElse(this); // if no root or parent is specified, the root is the current DataHolder
141    }
142    
143    @Override
144    public ModifiableIndexableComposite getComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
145    {
146        return (ModifiableIndexableComposite) super.getComposite(compositePath);
147    }
148    
149    @Override
150    public ModifiableIndexableComposite getLocalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
151    {
152        return (ModifiableIndexableComposite) super.getLocalComposite(compositePath);
153    }
154    
155    @Override
156    public ModifiableIndexableComposite getExternalComposite(String compositePath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
157    {
158        return (ModifiableIndexableComposite) super.getExternalComposite(compositePath);
159    }
160
161    @Override
162    protected ModifiableIndexableComposite _getComposite(String name, CompositeDefinition compositeDefinition) throws BadItemTypeException
163    {
164        return _getComposite(name, compositeDefinition, false);
165    }
166
167    @Override
168    public ModifiableIndexableRepeater getRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
169    {
170        return (ModifiableIndexableRepeater) super.getRepeater(repeaterPath);
171    }
172    
173    @Override
174    public ModifiableIndexableRepeater getLocalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
175    {
176        return (ModifiableIndexableRepeater) super.getLocalRepeater(repeaterPath);
177    }
178    
179    @Override
180    public ModifiableIndexableRepeater getExternalRepeater(String repeaterPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
181    {
182        return (ModifiableIndexableRepeater) super.getExternalRepeater(repeaterPath);
183    }
184    
185    @Override
186    protected ModifiableIndexableRepeater _getRepeater(String name, RepeaterDefinition repeaterDefinition) throws BadItemTypeException
187    {
188        return _getRepeater(name, repeaterDefinition, false);
189    }
190
191    public ModifiableIndexableComposite getComposite(String compositePath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
192    {
193        return _getComposite(compositePath, createNew, Optional.empty());
194    }
195    
196    public ModifiableIndexableComposite getLocalComposite(String compositePath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
197    {
198        return _getComposite(compositePath, createNew, Optional.of(ExternalizableDataStatus.LOCAL));
199    }
200    
201    public ModifiableIndexableComposite getExternalComposite(String compositePath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
202    {
203        return _getComposite(compositePath, createNew, Optional.of(ExternalizableDataStatus.EXTERNAL));
204    }
205
206    private ModifiableIndexableComposite _getComposite(String compositePath, boolean createNew, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
207    {
208        String[] pathSegments = StringUtils.split(compositePath, ModelItem.ITEM_PATH_SEPARATOR);
209        
210        if (pathSegments == null || pathSegments.length < 1)
211        {
212            throw new IllegalArgumentException("Unable to retrieve the composite at the given path. This path is empty.");
213        }
214        else if (pathSegments.length == 1)
215        {
216            // Simple path => get composite value
217            ModelItem modelItem = getDefinition(compositePath);
218            if (modelItem instanceof CompositeDefinition)
219            {
220                String compositeName = _getFinalDataName(compositePath, status);
221                return _getComposite(compositeName, (CompositeDefinition) modelItem, createNew);
222            }
223            else
224            {
225                throw new BadItemTypeException("The data at path '" + compositePath + "' is not a composite.");
226            }
227        }
228        else
229        {
230            String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
231            
232            // Multiple items are allowed only at the last segment of the data path
233            if (isMultiple(parentPath))
234            {
235                throw new BadDataPathCardinalityException("Unable to retrieve the composite at path '" + compositePath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path.");
236            }
237            
238            Object parentValue = getValue(parentPath);
239            String childName = pathSegments[pathSegments.length - 1];
240            if (parentValue != null && parentValue instanceof ModifiableIndexableDataHolder parent)
241            {
242                return status.isPresent()
243                        ? ExternalizableDataStatus.EXTERNAL.equals(status.get())
244                                ? getExternalComposite(compositePath, createNew)
245                                : getLocalComposite(compositePath, createNew)
246                        : parent.getComposite(childName, createNew);
247            }
248            else
249            {
250                throw new BadItemTypeException("The data at path '" + parentPath + "' in the repository doesn't exist or is not a data holder. It can not contain the data named '" + childName + "'.");
251            }
252        }
253    }
254    
255    /**
256     * Retrieves the composite with the given name
257     * @param name name of the composite to retrieve
258     * @param compositeDefinition the definition of the composite to retrieve
259     * @param createNew <code>true</code> to create the repeater if it does not exist, <code>false</code> otherwise
260     * @return the composite
261     * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite
262     */
263    protected ModifiableIndexableComposite _getComposite(String name, CompositeDefinition compositeDefinition, boolean createNew) throws BadItemTypeException
264    {
265        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) compositeDefinition.getType();
266        RepositoryData compositeRepositoryData = type.read(_modifiableRepositoryData, name);
267        
268        if (compositeRepositoryData != null)
269        {
270            return new DefaultModifiableModelAwareComposite((ModifiableRepositoryData) compositeRepositoryData, compositeDefinition, _lockableAmetysObject, this, _modifiableRoot);
271        }
272        else
273        {
274            if (createNew)
275            {
276                _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
277                ModifiableRepositoryData createdRepositoryData = type.add(_modifiableRepositoryData, name);
278                return new DefaultModifiableModelAwareComposite(createdRepositoryData, compositeDefinition, _lockableAmetysObject, this, _modifiableRoot);
279            }
280            else
281            {
282                return null;
283            }
284        }
285    }
286
287    public ModifiableIndexableRepeater getRepeater(String repeaterPath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
288    {
289        return _getRepeater(repeaterPath, createNew, Optional.empty());
290    }
291    
292    public ModifiableIndexableRepeater getLocalRepeater(String repeaterPath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
293    {
294        return _getRepeater(repeaterPath, createNew, Optional.of(ExternalizableDataStatus.LOCAL));
295    }
296    
297    public ModifiableIndexableRepeater getExternalRepeater(String repeaterPath, boolean createNew) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
298    {
299        return _getRepeater(repeaterPath, createNew, Optional.of(ExternalizableDataStatus.EXTERNAL));
300    }
301
302    private ModifiableIndexableRepeater _getRepeater(String repeaterPath, boolean createNew, Optional<ExternalizableDataStatus> status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
303    {
304        String[] pathSegments = StringUtils.split(repeaterPath, ModelItem.ITEM_PATH_SEPARATOR);
305        
306        if (pathSegments == null || pathSegments.length < 1)
307        {
308            throw new IllegalArgumentException("Unable to retrieve the repeater at the given path. This path is empty.");
309        }
310        else if (pathSegments.length == 1)
311        {
312            // Simple path => get composite value
313            ModelItem modelItem = getDefinition(repeaterPath);
314            if (modelItem instanceof RepeaterDefinition)
315            {
316                String repeaterName = _getFinalDataName(repeaterPath, status);
317                return _getRepeater(repeaterName, (RepeaterDefinition) modelItem, createNew);
318            }
319            else
320            {
321                throw new BadItemTypeException("The data at path '" + repeaterPath + "' is not a repeater.");
322            }
323        }
324        else
325        {
326            String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
327            
328            // Multiple items are allowed only at the last segment of the data path
329            if (isMultiple(parentPath))
330            {
331                throw new BadDataPathCardinalityException("Unable to retrieve the repeater at path '" + repeaterPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path.");
332            }
333            
334            Object parentValue = getValue(parentPath);
335            String childName = pathSegments[pathSegments.length - 1];
336            if (parentValue != null && parentValue instanceof ModifiableIndexableDataHolder parent)
337            {
338                return status.isPresent()
339                        ? ExternalizableDataStatus.EXTERNAL.equals(status.get())
340                                ? parent.getExternalRepeater(repeaterPath, createNew)
341                                : parent.getExternalRepeater(repeaterPath, createNew)
342                        : parent.getRepeater(childName, createNew);
343            }
344            else
345            {
346                throw new BadItemTypeException("The data at path '" + parentPath + "' in the repository doesn't exist or is not a composite or a repeater entry. It can not contain the data named '" + childName + "'.");
347            }
348        }
349    }
350    
351    /**
352     * Retrieves the repeater with the given name
353     * @param name name of the repeater to retrieve
354     * @param repeaterDefinition the definition of the repeater to retrieve
355     * @param createNew <code>true</code> to create the repeater if it does not exist, <code>false</code> otherwise
356     * @return the repeater
357     * @throws BadItemTypeException if the value stored in the repository with the given name is not a repeater
358     */
359    protected ModifiableIndexableRepeater _getRepeater(String name, RepeaterDefinition repeaterDefinition, boolean createNew) throws BadItemTypeException
360    {
361        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) repeaterDefinition.getType();
362        RepositoryData repeaterRepositoryData = type.read(_modifiableRepositoryData, name);
363        
364        if (repeaterRepositoryData != null)
365        {
366            return new DefaultModifiableModelAwareRepeater((ModifiableRepositoryData) repeaterRepositoryData, repeaterDefinition, _lockableAmetysObject, this, _modifiableRoot);
367        }
368        else
369        {
370            if (createNew)
371            {
372                _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
373                ModifiableRepositoryData createdRepositoryData = type.add(_modifiableRepositoryData, name);
374                return new DefaultModifiableModelAwareRepeater(createdRepositoryData, repeaterDefinition, _lockableAmetysObject, this, _modifiableRoot);
375            }
376            else
377            {
378                return null;
379            }
380        }
381    }
382    
383    @Override
384    protected Class _getRepeaterEntryClass()
385    {
386        return ModifiableModelAwareRepeaterEntry.class;
387    }
388    
389    @Override
390    protected Class _getRepeaterClass()
391    {
392        return ModifiableModelAwareRepeater.class;
393    }
394    
395    @Override
396    protected Class _getCompositeClass()
397    {
398        return ModifiableModelAwareComposite.class;
399    }
400    
401    public <T extends SynchronizationResult> T synchronizeValues(Map<String, Object> values) throws UndefinedItemPathException, BadItemTypeException
402    {
403        return synchronizeValues(values, _createSynchronizationContextInstance());
404    }
405    
406    public <T extends SynchronizationResult> T synchronizeValues(Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException
407    {
408        ViewItemAccessor viewItemAccessor = DataHolderHelper.createViewItemAccessorFromValues(_itemContainers, values);
409        return synchronizeValues(viewItemAccessor, values, context);
410    }
411    
412    public <T extends SynchronizationResult> T synchronizeValues(ViewItemAccessor viewItemAccessor, Map<String, Object> values) throws UndefinedItemPathException, BadItemTypeException
413    {
414        return synchronizeValues(viewItemAccessor, values, _createSynchronizationContextInstance());
415    }
416    
417    public <T extends SynchronizationResult> T synchronizeValues(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException
418    {
419        return _synchronizeValues(viewItemAccessor, values, context);
420    }
421    
422    /**
423     * Synchronizes the given values with the current {@link ModifiableModelAwareDataHolder}'s ones
424     * @param <T> the type of the {@link SynchronizationResult}
425     * @param viewItemAccessor The {@link ViewItemAccessor} for all items to synchronize
426     * @param values the values to synchronize
427     * @param context the context of the synchronization
428     * @return the {@link SynchronizationResult}
429     * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model
430     * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value
431     */
432    protected <T extends SynchronizationResult> T _synchronizeValues(ViewItemAccessor viewItemAccessor, Map<String, Object> values, SynchronizationContext context) throws UndefinedItemPathException, BadItemTypeException
433    {
434        T result = _createSetValueResultInstance();
435        
436        for (ViewItem viewItem : viewItemAccessor.getViewItems())
437        {
438            SynchronizationResult itemResult = null;
439            if (viewItem instanceof ModelViewItem)
440            {
441                if (viewItem instanceof ModelViewItemGroup modelViewItemGroup)
442                {
443                    itemResult = _synchronizeGroup(modelViewItemGroup, values, context);
444                }
445                else if (viewItem instanceof ViewElement viewElement)
446                {
447                    itemResult = _synchronizeElement(viewElement, values, context);
448                }
449            }
450            else if (viewItem instanceof ViewItemAccessor accessor)
451            {
452                itemResult = _synchronizeValues(accessor, values, context);
453            }
454            
455            result.aggregateResult(itemResult);
456        }
457        
458        return result;
459    }
460
461    /**
462     * Synchronizes the given values with the given group's ones
463     * @param <T> the type of the {@link SynchronizationResult}
464     * @param modelViewItemGroup The group containing items to synchronize
465     * @param values the values to synchronize
466     * @param synchronizationContext the context of the synchronization
467     * @return the {@link SynchronizationResult}
468     * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model
469     * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value
470     */
471    @SuppressWarnings("unchecked")
472    protected <T extends SynchronizationResult> T _synchronizeGroup(ModelViewItemGroup modelViewItemGroup, Map<String, Object> values, SynchronizationContext synchronizationContext) throws UndefinedItemPathException, BadItemTypeException
473    {
474        ModelItem modelItem = modelViewItemGroup.getDefinition();
475        String dataName = modelItem.getName();
476        Object value = values.get(dataName);
477        T result = null;
478        
479        if (value == null)
480        {
481            ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext);
482            result = _createSetValueResultInstance();
483            if (DataHolderHelper.hasValueOrEmpty(this, dataName, valueContext))
484            {
485                _removeValueForSynchronize(dataName, valueContext);
486                result.setHasChanged(true);
487            }
488        }
489        else if (modelItem instanceof RepeaterDefinition)
490        {
491            if (value instanceof SynchronizableRepeater || value instanceof List)
492            {
493                ModifiableModelAwareRepeater repeater = getRepeater(dataName, true);
494                SynchronizableRepeater repeaterValues = value instanceof SynchronizableRepeater ? (SynchronizableRepeater) value : SynchronizableRepeater.replaceAll((List<Map<String, Object>>) value, null);
495                result = repeater.synchronizeValues(modelViewItemGroup, repeaterValues, synchronizationContext);
496            }
497            else if (value instanceof UntouchedValue)
498            {
499                result = _createSetValueResultInstance();
500            }
501            else
502            {
503                throw new BadItemTypeException("Unable to synchronize the repeater named '" + dataName + "': the given value should be a list containing its entries");
504            }
505        }
506        else
507        {
508            if (value instanceof Map)
509            {
510                ModifiableModelAwareComposite composite = getComposite(dataName, true);
511                result = composite.synchronizeValues(modelViewItemGroup, (Map<String, Object>) value, synchronizationContext);
512            }
513            else
514            {
515                throw new BadItemTypeException("Unable to synchronize the composite named '" + dataName + "': the given value should be a map containing values of all of its items");
516            }
517        }
518        
519        if (!DataHolderHelper.getExternalizableDataProviderExtensionPoint().isDataExternalizable(getRootDataHolder(), modelItem))
520        {
521            _removeExternalizableMetadataIfExists(_modifiableRepositoryData, dataName);
522        }
523        
524        return result;
525    }
526    
527    /**
528     * Synchronizes the given values with the given element's ones
529     * @param <T> the type of the {@link SynchronizationResult}
530     * @param viewElement The element item to synchronize
531     * @param values the values to synchronize
532     * @param synchronizationContext the context of the synchronization
533     * @return the {@link SynchronizationResult}
534     * @throws UndefinedItemPathException if a key in the given Map refers to a data that is not defined by the model
535     * @throws BadItemTypeException if the type defined by the model of one of the Map's key doesn't match the corresponding value
536     */
537    protected <T extends SynchronizationResult> T _synchronizeElement(ViewElement viewElement, Map<String, Object> values, SynchronizationContext synchronizationContext) throws UndefinedItemPathException, BadItemTypeException
538    {
539        ElementDefinition definition = viewElement.getDefinition();
540        String dataName = definition.getName();
541        
542        Object valueFromMap = values.get(dataName);
543        ValueContext valueContext = DataHolderHelper.createValueContextFromSynchronizationContext(this, dataName, synchronizationContext);
544        
545        SynchronizableValue syncValue = valueFromMap instanceof SynchronizableValue ? (SynchronizableValue) valueFromMap : new SynchronizableValue(valueFromMap, valueContext.getStatus().orElse(null));
546        Object value = syncValue.getValue(valueContext.getStatus());
547        
548        T result = null;
549        
550        if (!(value instanceof UntouchedValue))
551        {
552            Object defaultValue = definition.getDefaultValue();
553            if (value == null && synchronizationContext.useDefaultFromModel() && defaultValue != null)
554            {
555                result = _setValueForSynchronize(dataName, new SynchronizableValue(defaultValue, valueContext.getStatus().orElse(null)), valueContext);
556            }
557            else
558            {
559                if (values.containsKey(dataName))
560                {
561                    result = _setValueForSynchronize(dataName, syncValue, valueContext);
562                }
563                else
564                {
565                    result = _removeValueForSynchronize(dataName, valueContext);
566                }
567            }
568        }
569        else
570        {
571            result = _createSetValueResultInstance();
572        }
573        
574        if (DataHolderHelper.getExternalizableDataProviderExtensionPoint().isDataExternalizable(getRootDataHolder(), definition))
575        {
576            boolean statusHasChanged = _updateStatusForSynchronize(dataName, syncValue, valueContext, synchronizationContext.forceStatusIfNotPresent(), values.containsKey(dataName));
577            if (statusHasChanged)
578            {
579                result.setHasChanged(true);
580            }
581        }
582        else
583        {
584            _removeExternalizableMetadataIfExists(_modifiableRepositoryData, dataName);
585        }
586        
587        List<DataComment> newComments = syncValue.getComments();
588        List<DataComment> oldComments = hasComments(dataName) ? getComments(dataName) : List.of();
589        if (newComments != null && !newComments.equals(oldComments))
590        {
591            result.setHasChanged(true);
592            setComments(dataName, newComments);
593        }
594        
595        return result;
596    }
597
598    /**
599     * Updates the status of the data with the given name
600     * @param dataName name of the data
601     * @param value the value
602     * @param context context of the data
603     * @param forceStatus <code>true</code> to force the status if it is not present, <code>false</code> otherwise
604     * @param doValuesContainData <code>true</code> if the values contain the data, <code>false</code> otherwise
605     * @return <code>true</code> if the status has changed, <code>false</code> otherwise
606     * @throws IllegalArgumentException if the given data name is null or empty
607     * @throws UndefinedItemPathException if the given data name is not defined by the model
608     * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 
609     */
610    protected boolean _updateStatusForSynchronize(String dataName, SynchronizableValue value, ValueContext context, boolean forceStatus, boolean doValuesContainData) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException
611    {
612        boolean hasChanged = false;
613        
614        ExternalizableDataStatus oldStatus = null;
615        if (_repositoryData.hasValue(dataName + STATUS_SUFFIX))
616        {
617            String status = _repositoryData.getString(dataName + STATUS_SUFFIX);
618            oldStatus = ExternalizableDataStatus.valueOf(status.toUpperCase());
619        }
620        ExternalizableDataStatus newStatus = value.getExternalizableStatus();
621        Optional<ExternalizableDataStatus> contextStatus = context.getStatus();
622        if (forceStatus && oldStatus == null && newStatus == null && doValuesContainData && contextStatus.isPresent())
623        {
624            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
625            setStatus(dataName, contextStatus.get());
626            hasChanged = true;
627        }
628        else if (newStatus != null && !newStatus.equals(oldStatus))
629        {
630            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
631            setStatus(dataName, newStatus);
632            hasChanged = true;
633        }
634        
635        return hasChanged;
636    }
637    
638    /**
639     * Sets the value of the data with the given name
640     * @param <T> the type of the {@link SynchronizationResult}
641     * @param dataName name of the data
642     * @param value the value to set. Give <code>null</code> to empty the value.
643     * @param context context of the data to set
644     * @return the {@link SynchronizationResult}
645     * @throws IllegalArgumentException if the given data name is null or empty
646     * @throws UndefinedItemPathException if the given data name is not defined by the model
647     * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 
648     */
649    protected <T extends SynchronizationResult> T _setValueForSynchronize(String dataName, SynchronizableValue value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException
650    {
651        T result = _createSetValueResultInstance();
652        
653        if (SynchronizableValue.Mode.REMOVE.equals(value.getMode()))
654        {
655            if (DataHolderHelper.hasValueOrEmpty(this, dataName, context))
656            {
657                if (isMultiple(dataName))
658                {
659                    Object oldValues = DataHolderHelper.getValue(this, dataName, context);
660                    Object valuesToRemove = DataHolderHelper.getArrayValuesFromSynchronizableValue(value, context);
661                    ElementType type = ((ElementDefinition) getDefinition(dataName)).getType();
662
663                    // Remove the given values from the existent ones
664                    Object newValues = DataHolderHelper.removeValuesInArray(oldValues, valuesToRemove, type);
665                    
666                    if (Array.getLength(oldValues) > Array.getLength(newValues))
667                    {
668                        result.setHasChanged(true);
669                        _setValue(dataName, newValues, context);
670                    }
671                }
672                else
673                {
674                    result.setHasChanged(true);
675                    _removeValue(dataName, context);
676                }
677            }
678        }
679        else if (SynchronizableValue.Mode.APPEND.equals(value.getMode()) && isMultiple(dataName))
680        {
681            Object valuesToAppend = DataHolderHelper.getArrayValuesFromSynchronizableValue(value, context);
682            if (Array.getLength(valuesToAppend) > 0)
683            {
684                Object oldValues = DataHolderHelper.getValue(this, dataName, context);
685                ElementType type = ((ElementDefinition) getDefinition(dataName)).getType();
686                
687                // Append the given values to the existent ones
688                Object newValues = oldValues != null
689                                ? DataHolderHelper.appendValuesInArray(oldValues, valuesToAppend, type)
690                                : valuesToAppend;
691                
692                result.setHasChanged(true);
693                _setValue(dataName, newValues, context);
694            }
695        }
696        else
697        {
698            boolean hasValueOrEmpty = DataHolderHelper.hasValueOrEmpty(this, dataName, context);
699            boolean hasEmptyValue = hasValueOrEmpty && !DataHolderHelper.hasValue(this, dataName, context);
700            Object newValue = value.getValue(context.getStatus());
701            
702            if (newValue != null || !hasEmptyValue)
703            {
704                if (hasValueOrEmpty)
705                {
706                    Object oldValue = DataHolderHelper.getValue(this, dataName, context);
707                    ElementType type = ((ElementDefinition) getDefinition(dataName)).getType();
708                    if (type.compareValues(value.getValue(context.getStatus()), oldValue).count() > 0)
709                    {
710                        // There are differences between old and new value
711                        result.setHasChanged(true);
712                        _setValue(dataName, newValue, context);
713                    }
714                }
715                else
716                {
717                    // There was no values, set one
718                    result.setHasChanged(true);
719                    _setValue(dataName, newValue, context);
720                }
721            }
722        }
723
724        return result;
725    }
726    
727    /**
728     * Removes the stored value of the data with the given name
729     * @param <T> the type of the {@link SynchronizationResult}
730     * @param dataName name of the data
731     * @param context context of the data to remove
732     * @return the {@link SynchronizationResult}
733     * @throws IllegalArgumentException if the given data name is null or empty
734     * @throws BadItemTypeException if the value of the parent with the given name is not an item container
735     * @throws UndefinedItemPathException if the given data name is not defined by the model
736     */
737    protected <T extends SynchronizationResult> T _removeValueForSynchronize(String dataName, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException
738    {
739        T result = _createSetValueResultInstance();
740        
741        if (DataHolderHelper.hasValueOrEmpty(this, dataName, context))
742        {
743            _removeValue(dataName, context);
744            result.setHasChanged(true);
745        }
746        
747        return result;
748    }
749    
750    public void setValue(String dataPath, Object value) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
751    {
752        _setValue(dataPath, value, ValueContext.newInstance());
753    }
754    
755    public void setLocalValue(String dataPath, Object localValue) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
756    {
757        _setValue(dataPath, localValue, ValueContext.newInstance().withStatus(ExternalizableDataStatus.LOCAL));
758    }
759    
760    public void setExternalValue(String dataPath, Object externalValue) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
761    {
762        _setValue(dataPath, externalValue, ValueContext.newInstance().withStatus(ExternalizableDataStatus.EXTERNAL));
763    }
764    
765    /**
766     * Sets the value of the data at the given path
767     * @param dataPath path of the data
768     * @param value the value to set. Give <code>null</code> to empty the value.
769     * @param context context of the data to set
770     * @throws IllegalArgumentException if the given data path is null or empty
771     * @throws UndefinedItemPathException if the given data path is not defined by the model
772     * @throws BadItemTypeException if the type defined by the model doesn't match the given value to set 
773     * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple
774     */
775    protected void _setValue(String dataPath, Object value, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
776    {
777        _checkModifiableDefinition(dataPath, "Unable to set the value '" + value + "' at path '" + dataPath + "'.");
778        
779        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
780        
781        if (pathSegments == null || pathSegments.length < 1)
782        {
783            throw new IllegalArgumentException("Unable to set the value '" + value + "' at the given path. This path is empty.");
784        }
785        else if (pathSegments.length == 1)
786        {
787            ModelItem modelItem = getDefinition(dataPath);
788            
789            // Simple path => set the value
790            if (modelItem instanceof ElementDefinition)
791            {
792                String dataName = _getFinalDataName(dataPath, context.getStatus());
793                _setElementValue((ElementDefinition) modelItem, dataName, value);
794            }
795            else
796            {
797                throw new BadItemTypeException("Unable to set the value '" + value + "' on the data at path '" + dataPath + "' in the repository because it is a group item.");
798            }
799        }
800        else
801        {
802            String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
803            
804            // Multiple items are allowed only at the last segment of the data path
805            if (isMultiple(parentPath))
806            {
807                throw new BadDataPathCardinalityException("Unable to set the value '" + value + "' on the data at path '" + dataPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path.");
808            }
809            
810            Object parentValue = getValue(parentPath);
811            String childName = pathSegments[pathSegments.length - 1];
812            if (parentValue != null && parentValue instanceof ModifiableModelAwareDataHolder)
813            {
814                ModifiableModelAwareDataHolder parent = (ModifiableModelAwareDataHolder) parentValue;
815                if (context.getStatus().isPresent())
816                {
817                    if (ExternalizableDataStatus.EXTERNAL.equals(context.getStatus().get()))
818                    {
819                        parent.setExternalValue(childName, value);
820                    }
821                    else
822                    {
823                        parent.setLocalValue(childName, value);
824                    }
825                }
826                else
827                {
828                    parent.setValue(childName, value);
829                }
830            }
831            else
832            {
833                throw new BadItemTypeException("The data at path '" + parentPath + "' in the repository doesn't exist or is not a composite or a repeater entry. It can not contain the data named '" + childName + "'.");
834            }
835        }
836    }
837
838    private void _setElementValue(ElementDefinition definition, String dataName, Object value)
839    {
840        _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
841        RepositoryElementType type = (RepositoryElementType) definition.getType();
842        
843        if (definition.isMultiple())
844        {
845            if (value == null)
846            {
847                type.write(_modifiableRepositoryData, dataName, Array.newInstance(type.getManagedClass(), 0));
848            }
849            else if (!value.getClass().isArray())
850            {
851                // The value is single but should be an array. Create the array with the single value
852                Object arrayValue = Array.newInstance(value.getClass(), 1);
853                Array.set(arrayValue, 0, value);
854                type.write(_modifiableRepositoryData, dataName, arrayValue);
855            }
856            else
857            {
858                type.write(_modifiableRepositoryData, dataName, value);
859            }
860        }
861        else
862        {
863            if (type.getManagedClassArray().isInstance(value))
864            {
865                // The value is multiple but should be single.
866                if (Array.getLength(value) > 1)
867                {
868                    throw new IllegalArgumentException("Unable to set the mutilple value '" + value + "' at path '" + dataName + "'. This item is not multiple.");
869                }
870                else
871                {
872                    Object singleValue = Array.getLength(value) == 1 ? Array.get(value, 0) : null;
873                    type.write(_modifiableRepositoryData, dataName, singleValue);
874                }
875            }
876            else
877            {
878                type.write(_modifiableRepositoryData, dataName, value);
879            }
880        }
881    }
882    
883    public void setStatus(String dataPath, ExternalizableDataStatus status) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
884    {
885        _checkModifiableDefinition(dataPath, "Unable to set the status at path '" + dataPath + "'.");
886        
887        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
888
889        if (pathSegments == null || pathSegments.length < 1)
890        {
891            throw new IllegalArgumentException("Unable to set the status at the given path. This path is empty.");
892        }
893        else if (pathSegments.length == 1)
894        {
895            ExternalizableDataStatus oldStatus = getStatus(dataPath);
896            boolean hasStatus = _repositoryData.hasValue(dataPath + STATUS_SUFFIX);
897            
898            if (!hasStatus || oldStatus != status)
899            {
900                _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
901                
902                // Set the status if it has not been set yet or if it is different from the new one
903                _modifiableRepositoryData.setValue(dataPath + STATUS_SUFFIX, status.name().toLowerCase());
904            }
905            
906            if (oldStatus != status)
907            {
908                // Switch value and alternative value if the old status is different from the new one
909                ModelItem modelItem = getDefinition(dataPath);
910                if (modelItem instanceof CompositeDefinition)
911                {
912                    _setStatus(dataPath, (CompositeDefinition) modelItem);
913                }
914                else if (modelItem instanceof RepeaterDefinition)
915                {
916                    _setStatus(dataPath, (RepeaterDefinition) modelItem);
917                }
918                else
919                {
920                    _setStatus(dataPath, (ElementDefinition) modelItem);
921                }
922            }
923        }
924        else
925        {
926            String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
927
928            // Multiple items are allowed only at the last segment of the data path
929            if (isMultiple(parentPath))
930            {
931                throw new BadDataPathCardinalityException("Unable to set the status on the data at path '" + dataPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path.");
932            }
933
934            Object parentValue = getValue(parentPath);
935            String childName = pathSegments[pathSegments.length - 1];
936            if (parentValue != null && parentValue instanceof ModifiableModelAwareDataHolder)
937            {
938                ModifiableModelAwareDataHolder parent = (ModifiableModelAwareDataHolder) parentValue;
939                parent.setStatus(childName, status);
940            }
941            else
942            {
943                throw new BadItemTypeException("The data at path '" + parentPath + "' in the repository doesn't exist or is not a composite or a repeater entry. It can not contain the data named '" + childName + "'.");
944            }
945        }
946    }
947
948    private void _setStatus(String dataPath, CompositeDefinition compositeDefinition)
949    {
950        if (_repositoryData.hasValue(dataPath))
951        {
952            ModifiableModelAwareComposite composite = _getComposite(dataPath, compositeDefinition);
953            ModifiableModelAwareComposite tempComposite = _getComposite(dataPath + __TEMP_SUFFIX, compositeDefinition, true);
954            composite.copyTo(tempComposite);
955
956            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
957            _modifiableRepositoryData.removeValue(dataPath);
958        }
959
960        if (_repositoryData.hasValue(dataPath + ALTERNATIVE_SUFFIX))
961        {
962            ModifiableModelAwareComposite altComposite = _getComposite(dataPath + ALTERNATIVE_SUFFIX, compositeDefinition);
963            ModifiableModelAwareComposite composite = _getComposite(dataPath, compositeDefinition, true);
964            altComposite.copyTo(composite);
965
966            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
967            _modifiableRepositoryData.removeValue(dataPath + ALTERNATIVE_SUFFIX);
968        }
969
970        if (_repositoryData.hasValue(dataPath + __TEMP_SUFFIX))
971        {
972            ModifiableModelAwareComposite tempComposite = _getComposite(dataPath + __TEMP_SUFFIX, compositeDefinition);
973            ModifiableModelAwareComposite altComposite = _getComposite(dataPath + ALTERNATIVE_SUFFIX, compositeDefinition, true);
974            tempComposite.copyTo(altComposite);
975
976            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
977            _modifiableRepositoryData.removeValue(dataPath + __TEMP_SUFFIX);
978        }
979    }
980
981    private void _setStatus(String dataPath, RepeaterDefinition repeaterDefinition)
982    {
983        if (_repositoryData.hasValue(dataPath))
984        {
985            ModifiableModelAwareRepeater repeater = _getRepeater(dataPath, repeaterDefinition);
986            ModifiableModelAwareRepeater tempRepeater = _getRepeater(dataPath + __TEMP_SUFFIX, repeaterDefinition, true);
987            repeater.copyTo(tempRepeater);
988
989            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
990            _modifiableRepositoryData.removeValue(dataPath);
991        }
992
993        if (_repositoryData.hasValue(dataPath + ALTERNATIVE_SUFFIX))
994        {
995            ModifiableModelAwareRepeater altRepeater = _getRepeater(dataPath + ALTERNATIVE_SUFFIX, repeaterDefinition);
996            ModifiableModelAwareRepeater repeater = _getRepeater(dataPath, repeaterDefinition, true);
997            altRepeater.copyTo(repeater);
998
999            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1000            _modifiableRepositoryData.removeValue(dataPath + ALTERNATIVE_SUFFIX);
1001        }
1002
1003        if (_repositoryData.hasValue(dataPath + __TEMP_SUFFIX))
1004        {
1005            ModifiableModelAwareRepeater tempRepeater = _getRepeater(dataPath + __TEMP_SUFFIX, repeaterDefinition);
1006            ModifiableModelAwareRepeater altRepeater = _getRepeater(dataPath + ALTERNATIVE_SUFFIX, repeaterDefinition, true);
1007            tempRepeater.copyTo(altRepeater);
1008
1009            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1010            _modifiableRepositoryData.removeValue(dataPath + __TEMP_SUFFIX);
1011        }
1012    }
1013
1014    private void _setStatus(String dataName, ElementDefinition elementDefinition)
1015    {
1016        RepositoryElementType type = (RepositoryElementType) elementDefinition.getType();
1017        if (type.hasValue(_repositoryData, dataName))
1018        {
1019            Object value = type.read(_repositoryData, dataName);
1020
1021            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1022            type.write(_modifiableRepositoryData, dataName + __TEMP_SUFFIX, value);
1023            type.remove(_modifiableRepositoryData, dataName);
1024        }
1025        
1026        if (type.hasValue(_repositoryData, dataName + ALTERNATIVE_SUFFIX))
1027        {
1028            Object altValue = type.read(_repositoryData, dataName + ALTERNATIVE_SUFFIX);
1029            
1030            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1031            type.write(_modifiableRepositoryData, dataName, altValue);
1032            type.remove(_modifiableRepositoryData, dataName + ALTERNATIVE_SUFFIX);
1033        }
1034
1035        if (type.hasValue(_repositoryData, dataName + __TEMP_SUFFIX))
1036        {
1037            Object tempValue = type.read(_repositoryData, dataName + __TEMP_SUFFIX);
1038            
1039            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1040            type.write(_modifiableRepositoryData, dataName + ALTERNATIVE_SUFFIX, tempValue);
1041            type.remove(_modifiableRepositoryData, dataName + __TEMP_SUFFIX);
1042        }
1043    }
1044    
1045    public void setComments(String dataName, List<DataComment> comments) throws IllegalArgumentException, UndefinedItemPathException
1046    {
1047        _checkModifiableDefinition(dataName, "Unable to retrieve the comments of the data named '" + dataName + "'.");
1048        
1049        // Remove old comments if needed
1050        if (hasComments(dataName))
1051        {
1052            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1053            _modifiableRepositoryData.removeValue(dataName + COMMENTS_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
1054        }
1055        
1056        if (!comments.isEmpty())
1057        {
1058            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1059            ModifiableRepositoryData commentsRepositoryData = _modifiableRepositoryData.addRepositoryData(dataName + COMMENTS_SUFFIX, RepositoryConstants.COMPOSITE_METADTA_NODETYPE, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
1060            
1061            int commentId = 1;
1062            for (DataComment comment : comments)
1063            {
1064                ModifiableRepositoryData commentRepositoryData = commentsRepositoryData.addRepositoryData(String.valueOf(commentId), RepositoryConstants.COMPOSITE_METADTA_NODETYPE, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
1065                commentId++;
1066                
1067                commentRepositoryData.setValue("comment", comment.getComment());
1068                commentRepositoryData.setValue("author", comment.getAuthor());
1069                commentRepositoryData.setValue("date", DateUtils.asCalendar(comment.getDate()));
1070            }
1071        }
1072    }
1073    
1074    public void removeValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
1075    {
1076        _removeValue(dataPath, ValueContext.newInstance());
1077    }
1078    
1079    public void removeLocalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
1080    {
1081        _removeValue(dataPath, ValueContext.newInstance().withStatus(ExternalizableDataStatus.LOCAL));
1082    }
1083    
1084    public void removeExternalValue(String dataPath) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
1085    {
1086        _removeValue(dataPath, ValueContext.newInstance().withStatus(ExternalizableDataStatus.EXTERNAL));
1087    }
1088    
1089    /**
1090     * Removes the stored value of the data at the given path
1091     * @param dataPath path of the data
1092     * @param context context of the data to remove
1093     * @throws IllegalArgumentException if the given data path is null or empty
1094     * @throws BadItemTypeException if the value of the parent of the given path is not an item container
1095     * @throws UndefinedItemPathException if the given data path is not defined by the model
1096     * @throws BadDataPathCardinalityException if the definition of a part of the data path is multiple. Only the last part can be multiple
1097     */
1098    protected void _removeValue(String dataPath, ValueContext context) throws IllegalArgumentException, UndefinedItemPathException, BadItemTypeException, BadDataPathCardinalityException
1099    {
1100        _checkModifiableDefinition(dataPath, "Unable to retrieve the value at path '" + dataPath + "'.");
1101
1102        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
1103        
1104        if (pathSegments == null || pathSegments.length < 1)
1105        {
1106            throw new IllegalArgumentException("Unable to remove the value at the given path. This path is empty.");
1107        }
1108        else if (pathSegments.length == 1)
1109        {
1110            _doRemoveValue(dataPath, context);
1111        }
1112        else
1113        {
1114            String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
1115            
1116            // Multiple items are allowed only at the last segment of the data path
1117            if (isMultiple(parentPath))
1118            {
1119                throw new BadDataPathCardinalityException("Unable to remove the value at path '" + dataPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path.");
1120            }
1121            
1122            Object parentValue = getValue(parentPath);
1123            String childName = pathSegments[pathSegments.length - 1];
1124            if (parentValue != null && parentValue instanceof ModifiableModelAwareDataHolder)
1125            {
1126                ModifiableModelAwareDataHolder parent = (ModifiableModelAwareDataHolder) parentValue;
1127                if (context.getStatus().isPresent())
1128                {
1129                    if (ExternalizableDataStatus.EXTERNAL.equals(context.getStatus().get()))
1130                    {
1131                        parent.removeExternalValue(childName);
1132                    }
1133                    else
1134                    {
1135                        parent.removeLocalValue(childName);
1136                    }
1137                }
1138                else
1139                {
1140                    parent.removeValue(childName);
1141                }
1142            }
1143        }
1144    }
1145
1146    private void _doRemoveValue(String dataName, ValueContext context)
1147    {
1148        ModelItem modelItem = getDefinition(dataName);
1149        String finalDataName = _getFinalDataName(dataName, context.getStatus());
1150        RepositoryModelItemType type = getType(dataName);
1151        if (modelItem instanceof RepeaterDefinition && DataHolderHelper.isRepeaterEntryPath(finalDataName))
1152        {
1153            Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(finalDataName);
1154            String repeaterName = repeaterNameAndEntryPosition.getLeft();
1155            int entryPosition = repeaterNameAndEntryPosition.getRight();
1156            ModifiableModelAwareRepeater repeater = _getRepeater(repeaterName, (RepeaterDefinition) modelItem);
1157            repeater.removeEntry(entryPosition);
1158        }
1159        else
1160        {
1161            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1162            type.remove(_modifiableRepositoryData, finalDataName);
1163        }
1164    }
1165    
1166    public void removeExternalizableMetadataIfExists(String dataPath) throws IllegalArgumentException, BadItemTypeException, UndefinedItemPathException, BadDataPathCardinalityException
1167    {
1168        _checkModifiableDefinition(dataPath, "Unable to retrieve the value at path '" + dataPath + "'.");
1169
1170        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
1171        
1172        if (pathSegments == null || pathSegments.length < 1)
1173        {
1174            throw new IllegalArgumentException("Unable to remove the value at the given path. This path is empty.");
1175        }
1176        else if (pathSegments.length == 1)
1177        {
1178            _removeExternalizableMetadataIfExists(_modifiableRepositoryData, dataPath);
1179        }
1180        else
1181        {
1182            String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
1183            
1184            // Multiple items are allowed only at the last segment of the data path
1185            if (isMultiple(parentPath))
1186            {
1187                throw new BadDataPathCardinalityException("Unable to remove the value at path '" + dataPath + "'. The segment '" + pathSegments[pathSegments.length - 2] + "' refers to a multiple data and can not be used inside the data path.");
1188            }
1189            
1190            Object parentValue = getValue(parentPath);
1191            String childName = pathSegments[pathSegments.length - 1];
1192            if (parentValue != null && parentValue instanceof ModifiableModelAwareDataHolder)
1193            {
1194                ModifiableModelAwareDataHolder parent = (ModifiableModelAwareDataHolder) parentValue;
1195                parent.removeExternalizableMetadataIfExists(childName);
1196            }
1197        }
1198    }
1199    
1200    private void _removeExternalizableMetadataIfExists(ModifiableRepositoryData repositoryData, String dataName)
1201    {
1202        RepositoryModelItemType type = getType(dataName);
1203        if (type.hasValue(repositoryData, dataName + ALTERNATIVE_SUFFIX))
1204        {
1205            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1206            type.remove(repositoryData, dataName + ALTERNATIVE_SUFFIX);
1207        }
1208        
1209        if (repositoryData.hasValue(dataName + STATUS_SUFFIX))
1210        {
1211            _lockableAmetysObject.ifPresent(lao -> lao.setLockInfoOnCurrentContext());
1212            repositoryData.removeValue(dataName + STATUS_SUFFIX);
1213        }
1214    }
1215    
1216    /**
1217     * Creates an instance of {@link SynchronizationResult}
1218     * @param <T> the type of the {@link SynchronizationResult}
1219     * @return the created instance of {@link SynchronizationResult}
1220     */
1221    @SuppressWarnings("unchecked")
1222    protected <T extends SynchronizationResult> T _createSetValueResultInstance()
1223    {
1224        return (T) new SynchronizationResult();
1225    }
1226    
1227    @Override
1228    public ModifiableRepositoryData getRepositoryData()
1229    {
1230        return _modifiableRepositoryData;
1231    }
1232    
1233    @Override
1234    public Optional<? extends ModifiableIndexableDataHolder> getParentDataHolder()
1235    {
1236        return _modifiableParent;
1237    }
1238    
1239    @Override
1240    public ModifiableIndexableDataHolder getRootDataHolder()
1241    {
1242        return _modifiableRoot;
1243    }
1244    
1245    /**
1246     * Check definition for data path, for modification methods
1247     * @param dataPath the data path
1248     * @param errorMsg the error message to throw
1249     */
1250    protected void _checkModifiableDefinition(String dataPath, String errorMsg)
1251    {
1252        super._checkDefinition(dataPath, errorMsg);
1253        
1254        ModelItem modelItem = getDefinition(dataPath);
1255        if (modelItem instanceof ElementDefinition definition && !definition.isEditable())
1256        {
1257            throw new UndefinedItemPathException(errorMsg + " The model item '" + modelItem.getPath() + "' can't be modified.");
1258        }
1259    }
1260}