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