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.stream.Collectors;
021
022import org.apache.commons.lang3.StringUtils;
023import org.apache.commons.lang3.tuple.Pair;
024
025import org.ametys.plugins.repository.data.UnknownDataException;
026import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
027import org.ametys.plugins.repository.data.holder.group.Repeater;
028import org.ametys.plugins.repository.data.holder.group.impl.ModelLessComposite;
029import org.ametys.plugins.repository.data.holder.group.impl.ModelLessRepeater;
030import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
031import org.ametys.plugins.repository.data.type.ModelItemTypeConstants;
032import org.ametys.plugins.repository.data.type.RepositoryElementType;
033import org.ametys.plugins.repository.data.type.RepositoryModelItemGroupType;
034import org.ametys.plugins.repository.data.type.RepositoryModelItemType;
035import org.ametys.runtime.model.ModelItem;
036import org.ametys.runtime.model.exception.BadDataPathCardinalityException;
037import org.ametys.runtime.model.exception.BadItemTypeException;
038import org.ametys.runtime.model.exception.NotUniqueTypeException;
039import org.ametys.runtime.model.exception.UnknownTypeException;
040import org.ametys.runtime.model.type.ModelItemType;
041import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
042
043/**
044 * Default implementation for data holder without model
045 */
046public class DefaultModelLessDataHolder implements ModelLessDataHolder
047{
048    /** Extension point to use to get available element types */
049    protected AbstractThreadSafeComponentExtensionPoint<RepositoryModelItemType> _typeExtensionPoint;
050    
051    /** Repository data to use to store data in the repository */
052    protected RepositoryData _repositoryData;
053    
054    /**
055     * Creates a default model free data holder
056     * @param typeExtensionPoint the extension point to use to get available element types
057     * @param repositoryData the repository data to use
058     */
059    public DefaultModelLessDataHolder(AbstractThreadSafeComponentExtensionPoint<RepositoryModelItemType> typeExtensionPoint, RepositoryData repositoryData)
060    {
061        _typeExtensionPoint = typeExtensionPoint;
062        _repositoryData = repositoryData;
063    }
064    
065    public ModelLessComposite getComposite(String compositePath) throws IllegalArgumentException, BadItemTypeException
066    {
067        Object value = getValueOfType(compositePath, ModelItemTypeConstants.COMPOSITE_TYPE_ID);
068        if (value == null)
069        {
070            return null;
071        }
072        else if (value instanceof ModelLessComposite)
073        {
074            return (ModelLessComposite) value;
075        }
076        else
077        {
078            throw new BadItemTypeException("The item at path '" + compositePath + "' is not a composite.");
079        }
080    }
081
082    public ModelLessRepeater getRepeater(String repeaterPath) throws IllegalArgumentException, BadItemTypeException
083    {
084        Object value = getValueOfType(repeaterPath, ModelItemTypeConstants.REPEATER_TYPE_ID);
085        if (value == null)
086        {
087            return null;
088        }
089        else if (value instanceof ModelLessRepeater)
090        {
091            return (ModelLessRepeater) value;
092        }
093        else
094        {
095            throw new BadItemTypeException("The data at path '" + repeaterPath + "' is not a repeater.");
096        }
097    }
098    
099    public <T> T getValue(String dataPath) throws IllegalArgumentException, UnknownTypeException, NotUniqueTypeException
100    {
101        try
102        {
103            return getValueOfType(dataPath, getType(dataPath).getId());
104        }
105        catch (UnknownDataException e)
106        {
107            return null;
108        }
109    }
110    
111    public <T> T getValue(String dataPath, T defaultValue) throws IllegalArgumentException, UnknownTypeException, NotUniqueTypeException
112    {
113        try
114        {
115            return getValueOfType(dataPath, getType(dataPath).getId(), defaultValue);
116        }
117        catch (UnknownDataException e)
118        {
119            return defaultValue;
120        }
121    }
122    
123    public <T> T getValueOfType(String dataPath, String dataTypeId, T defaultValue) throws IllegalArgumentException, BadItemTypeException
124    {
125        T value = getValueOfType(dataPath, dataTypeId);
126        return value != null ? value : defaultValue;
127    }
128    
129    @SuppressWarnings("unchecked")
130    public <T> T getValueOfType(String dataPath, String dataTypeId) throws IllegalArgumentException, BadItemTypeException
131    {
132        if (!_typeExtensionPoint.hasExtension(dataTypeId))
133        {
134            String availableTypes = StringUtils.join(_typeExtensionPoint.getExtensionsIds(), ", ");
135            throw new UnknownTypeException("The type '" + dataTypeId + "' is not available for the extension point '" + _typeExtensionPoint + "'. Available types are: '" + availableTypes + "'.");
136        }
137        
138        String[] pathSegments = StringUtils.split(dataPath,  ModelItem.ITEM_PATH_SEPARATOR);
139        
140        if (pathSegments == null || pathSegments.length < 1)
141        {
142            throw new IllegalArgumentException("Unable to retrieve the data at the given path. This path is empty.");
143        }
144        else if (pathSegments.length == 1)
145        {
146            // Simple path => get the value
147            RepositoryModelItemType type = _typeExtensionPoint.getExtension(dataTypeId);
148            if (type instanceof RepositoryElementType)
149            {
150                return (T) ((RepositoryElementType) type).read(_repositoryData, dataPath);
151            }
152            else
153            {
154                Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath);
155                if (repeaterNameAndEntryPosition != null)
156                {
157                    return (T) DataHolderHelper.getRepeaterEntry(this, repeaterNameAndEntryPosition.getLeft(), repeaterNameAndEntryPosition.getRight());
158                }
159                else
160                {
161                    if (ModelItemTypeConstants.REPEATER_TYPE_ID.equals(type.getId()))
162                    {
163                        return (T) _getRepeater(dataPath);
164                    }
165                    else
166                    {
167                        return (T) _getComposite(dataPath);
168                    }
169                }
170            }
171        }
172        else
173        {
174            // Path where current part is a data holder
175            ModelLessDataHolder parent = _getParentValue(dataPath);
176            if (parent == null)
177            {
178                return null;
179            }
180            else
181            {
182                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
183                return parent.getValueOfType(childName, dataTypeId);
184            }
185        }
186    }
187    
188    /**
189     * Retrieves the composite with the given name
190     * @param name name of the composite to retrieve
191     * @return the composite
192     * @throws BadItemTypeException if the value stored in the repository with the given name is not a composite
193     */
194    protected ModelLessComposite _getComposite(String name) throws BadItemTypeException
195    {
196        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) _typeExtensionPoint.getExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID);
197        RepositoryData compositeRepositoryData = type.read(_repositoryData, name);
198        
199        if (compositeRepositoryData != null)
200        {
201            return new ModelLessComposite(_typeExtensionPoint, compositeRepositoryData);
202        }
203        else
204        {
205            return null;
206        }
207    }
208    
209    /**
210     * Retrieves the repeater with the given name
211     * @param name name of the repeater to retrieve
212     * @return the repeater
213     * @throws BadItemTypeException if the value stored in the repository with the given name is not a repeater
214     */
215    protected ModelLessRepeater _getRepeater(String name) throws BadItemTypeException
216    {
217        RepositoryModelItemGroupType type = (RepositoryModelItemGroupType) _typeExtensionPoint.getExtension(ModelItemTypeConstants.REPEATER_TYPE_ID);
218        RepositoryData repeaterRepositoryData = type.read(_repositoryData, name);
219        
220        if (repeaterRepositoryData != null)
221        {
222            return new ModelLessRepeater(_typeExtensionPoint, repeaterRepositoryData);
223        }
224        else
225        {
226            return null;
227        }
228    }
229
230    public boolean hasValue(String dataPath) throws IllegalArgumentException
231    {
232        String[] pathSegments = StringUtils.split(dataPath, ModelItem.ITEM_PATH_SEPARATOR);
233        
234        if (pathSegments == null || pathSegments.length < 1)
235        {
236            throw new IllegalArgumentException("The data path is empty. It is not possible to determine if it has a value or not.");
237        }
238        else if (pathSegments.length == 1)
239        {
240            Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath);
241            if (repeaterNameAndEntryPosition != null)
242            {
243                if (_repositoryData.hasValue(repeaterNameAndEntryPosition.getLeft()))
244                {
245                    Repeater repeater = _getRepeater(repeaterNameAndEntryPosition.getLeft());
246                    return repeater.hasEntry(repeaterNameAndEntryPosition.getRight());
247                }
248                else
249                {
250                    return false;
251                }
252            }
253            else
254            {
255                return _repositoryData.hasValue(dataPath);
256            }
257        }
258        else
259        {
260            // Path where current part is a data holder
261            ModelLessDataHolder parent = _getParentValue(dataPath);
262            if (parent == null)
263            {
264                return false;
265            }
266            else
267            {
268                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
269                return parent.hasValue(childName);
270            }
271        }
272    }
273
274    public boolean isMultiple(String dataPath) throws IllegalArgumentException, UnknownDataException
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 is multiple or not.");
281        }
282        else if (pathSegments.length == 1)
283        {
284            Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataPath);
285            if (repeaterNameAndEntryPosition != null)
286            {
287                return false;
288            }
289            else
290            {
291                return _repositoryData.isMultiple(dataPath);
292            }
293        }
294        else
295        {
296            // Path where current part is a data holder
297            ModelLessDataHolder parent = _getParentValue(dataPath);
298            if (parent == null)
299            {
300                throw new UnknownDataException("The data at path '" + dataPath + "' does not exist. It is not possible to determine if it is multiple or not.");
301            }
302            else
303            {
304                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
305                return parent.isMultiple(childName);
306            }
307        }
308    }
309    
310    public ModelItemType getType(String dataPath) throws IllegalArgumentException, UnknownTypeException, UnknownDataException, NotUniqueTypeException
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 its type.");
317        }
318        else if (pathSegments.length == 1)
319        {
320            return _getType(dataPath);
321        }
322        else
323        {
324            // Path where current part is a data holder
325            ModelLessDataHolder parent = _getParentValue(dataPath);
326            if (parent == null)
327            {
328                throw new UnknownDataException("The data at path '" + dataPath + "' does not exist. It is not possible to determine its type.");
329            }
330            else
331            {
332                String childName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR) + 1);
333                return parent.getType(childName);
334            }
335        }
336    }
337    
338    private ModelItemType _getType(String dataName) throws UnknownTypeException, UnknownDataException, NotUniqueTypeException
339    {
340        Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataName);
341        if (repeaterNameAndEntryPosition != null)
342        {
343            if (_typeExtensionPoint.hasExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID))
344            {
345                return _typeExtensionPoint.getExtension(ModelItemTypeConstants.COMPOSITE_TYPE_ID);
346            }
347            else
348            {
349                throw new UnknownTypeException("Unable to retrieve the type of the data '" + dataName + "'. This data is a repeater entry but composites are not allowed for this object");
350            }
351        }
352        else
353        {
354            List<RepositoryModelItemType> compatibleTypes = _typeExtensionPoint.getExtensionsIds().stream()
355                                                                                                 .map(id -> _typeExtensionPoint.getExtension(id))
356                                                                                                 .filter(type -> type.isCompatible(_repositoryData, dataName))
357                                                                                                 .collect(Collectors.toList());
358            if (compatibleTypes.isEmpty())
359            {
360                // The type has not been found, thrown an UnknownTypeException
361                throw new UnknownTypeException("Unable to retrieve the type of the data '" + dataName + "'. No compatible type have been found.");
362            }
363            else if (compatibleTypes.size() > 1)
364            {
365                // Many types have been found, thrown an UnknownTypeException
366                List<String> compatibleTypesIds = compatibleTypes.stream().map(type -> type.getId()).collect(Collectors.toList());
367                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, ", "));
368            }
369            else
370            {
371                return compatibleTypes.get(0);
372            }
373        }
374    }
375    
376    /**
377     * Retrieves the data holder, last parent segment of the given data path
378     * Example : call this method with a path like 'my-composite/my-repeater[1]/my-data' will retrieve the first repeater entry of the repeater 'my-repeater' in the composite 'my-composite'
379     * @param dataPath the data path
380     * @return the parent data holder
381     */
382    protected ModelLessDataHolder _getParentValue(String dataPath)
383    {
384        String[] pathSegments = StringUtils.split(dataPath,  ModelItem.ITEM_PATH_SEPARATOR);
385        String parentPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 0, pathSegments.length - 1);
386        
387        try
388        {
389            // Multiple items are allowed only at the last segment of the data path
390            if (isMultiple(parentPath))
391            {
392                throw new BadDataPathCardinalityException("Unable to get the parent of 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.");
393            }
394        }
395        catch (UnknownDataException e)
396        {
397            return null;
398        }
399        
400        try
401        {
402            return getValue(parentPath);
403        }
404        catch (NotUniqueTypeException e)
405        {
406            // This case can occur whit old node type (compositeMetadata), used by both composites and repeaters
407            return getValueOfType(parentPath, ModelItemTypeConstants.COMPOSITE_TYPE_ID);
408        }
409    }
410    
411    public Collection<String> getDataNames()
412    {
413        return _repositoryData.getDataNames();
414    }
415}