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