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.util.List;
019import java.util.Map;
020import java.util.Optional;
021import java.util.stream.Collectors;
022
023import org.apache.commons.lang3.StringUtils;
024
025import org.ametys.plugins.repository.RepositoryConstants;
026import org.ametys.plugins.repository.data.UnknownDataException;
027import org.ametys.plugins.repository.data.holder.DataHolder;
028import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
029import org.ametys.plugins.repository.data.holder.group.ModelLessComposite;
030import org.ametys.plugins.repository.data.holder.group.ModifiableModelLessComposite;
031import org.ametys.plugins.repository.data.holder.group.impl.DefaultModifiableModelLessComposite;
032import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
033import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
034import org.ametys.plugins.repository.data.type.ModelItemTypeConstants;
035import org.ametys.plugins.repository.data.type.ModelItemTypeExtensionPoint;
036import org.ametys.plugins.repository.data.type.RepositoryElementType;
037import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType;
038import org.ametys.plugins.repository.data.type.RepositoryModelItemType;
039import org.ametys.runtime.model.ModelItem;
040import org.ametys.runtime.model.exception.BadDataPathCardinalityException;
041import org.ametys.runtime.model.exception.BadItemTypeException;
042import org.ametys.runtime.model.exception.NotUniqueTypeException;
043import org.ametys.runtime.model.exception.UnknownTypeException;
044import org.ametys.runtime.model.type.ElementType;
045
046/**
047 * Default implementation for modifiable data holder without model
048 */
049public class DefaultModifiableModelLessDataHolder extends DefaultModelLessDataHolder implements ModifiableModelLessDataHolder
050{
051    /** Repository data to use to store data in the repository */
052    protected ModifiableRepositoryData _modifiableRepositoryData;
053    
054    /** Parent of the current {@link DataHolder} */
055    protected Optional<? extends ModifiableModelLessDataHolder> _modifiableParent;
056    
057    /** Root {@link DataHolder} */
058    protected ModifiableModelLessDataHolder _modifiableRoot;
059    
060    /**
061     * Creates a modifiable default model free data holder
062     * @param typeExtensionPoint the extension point to use to get available element types
063     * @param repositoryData the repository data to use
064     */
065    public DefaultModifiableModelLessDataHolder(ModelItemTypeExtensionPoint typeExtensionPoint, ModifiableRepositoryData repositoryData)
066    {
067        this(typeExtensionPoint, repositoryData, Optional.empty(), Optional.empty());
068    }
069    
070    /**
071     * Creates a modifiable default model free data holder
072     * @param typeExtensionPoint the extension point to use to get available element types
073     * @param parent the parent of the created {@link DataHolder}, can be <code>null</code> if the created {@link DataHolder} is the root {@link DataHolder}
074     * @param root the root {@link DataHolder}
075     * @param repositoryData the repository data to use
076     */
077    public DefaultModifiableModelLessDataHolder(ModelItemTypeExtensionPoint typeExtensionPoint, ModifiableRepositoryData repositoryData, Optional<? extends ModifiableModelLessDataHolder> parent, Optional<? extends ModifiableModelLessDataHolder> root)
078    {
079        super(typeExtensionPoint, repositoryData, parent, root);
080        _modifiableRepositoryData = repositoryData;
081        _modifiableParent = parent;
082        _modifiableRoot = root.map(ModifiableModelLessDataHolder.class::cast)
083                .or(() -> _modifiableParent.map(ModifiableModelLessDataHolder::getRootDataHolder)) // if no root is specified but a parent, the root is the parent's root
084                .orElse(this); // if no root or parent is specified, the root is the current DataHolder
085    }
086    
087    @Override
088    public ModifiableModelLessComposite getComposite(String compositePath) throws IllegalArgumentException, BadItemTypeException
089    {
090        return (ModifiableModelLessComposite) super.getComposite(compositePath);
091    }
092    
093    @Override
094    protected ModelLessComposite _getComposite(String name) throws BadItemTypeException
095    {
096        return _getComposite(name, false);
097    }
098
099    public ModifiableModelLessComposite getComposite(String compositePath, boolean createNew) throws IllegalArgumentException, BadItemTypeException
100    {
101        if (!_typeExtensionPoint.hasExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID))
102        {
103            throw new UnknownTypeException("The composites are not available for the extension point '" + _typeExtensionPoint + "'");
104        }
105        
106        String[] pathSegments = StringUtils.split(compositePath, ModelItem.ITEM_PATH_SEPARATOR);
107        
108        if (pathSegments == null || pathSegments.length < 1)
109        {
110            throw new IllegalArgumentException("Unable to retrieve the composite at the given path. This path is empty.");
111        }
112        else if (pathSegments.length == 1)
113        {
114            // Simple path => get the composite
115            return _getComposite(compositePath, createNew);
116        }
117        else
118        {
119            // Path where current part is a data holder
120            ModifiableModelLessDataHolder parent = _getParentValue(this, compositePath);
121            if (parent == null)
122            {
123                return null;
124            }
125            else
126            {
127                String childName = compositePath.substring(compositePath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
128                return parent.getComposite(childName, createNew);
129            }
130        }
131    }
132    
133    /**
134     * Retrieves the composite with the given name
135     * @param name name of the composite to retrieve
136     * @param createNew <code>true</code> to create the composite if it does not exist, <code>false</code> otherwise
137     * @return the composite
138     * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite
139     */
140    protected ModifiableModelLessComposite _getComposite(String name, boolean createNew) throws BadItemTypeException
141    {
142        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) _typeExtensionPoint.getExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID);
143        RepositoryData compositeRepositoryData = type.read(_modifiableRepositoryData, name);
144        
145        if (compositeRepositoryData != null)
146        {
147            return new DefaultModifiableModelLessComposite(_typeExtensionPoint, (ModifiableRepositoryData) compositeRepositoryData, this, _modifiableRoot);
148        }
149        else
150        {
151            if (createNew)
152            {
153                ModifiableRepositoryData createdRepositoryData = type.add(_modifiableRepositoryData, name);
154                return new DefaultModifiableModelLessComposite(_typeExtensionPoint, createdRepositoryData, this, _modifiableRoot);
155            }
156            else
157            {
158                return null;
159            }
160        }
161    }
162    
163    @SuppressWarnings("unchecked")
164    public boolean synchronizeValues(Map<String, Object> values) throws UnknownTypeException, NotUniqueTypeException
165    {
166        boolean hasChanged = false;
167        
168        for (Map.Entry<String, Object> entry : values.entrySet())
169        {
170            String dataName = entry.getKey();
171            Object value = entry.getValue();
172            
173            if (value instanceof Map)
174            {
175                ModifiableModelLessComposite composite = getComposite(dataName, true);
176                hasChanged = composite.synchronizeValues((Map<String, Object>) value) || hasChanged;
177            }
178            else
179            {
180                if (_repositoryData.hasValue(dataName + RepositoryModelItemType.EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL))
181                {
182                    // The old value is set as empty via an internal boolean property
183                    if (value != null)
184                    {
185                        // the new value is not empty
186                        hasChanged = true;
187                        setValue(dataName, value);
188                    }
189                }
190                else if (hasValueOrEmpty(dataName))
191                {
192                    ElementType type = (ElementType) getType(dataName);
193                    Object oldValue = getValue(dataName);
194                    
195                    if (type.compareValues(value, oldValue).count() > 0)
196                    {
197                        hasChanged = true;
198                        setValue(dataName, value);
199                    }
200                }
201                else
202                {
203                    // There is no old value. If the given value is null, there is no way to determine the type of the value to set
204                    if (value != null)
205                    {
206                        hasChanged = true;
207                        setValue(dataName, value);
208                    }
209                }
210            }
211        }
212        
213        return hasChanged;
214    }
215
216    public void setValue(String dataPath, Object value) throws IllegalArgumentException, UnknownTypeException, NotUniqueTypeException, UnknownDataException
217    {
218        List<RepositoryModelItemType> compatibleTypes = _typeExtensionPoint.getExtensionsIds().stream()
219                .map(id -> _typeExtensionPoint.getExtension(id))
220                .filter(RepositoryElementType.class::isInstance)
221                .map(RepositoryElementType.class::cast)
222                .filter(type -> type.isCompatible(value))
223                .collect(Collectors.toList());
224        
225        if (compatibleTypes.isEmpty())
226        {
227            // The type has not been found, throw an UnknownTypeException
228            String availableTypes = StringUtils.join(_typeExtensionPoint.getExtensionsIds(), ", ");
229            throw new UnknownTypeException("Unable to retrieve the type of the data '" + dataPath + "'. No compatible type have been found. Available types are: '" + availableTypes + "'.");
230        }
231        else if (compatibleTypes.size() > 1)
232        {
233            // Many types have been found, throw an UnknownTypeException
234            List<String> compatibleTypesIds = compatibleTypes.stream().map(type -> type.getId()).collect(Collectors.toList());
235            throw new NotUniqueTypeException("Unable to retrieve the type of the data '" + dataPath + "'. Many compatible types have been found, there is no way to determine which one is the good one. Compatible types found are: " + StringUtils.join(compatibleTypesIds, ", "));
236        }
237        else
238        {
239            setValue(dataPath, value, compatibleTypes.get(0).getId());
240        }
241    }
242
243    public void setValue(String dataPath, Object value, String dataTypeId) throws IllegalArgumentException, BadItemTypeException, UnknownDataException
244    {
245        if (!_typeExtensionPoint.hasExtension(dataTypeId))
246        {
247            String availableTypes = StringUtils.join(_typeExtensionPoint.getExtensionsIds(), ", ");
248            throw new UnknownTypeException("The type '" + dataTypeId + "' is not available for the extension point '" + _typeExtensionPoint + "'. Available types are: '" + availableTypes + "'.");
249        }
250        
251        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
252        
253        if (pathSegments == null || pathSegments.length < 1)
254        {
255            throw new IllegalArgumentException("Unable to set a value at the given path. This path is empty.");
256        }
257        else if (pathSegments.length == 1)
258        {
259            // Simple path => set the value
260            RepositoryModelItemType type = (RepositoryModelItemType) _typeExtensionPoint.getExtension(dataTypeId);
261            if (type instanceof RepositoryElementType)
262            {
263                ((RepositoryElementType) type).write(_modifiableRepositoryData, dataPath, value);
264            }
265            else
266            {
267                throw new BadItemTypeException("Unable to set the value '" + value + "' on the data at path '" + dataPath + "' in the repository because it is a group item.");
268            }
269        }
270        else
271        {
272            // Path where current part is a data holder
273            ModifiableModelLessDataHolder parent = _getParentValue(this, dataPath);
274            if (parent == null)
275            {
276                String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
277                throw new UnknownDataException("The data at path '" + parentPath + "' does not exist. It is not possible to get the data at path '" + dataPath + "'.");
278            }
279            else
280            {
281                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
282                parent.setValue(childName, value, dataTypeId);
283            }
284        }
285    }
286
287    public void removeValue(String dataPath) throws IllegalArgumentException, BadItemTypeException
288    {
289        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
290        
291        if (pathSegments == null || pathSegments.length < 1)
292        {
293            throw new IllegalArgumentException("Unable to remove the value at the given path. This path is empty.");
294        }
295        else if (pathSegments.length == 1)
296        {
297            if (_modifiableRepositoryData.hasValue(dataPath))
298            {
299                _modifiableRepositoryData.removeValue(dataPath);
300            }
301
302            if (_modifiableRepositoryData.hasValue(dataPath + RepositoryModelItemType.EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL))
303            {
304                _modifiableRepositoryData.removeValue(dataPath + RepositoryModelItemType.EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
305            }
306
307        }
308        else
309        {
310            // Path where current part is a data holder
311            ModifiableModelLessDataHolder parent = _getParentValue(this, dataPath);
312            if (parent != null)
313            {
314                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
315                parent.removeValue(childName);
316            }
317        }
318    }
319    
320    /**
321     * Retrieves the modifiable data holder, last parent segment of the given data path
322     * Example : call this method with a path like 'my-composite1/my-composite2/my-data' will retrieve the composite 'my-composite2' in the composite 'my-composite1'
323     * @param modifiableDataHolder the modifiable data holder
324     * @param dataPath the data path
325     * @return the parent data holder
326     * @throws BadDataPathCardinalityException if the value of a part of the data path is multiple. Only the last part can be multiple
327     * @throws BadItemTypeException if the value at the given data path is not a composite
328     */
329    protected static ModifiableModelLessDataHolder _getParentValue(ModifiableModelLessDataHolder modifiableDataHolder, String dataPath) throws BadDataPathCardinalityException, BadItemTypeException
330    {
331        return (ModifiableModelLessDataHolder) DefaultModelLessDataHolder._getParentValue(modifiableDataHolder, dataPath);
332    }
333    
334    @Override
335    public ModifiableRepositoryData getRepositoryData()
336    {
337        return _modifiableRepositoryData;
338    }
339    
340    @Override
341    public Optional<? extends ModifiableModelLessDataHolder> getParentDataHolder()
342    {
343        return _modifiableParent;
344    }
345    
346    @Override
347    public ModifiableModelLessDataHolder getRootDataHolder()
348    {
349        return _modifiableRoot;
350    }
351}