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