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.Collection;
019import java.util.List;
020import java.util.Optional;
021import java.util.stream.Collectors;
022
023import org.apache.commons.lang3.StringUtils;
024import org.xml.sax.ContentHandler;
025import org.xml.sax.SAXException;
026
027import org.ametys.plugins.repository.RepositoryConstants;
028import org.ametys.plugins.repository.data.UnknownDataException;
029import org.ametys.plugins.repository.data.ametysobject.DataAwareAmetysObject;
030import org.ametys.plugins.repository.data.holder.DataHolder;
031import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
032import org.ametys.plugins.repository.data.holder.group.impl.ModelLessComposite;
033import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
034import org.ametys.plugins.repository.data.type.ModelItemTypeConstants;
035import org.ametys.plugins.repository.data.type.RepositoryElementType;
036import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType;
037import org.ametys.plugins.repository.data.type.RepositoryModelItemType;
038import org.ametys.runtime.model.ModelItem;
039import org.ametys.runtime.model.exception.BadDataPathCardinalityException;
040import org.ametys.runtime.model.exception.BadItemTypeException;
041import org.ametys.runtime.model.exception.NotUniqueTypeException;
042import org.ametys.runtime.model.exception.UnknownTypeException;
043import org.ametys.runtime.model.type.DataContext;
044import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
045
046/**
047 * Default implementation for data holder without model
048 */
049public class DefaultModelLessDataHolder implements ModelLessDataHolder
050{
051    /** Extension point to use to get available element types */
052    protected AbstractThreadSafeComponentExtensionPoint<RepositoryModelItemType> _typeExtensionPoint;
053    
054    /** Repository data to use to store data in the repository */
055    protected RepositoryData _repositoryData;
056    
057    /** Parent of the current {@link DataHolder} */
058    protected Optional<? extends ModelLessDataHolder> _parent;
059    
060    /** Root {@link DataHolder} */
061    protected ModelLessDataHolder _root;
062    
063    /**
064     * Creates a default model free data holder
065     * @param typeExtensionPoint the extension point to use to get available element types
066     * @param repositoryData the repository data to use
067     */
068    public DefaultModelLessDataHolder(AbstractThreadSafeComponentExtensionPoint<RepositoryModelItemType> typeExtensionPoint, RepositoryData repositoryData)
069    {
070        this(typeExtensionPoint, repositoryData, Optional.empty(), Optional.empty());
071    }
072    
073    /**
074     * Creates a default model free data holder
075     * @param typeExtensionPoint the extension point to use to get available element types
076     * @param parent the parent of the created {@link DataHolder}, empty if the created {@link DataHolder} is the root {@link DataHolder}
077     * @param root the root {@link DataAwareAmetysObject}
078     * @param repositoryData the repository data to use
079     */
080    public DefaultModelLessDataHolder(AbstractThreadSafeComponentExtensionPoint<RepositoryModelItemType> typeExtensionPoint, RepositoryData repositoryData, Optional<? extends ModelLessDataHolder> parent, Optional<? extends ModelLessDataHolder> root)
081    {
082        _typeExtensionPoint = typeExtensionPoint;
083        _repositoryData = repositoryData;
084        
085        _parent = parent;
086        _root = root.map(ModelLessDataHolder.class::cast)
087                .or(() -> _parent.map(ModelLessDataHolder::getRootDataHolder)) // if no root is specified but a parent, the root is the parent's root
088                .orElse(this); // if no root or parent is specified, the root is the current DataHolder
089    }
090    
091    public ModelLessComposite getComposite(String compositePath) throws IllegalArgumentException, BadItemTypeException
092    {
093        Object value = getValueOfType(compositePath, ModelItemTypeConstants.COMPOSITE_TYPE_ID);
094        if (value == null)
095        {
096            return null;
097        }
098        else if (value instanceof ModelLessComposite)
099        {
100            return (ModelLessComposite) value;
101        }
102        else
103        {
104            throw new BadItemTypeException("The item at path '" + compositePath + "' is not a composite.");
105        }
106    }
107
108    public <T> T getValue(String dataPath) throws IllegalArgumentException, UnknownTypeException, NotUniqueTypeException
109    {
110        String[] pathSegments = StringUtils.split(dataPath,  ModelItem.ITEM_PATH_SEPARATOR);
111        
112        if (pathSegments == null || pathSegments.length < 1)
113        {
114            throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty.");
115        }
116        else if (pathSegments.length == 1)
117        {
118            try
119            {
120                if (_repositoryData.hasValue(dataPath + RepositoryModelItemType.EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL))
121                {
122                    return null;
123                }
124                else
125                {
126                    return getValueOfType(dataPath, getType(dataPath).getId());
127                }
128            }
129            catch (UnknownDataException e)
130            {
131                return null;
132            }
133        }
134        else
135        {
136            // Path where current part is a data holder
137            ModelLessDataHolder parent = _getParentValue(this, dataPath);
138            if (parent == null)
139            {
140                return null;
141            }
142            else
143            {
144                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
145                return parent.getValue(childName);
146            }
147        }
148    }
149    
150    public <T> T getValue(String dataPath, T defaultValue) throws IllegalArgumentException, UnknownTypeException, NotUniqueTypeException
151    {
152        String[] pathSegments = StringUtils.split(dataPath,  ModelItem.ITEM_PATH_SEPARATOR);
153        
154        if (pathSegments == null || pathSegments.length < 1)
155        {
156            throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty.");
157        }
158        else if (pathSegments.length == 1)
159        {
160            try
161            {
162                if (_repositoryData.hasValue(dataPath + RepositoryModelItemType.EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL))
163                {
164                    return defaultValue;
165                }
166                else
167                {
168                    return getValueOfType(dataPath, getType(dataPath).getId(), defaultValue);
169                }
170            }
171            catch (UnknownDataException e)
172            {
173                return defaultValue;
174            }
175        }
176        else
177        {
178            // Path where current part is a data holder
179            ModelLessDataHolder parent = _getParentValue(this, dataPath);
180            if (parent == null)
181            {
182                return defaultValue;
183            }
184            else
185            {
186                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
187                return parent.getValue(childName, defaultValue);
188            }
189        }
190    }
191    
192    public <T> T getValueOfType(String dataPath, String dataTypeId, T defaultValue) throws IllegalArgumentException, BadItemTypeException
193    {
194        if (!_typeExtensionPoint.hasExtension(dataTypeId))
195        {
196            String availableTypes = StringUtils.join(_typeExtensionPoint.getExtensionsIds(), ", ");
197            throw new UnknownTypeException("The type '" + dataTypeId + "' is not available for the extension point '" + _typeExtensionPoint + "'. Available types are: '" + availableTypes + "'.");
198        }
199        RepositoryModelItemType type = _typeExtensionPoint.getExtension(dataTypeId);
200        
201        if (_hasValue(this, dataPath, type))
202        {
203            return getValueOfType(dataPath, dataTypeId);
204        }
205        
206        return defaultValue;
207    }
208    
209    @SuppressWarnings("unchecked")
210    public <T> T getValueOfType(String dataPath, String dataTypeId) throws IllegalArgumentException, UnknownTypeException, BadItemTypeException, BadDataPathCardinalityException
211    {
212        if (!_typeExtensionPoint.hasExtension(dataTypeId))
213        {
214            String availableTypes = StringUtils.join(_typeExtensionPoint.getExtensionsIds(), ", ");
215            throw new UnknownTypeException("The type '" + dataTypeId + "' is not available for the extension point '" + _typeExtensionPoint + "'. Available types are: '" + availableTypes + "'.");
216        }
217        
218        String[] pathSegments = StringUtils.split(dataPath,  ModelItem.ITEM_PATH_SEPARATOR);
219        
220        if (pathSegments == null || pathSegments.length < 1)
221        {
222            throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty.");
223        }
224        else if (pathSegments.length == 1)
225        {
226            // Simple path => get the value
227            RepositoryModelItemType type = _typeExtensionPoint.getExtension(dataTypeId);
228            if (type instanceof RepositoryElementType)
229            {
230                return (T) ((RepositoryElementType) type).read(_repositoryData, dataPath);
231            }
232            else
233            {
234                return (T) _getComposite(dataPath);
235            }
236        }
237        else
238        {
239            // Path where current part is a data holder
240            ModelLessDataHolder parent = _getParentValue(this, dataPath);
241            if (parent == null)
242            {
243                return null;
244            }
245            else
246            {
247                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
248                return parent.getValueOfType(childName, dataTypeId);
249            }
250        }
251    }
252    
253    /**
254     * Retrieves the composite with the given name
255     * @param name name of the composite to retrieve
256     * @return the composite
257     * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite
258     */
259    protected ModelLessComposite _getComposite(String name) throws BadItemTypeException
260    {
261        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) _typeExtensionPoint.getExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID);
262        RepositoryData compositeRepositoryData = type.read(_repositoryData, name);
263        
264        if (compositeRepositoryData != null)
265        {
266            return new ModelLessComposite(_typeExtensionPoint, compositeRepositoryData, this, _root);
267        }
268        else
269        {
270            return null;
271        }
272    }
273    
274    public boolean hasValue(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
275    {
276        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
277        
278        if (pathSegments == null || pathSegments.length < 1)
279        {
280            throw new IllegalArgumentException("The data path is empty. It is not possible to determine if it has a value or not.");
281        }
282        else if (pathSegments.length == 1)
283        {
284            try
285            {
286                RepositoryModelItemType type = getType(dataPath);
287                return type.hasNonEmptyValue(_repositoryData, dataPath);
288            }
289            catch (UnknownDataException | UnknownTypeException | NotUniqueTypeException e)
290            {
291                return false;
292            }
293        }
294        else
295        {
296            // Path where current part is a data holder
297            ModelLessDataHolder parent = _getParentValue(this, dataPath);
298            if (parent == null)
299            {
300                return false;
301            }
302            else
303            {
304                String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
305                return parent.hasValue(childName);
306            }
307        }
308    }
309    
310    public boolean hasValueOrEmpty(String dataPath) throws IllegalArgumentException, BadDataPathCardinalityException
311    {
312        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
313        
314        if (pathSegments == null || pathSegments.length < 1)
315        {
316            throw new IllegalArgumentException("The data path is empty. It is not possible to determine if it has a value or not.");
317        }
318        else if (pathSegments.length == 1)
319        {
320            return _repositoryData.hasValue(dataPath) || _repositoryData.hasValue(dataPath + RepositoryModelItemType.EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
321        }
322        else
323        {
324            // Path where current part is a data holder
325            ModelLessDataHolder parent = _getParentValue(this, dataPath);
326            if (parent == null)
327            {
328                return false;
329            }
330            else
331            {
332                String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
333                return parent.hasValueOrEmpty(childName);
334            }
335        }
336    }
337    
338    /**
339     * Checks if there is a non empty value for the data at the given path
340     * @param dataHolder the data holder
341     * @param dataPath path of the data
342     * @param dataType the type of the data
343     * @return <code>true</code> if there is a non empty value for the data, <code>false</code> otherwise
344     * @throws IllegalArgumentException if the given data path is null or empty
345     * @throws BadDataPathCardinalityException if the value of a part of the data path is multiple. Only the last part can be multiple
346     */
347    protected static boolean _hasValue(ModelLessDataHolder dataHolder, String dataPath, RepositoryModelItemType dataType) throws IllegalArgumentException, BadDataPathCardinalityException
348    {
349        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
350        
351        if (pathSegments == null || pathSegments.length < 1)
352        {
353            throw new IllegalArgumentException("The data path is empty. It is not possible to determine if it has a value or not.");
354        }
355        else if (pathSegments.length == 1)
356        {
357            return dataType.hasNonEmptyValue(dataHolder.getRepositoryData(), dataPath);
358        }
359        else
360        {
361            // Path where current part is a data holder
362            ModelLessDataHolder parent = _getParentValue(dataHolder, dataPath);
363            if (parent == null)
364            {
365                return false;
366            }
367            else
368            {
369                String childName = StringUtils.substringAfterLast(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
370                return _hasValue(parent, childName, dataType);
371            }
372        }
373    }
374
375    public boolean isMultiple(String dataPath) throws IllegalArgumentException, UnknownDataException, NotUniqueTypeException, BadDataPathCardinalityException
376    {
377        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
378        
379        if (pathSegments == null || pathSegments.length < 1)
380        {
381            throw new IllegalArgumentException("The data path is empty. It is not possible to determine if it is multiple or not.");
382        }
383        else if (pathSegments.length == 1)
384        {
385            RepositoryModelItemType type = getType(dataPath);
386            return type.isMultiple(_repositoryData, dataPath);
387        }
388        else
389        {
390            // Path where current part is a data holder
391            ModelLessDataHolder parent = _getParentValue(this, dataPath);
392            if (parent == null)
393            {
394                throw new UnknownDataException("The data at path '" + dataPath + "' does not exist. It is not possible to determine if it is multiple or not.");
395            }
396            else
397            {
398                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
399                return parent.isMultiple(childName);
400            }
401        }
402    }
403    
404    public RepositoryModelItemType getType(String dataPath) throws IllegalArgumentException, UnknownTypeException, UnknownDataException, NotUniqueTypeException
405    {
406        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
407        
408        if (pathSegments == null || pathSegments.length < 1)
409        {
410            throw new IllegalArgumentException("The data path is empty. It is not possible to determine its type.");
411        }
412        else if (pathSegments.length == 1)
413        {
414            return _getType(dataPath);
415        }
416        else
417        {
418            // Path where current part is a data holder
419            ModelLessDataHolder parent = _getParentValue(this, dataPath);
420            if (parent == null)
421            {
422                throw new UnknownDataException("The data at path '" + dataPath + "' does not exist. It is not possible to determine its type.");
423            }
424            else
425            {
426                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
427                return parent.getType(childName);
428            }
429        }
430    }
431    
432    private RepositoryModelItemType _getType(String dataName) throws UnknownTypeException, UnknownDataException, NotUniqueTypeException
433    {
434        List<RepositoryModelItemType> compatibleTypes = _typeExtensionPoint.getExtensionsIds().stream()
435                .map(id -> _typeExtensionPoint.getExtension(id))
436                .filter(type -> type.isCompatible(_repositoryData, dataName))
437                .collect(Collectors.toList());
438        if (compatibleTypes.isEmpty())
439        {
440            // The type has not been found, thrown an UnknownTypeException
441            throw new UnknownTypeException("Unable to retrieve the type of the data '" + dataName + "'. No compatible type have been found.");
442        }
443        else if (compatibleTypes.size() > 1)
444        {
445            // Many types have been found, thrown an UnknownTypeException
446            List<String> compatibleTypesIds = compatibleTypes.stream().map(type -> type.getId()).collect(Collectors.toList());
447            throw new NotUniqueTypeException("Unable to retrieve the type of the data '" + dataName + "'. 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, ", "));
448        }
449        else
450        {
451            return compatibleTypes.get(0);
452        }
453    }
454    
455    /**
456     * Retrieves the data holder, last parent segment of the given data path
457     * 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'
458     * @param dataHolder the data holder
459     * @param dataPath the data path
460     * @return the parent data holder
461     * @throws BadDataPathCardinalityException if the value of a part of the data path is multiple. Only the last part can be multiple
462     * @throws BadItemTypeException if the value at the given data path is not a composite
463     */
464    protected static ModelLessDataHolder _getParentValue(ModelLessDataHolder dataHolder, String dataPath) throws BadDataPathCardinalityException, BadItemTypeException
465    {
466        String[] pathSegments = StringUtils.split(dataPath,  ModelItem.ITEM_PATH_SEPARATOR);
467        String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
468        
469        return dataHolder.getValueOfType(parentPath, ModelItemTypeConstants.COMPOSITE_TYPE_ID);
470    }
471    
472    public Collection<String> getDataNames()
473    {
474        return _repositoryData.getDataNames();
475    }
476
477    public void dataToSAX(ContentHandler contentHandler, String dataPath, DataContext context) throws SAXException, UnknownTypeException, NotUniqueTypeException
478    {
479        DataHolderHelper.dataToSAX(this, contentHandler, dataPath, context.withDataPath(dataPath));
480    }
481    
482    public void dataToSAX(ContentHandler contentHandler, DataContext context) throws SAXException, UnknownTypeException, NotUniqueTypeException
483    {
484        DataHolderHelper.dataToSAX(this, contentHandler, context);
485    }
486    
487    public RepositoryData getRepositoryData()
488    {
489        return _repositoryData;
490    }
491    
492    public Optional<? extends ModelLessDataHolder> getParentDataHolder()
493    {
494        return _parent;
495    }
496    
497    public ModelLessDataHolder getRootDataHolder()
498    {
499        return _root;
500    }
501}