001/*
002 *  Copyright 2019 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.type;
017
018import java.lang.reflect.Array;
019
020import org.ametys.plugins.repository.RepositoryConstants;
021import org.ametys.plugins.repository.data.UnknownDataException;
022import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
023import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
024import org.ametys.runtime.model.exception.BadItemTypeException;
025
026/**
027 * Interface for complex types of elements stored in the repository
028 * The complex types ares those which need nodes to be stored
029 * @param <T> Type of the element value
030 */
031public interface ComplexRepositoryElementType<T> extends RepositoryElementType<T>
032{
033    /** Data name for storing the type identifier of an element with multiple values*/
034    public static String TYPE_ID_DATA_NAME = "typeId";
035    
036    @SuppressWarnings("unchecked")
037    public default Object read(RepositoryData parentData, String name) throws BadItemTypeException
038    {
039        if (!parentData.hasValue(name))
040        {
041            return null;
042        }
043        
044        if (!isCompatible(parentData, name))
045        {
046            throw new BadItemTypeException("Try to get " + getId() + " value from the non '" + getId() + "' data '" + name + "' on '" + parentData + "'");
047        }
048        
049        if (parentData.isMultiple(name))
050        {
051            RepositoryData multipleParentData = parentData.getRepositoryData(name);
052            
053            return multipleParentData.getDataNames()
054                                     .stream()
055                                     .map(singleDataName -> multipleParentData.getRepositoryData(singleDataName))
056                                     .map(singleData -> readSingleValue(singleData))
057                                     .toArray(size -> (T[]) Array.newInstance(getManagedClass(), size));
058        }
059        else
060        {
061            RepositoryData data = parentData.getRepositoryData(name);
062            return !isSingleValueEmpty(data) ? readSingleValue(data) : null;
063        }
064    }
065    
066    public default boolean hasNonEmptyValue(RepositoryData parentData, String name) throws BadItemTypeException
067    {
068        if (!parentData.hasValue(name))
069        {
070            return false;
071        }
072        
073        if (!isCompatible(parentData, name))
074        {
075            throw new BadItemTypeException("Try to check " + getId() + " value from the non '" + getId() + "' data '" + name + "' on '" + parentData + "'");
076        }
077        
078        if (parentData.isMultiple(name))
079        {
080            RepositoryData multipleParentData = parentData.getRepositoryData(name);
081            
082            return !multipleParentData.getDataNames().isEmpty();
083        }
084        else
085        {
086            RepositoryData data = parentData.getRepositoryData(name);
087            return !isSingleValueEmpty(data);
088        }
089    }
090    
091    /**
092     * Checks if the single value is empty
093     * @param singleValueData repository data containing the value to check
094     * @return <code>true</code> if the value is empty, <code>false</code> otherwise
095     */
096    public boolean isSingleValueEmpty(RepositoryData singleValueData);
097    
098    /**
099     * Read the single value in the given repository data
100     * @param singleValueData repository data containing the value
101     * @return the value. Can return null if the given data does not contain the necessary elements
102     */
103    public T readSingleValue(RepositoryData singleValueData);
104    
105    @SuppressWarnings("unchecked")
106    public default void write(ModifiableRepositoryData parentData, String name, Object value) throws BadItemTypeException
107    {
108        if (value == null)
109        {
110            if (parentData.hasValue(name) && parentData.isMultiple(name))
111            {
112                parentData.removeValue(name);
113                ModifiableRepositoryData multipleParentData = parentData.addRepositoryData(name, RepositoryConstants.MULTIPLE_ITEM_NODETYPE);
114                // Add the type identifier in a property to know what is the type of the data even if there is no value
115                multipleParentData.setValue(TYPE_ID_DATA_NAME, getId(), RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
116            }
117            else
118            {
119                emptySingleValue(parentData, name);
120            }
121        }
122        else if (getManagedClass().isInstance(value))
123        {
124            if (parentData.hasValue(name) && removeValueBeforeWritingIt())
125            {
126                parentData.removeValue(name);
127            }
128            
129            writeSingleValue(parentData, name, (T) value);
130        }
131        else if (getManagedClassArray().isInstance(value))
132        {
133            ModifiableRepositoryData multipleParentData;
134            
135            if (parentData.hasValue(name) && !removeValueBeforeWritingIt())
136            {
137                multipleParentData = parentData.getRepositoryData(name);
138            }
139            else
140            {
141                if (parentData.hasValue(name))
142                {
143                    parentData.removeValue(name);
144                }
145                
146                multipleParentData = parentData.addRepositoryData(name, RepositoryConstants.MULTIPLE_ITEM_NODETYPE);
147                // Add the type identifier in a property to know what the type of the data even if there is no value
148                multipleParentData.setValue(TYPE_ID_DATA_NAME, getId(), RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
149            }
150            
151            // Loop on all given values or all entries already present in the repository data
152            for (int i = 0; i < Math.max(multipleParentData.getDataNames().size(), ((T[]) value).length); i++)
153            {
154                String entryName = String.valueOf(i + 1);
155                
156                if (i < ((T[]) value).length)
157                {
158                    T singleValue = ((T[]) value)[i];
159                    if (singleValue == null)
160                    {
161                        throw new IllegalArgumentException("Try to set a null value into the multiple " + getId() + " data '" + name + "' on '" + parentData + "'");
162                    }
163                    else
164                    {
165                        writeSingleValue(multipleParentData, entryName, singleValue);
166                    }
167                }
168                else
169                {
170                    multipleParentData.removeValue(entryName);
171                }
172            }
173        }
174        else
175        {
176            StringBuilder message = new StringBuilder().append("Try to set the non ").append(getId()).append(" value '").append(value);
177            message.append("' to the ").append(getId()).append(" data '").append(name).append("' on '").append(parentData).append("'");
178            throw new BadItemTypeException(message.toString());
179        }
180    }
181    
182    /**
183     * Determines if the existing value has to be removed before writing the new one
184     * @return <code>true</code> if the previous value has to be removed, <code>false</code> otherwise
185     */
186    public default boolean removeValueBeforeWritingIt()
187    {
188        return true;
189    }
190    
191    /**
192     * Empties the single value into the given repositoryData
193     * This method is called by the {@link #write(ModifiableRepositoryData, String, Object)} method, when the given value is <code>null</code>
194     * @param parentData repository where to empty the single value.
195     * @param name the name of the element to empty
196     */
197    public void emptySingleValue(ModifiableRepositoryData parentData, String name);
198    
199    /**
200     * Write the single value into the given repository data
201     * This method is called by the {@link #write(ModifiableRepositoryData, String, Object)} method, once for each value if the value is an array
202     * @param parentData repository where to store the single value.
203     * @param name the name of the element to write
204     * @param value the single value to write. Can be null. In this case, an empty data must be created
205     */
206    public void writeSingleValue(ModifiableRepositoryData parentData, String name, T value);
207    
208    public default boolean isCompatible(RepositoryData parentData, String name) throws UnknownDataException
209    {
210        if (parentData.hasValue(name + EMPTY_METADATA_SUFFIX, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL))
211        {
212            return true;
213        }
214        
215        if (getRepositoryDataType().equals(parentData.getType(name)))
216        {
217            return true;
218        }
219
220        if (RepositoryConstants.MULTIPLE_ITEM_NODETYPE.equals(parentData.getType(name)))
221        {
222            RepositoryData multipleItemNode = parentData.getRepositoryData(name);
223            return getId().equals(multipleItemNode.getString(TYPE_ID_DATA_NAME, RepositoryConstants.NAMESPACE_PREFIX_INTERNAL));
224        }
225            
226        return false;
227    }
228}