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.Locale;
020
021import org.apache.commons.lang3.StringUtils;
022import org.apache.commons.lang3.tuple.ImmutablePair;
023import org.apache.commons.lang3.tuple.Pair;
024import org.xml.sax.ContentHandler;
025import org.xml.sax.SAXException;
026
027import org.ametys.plugins.repository.data.holder.DataHolder;
028import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
029import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
030import org.ametys.plugins.repository.data.holder.ModifiableDataHolder;
031import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
032import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
033import org.ametys.plugins.repository.data.holder.group.Composite;
034import org.ametys.plugins.repository.data.holder.group.ModifiableComposite;
035import org.ametys.plugins.repository.data.holder.group.ModifiableRepeater;
036import org.ametys.plugins.repository.data.holder.group.Repeater;
037import org.ametys.plugins.repository.data.holder.group.RepeaterEntry;
038import org.ametys.plugins.repository.metadata.MultilingualString;
039import org.ametys.runtime.model.ModelItem;
040import org.ametys.runtime.model.ModelItemContainer;
041import org.ametys.runtime.model.ModelItemGroup;
042import org.ametys.runtime.model.exception.BadItemTypeException;
043import org.ametys.runtime.model.exception.UndefinedItemPathException;
044import org.ametys.runtime.model.type.ModelItemType;
045
046/**
047 * Helper for implementations of data holder
048 */
049public final class DataHolderHelper
050{
051    private DataHolderHelper()
052    {
053        // Empty constructor
054    }
055    
056    /**
057     * Checks if there is a model item at the given relative path
058     * @param path path of the model item. This path is relative to the given containers. No matter if it is a definition or data path (with repeater entry positions)
059     * @param itemContainers model item containers where to search if there is a model item
060     * @return <code>true</code> if there is model item at this path, <code>false</code> otherwise
061     */
062    public static boolean hasModelItem(String path, Collection<? extends ModelItemContainer> itemContainers)
063    {
064        try
065        {
066            getModelItem(path, itemContainers);
067            return true;
068        }
069        catch (UndefinedItemPathException e)
070        {
071            return false;
072        }
073    }
074    
075    /**
076     * Retrieves the model item at the given relative path
077     * @param path path of the model item to retrieve. This path is relative to the given containers. No matter if it is a definition or data path (with repeater entry positions)
078     * @param itemContainers model item containers where to search the model item
079     * @return the model item
080     * @throws IllegalArgumentException if the given path is null or empty
081     * @throws UndefinedItemPathException if there is no item defined at the given path in given item containers
082     */
083    public static ModelItem getModelItem(String path, Collection<? extends ModelItemContainer> itemContainers) throws IllegalArgumentException, UndefinedItemPathException
084    {
085        if (StringUtils.isEmpty(path))
086        {
087            throw new IllegalArgumentException("Unable to retrieve the model item at the given path. This path is empty.");
088        }
089        
090        String definitionPath = getDefinitionPathFromDataPath(path);
091        
092        ModelItem item = null;
093        for (ModelItemContainer container : itemContainers)
094        {
095            if (container.hasModelItem(definitionPath))
096            {
097                item = container.getModelItem(definitionPath);
098                break;
099            }
100        }
101        
102        if (item != null)
103        {
104            return item;
105        }
106        else
107        {
108            String absolutePath = path;
109            if (itemContainers.size() == 1)
110            {
111                ModelItemContainer container = itemContainers.iterator().next();
112                if (container instanceof ModelItemGroup)
113                {
114                    absolutePath = ((ModelItemGroup) container).getPath() + ModelItem.ITEM_PATH_SEPARATOR + path;
115                }
116            }
117            
118            throw new UndefinedItemPathException("Unable to retrieve the model item at path '" + absolutePath + "'. This path is not defined by the model.");
119        }
120    }
121    
122    /**
123     * Retrieves the given dataPath as a definition path (without the repeaterEntry positions)
124     * @param dataPath the data path
125     * @return the definition path
126     */
127    public static String getDefinitionPathFromDataPath(String dataPath)
128    {
129        return dataPath.replaceAll("\\[[0-9]+\\]", "");
130    }
131    
132    /**
133     * Retrieves the repeater entry represented by the given path segment
134     * @param dataHolder data holder that contains the repeater entry. The data holder must be the direct parent of the repeater
135     * @param repeaterName the name of the repeater
136     * @param entryPosition the position of the entry
137     * @return the repeater entry
138     * @throws BadItemTypeException if the value stored in the repository for the given repeater name is not a repeater
139     */
140    public static RepeaterEntry getRepeaterEntry(DataHolder dataHolder, String repeaterName, int entryPosition) throws BadItemTypeException
141    {
142        Repeater repeater = dataHolder.getRepeater(repeaterName);
143        if (repeater.hasEntry(entryPosition))
144        {
145            return repeater.getEntry(entryPosition);
146        }
147        else
148        {
149            return null;
150        }
151    }
152    
153    /**
154     * Test if the path is a repeater entry path (for example entries[1])
155     * @param path the path representing the repeater entry
156     * @return true if the pathSegment is a repeater entry path
157     */
158    public static boolean isRepeaterEntryPath(String path)
159    {
160        return path.matches(".*(\\[[0-9]+\\])$");
161    }
162    
163    /**
164     * Retrieves the pair of repeater name and entry position of the given path segment
165     * Return <code>null</code> if the given path does not represent a repeater entry
166     * @param pathSegment the path segment representing the repeater entry
167     * @return the pair of repeater name and entry position
168     */
169    public static Pair<String, Integer> getRepeaterNameAndEntryPosition(String pathSegment)
170    {
171        if (isRepeaterEntryPath(pathSegment))
172        {
173            String repeaterName = pathSegment.substring(0, pathSegment.indexOf("["));
174            String entryPositionAsString = pathSegment.substring(pathSegment.indexOf("[") + 1, pathSegment.length() - 1);
175            return new ImmutablePair<>(repeaterName, Integer.parseInt(entryPositionAsString));
176        }
177        else
178        {
179            return null;
180        }
181    }
182    
183    /**
184     * Copies the source {@link DataHolder} to the given {@link ModifiableDataHolder} destination.
185     * @param source the source {@link DataHolder}
186     * @param destination the {@link ModifiableDataHolder} destination 
187     */
188    public static void copyDataHolder(DataHolder source, ModifiableDataHolder destination)
189    {
190        for (String name : source.getDataNames())
191        {
192            Object value = source instanceof ModelAwareDataHolder ? ((ModelAwareDataHolder) source).getValue(name) : ((ModelLessDataHolder) source).getValue(name);
193            
194            if (value instanceof Composite)
195            {
196                ModifiableComposite compositeDestination = destination.getComposite(name, true);
197                ((Composite) value).copyTo(compositeDestination);
198            }
199            else if (value instanceof Repeater)
200            {
201                ModifiableRepeater repeaterDestination = destination.getRepeater(name, true);
202                ((Repeater) value).copyTo(repeaterDestination);
203            }
204            else if (destination instanceof ModifiableModelAwareDataHolder)
205            {
206                ((ModifiableModelAwareDataHolder) destination).setValue(name, value);
207            }
208            else
209            {
210                ((ModifiableModelLessDataHolder) destination).setValue(name, value);
211            }
212        }
213    }
214    
215    /**
216     * Generates SAX events for the given {@link DataHolder}
217     * @param dataHolder the {@link DataHolder} to SAX
218     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
219     * @param locale The locale to use for localized data, such as {@link MultilingualString}. Can be <code>null</code> to generate SAX events for all existing {@link Locale}s.
220     * @throws SAXException if an error occurs during the SAX events generation
221     */
222    public static void dataHolderToSAX(DataHolder dataHolder, ContentHandler contentHandler, Locale locale) throws SAXException
223    {
224        for (String name : dataHolder.getDataNames())
225        {
226            ModelItemType type = dataHolder instanceof ModelAwareDataHolder ? ((ModelAwareDataHolder) dataHolder).getType(name) : ((ModelLessDataHolder) dataHolder).getType(name);
227            Object value = dataHolder instanceof ModelAwareDataHolder ? ((ModelAwareDataHolder) dataHolder).getValue(name) : ((ModelLessDataHolder) dataHolder).getValueOfType(name, type.getId());
228            type.valueToSAX(contentHandler, name, value, locale);
229        }
230    }
231    
232    /**
233     * Generates SAX events for the data at the given data path in the given {@link DataHolder}
234     * Do not generate any event if there is no values at the given path
235     * @param dataHolder the {@link DataHolder} containing the value to SAX
236     * @param contentHandler the {@link ContentHandler} that will receive the SAX events
237     * @param dataPath the path of the data to SAX
238     * @param tagName the tag name of the SAX event to generate. If <code>null</code> the name of the data is used
239     * @param locale The locale to use for localized data, such as {@link MultilingualString}. Can be <code>null</code> to generate SAX events for all existing {@link Locale}s.
240     * @throws SAXException if an error occurs during the SAX events generation
241     */
242    public static void dataHolderValueToSAX(DataHolder dataHolder, ContentHandler contentHandler, String dataPath, String tagName, Locale locale) throws SAXException
243    {
244        // Get the value to SAX
245        if (dataHolder.hasValue(dataPath))
246        {
247            ModelItemType type = dataHolder instanceof ModelAwareDataHolder ? ((ModelAwareDataHolder) dataHolder).getType(dataPath) : ((ModelLessDataHolder) dataHolder).getType(dataPath);
248            Object value = dataHolder instanceof ModelAwareDataHolder ? ((ModelAwareDataHolder) dataHolder).getValue(dataPath) : ((ModelLessDataHolder) dataHolder).getValueOfType(dataPath, type.getId());
249            
250            // Get the tag where to SAX the value
251            String finalTagName = tagName;
252            if (StringUtils.isEmpty(finalTagName))
253            {
254                // If no specific tag name is given, use the data name
255                if (dataPath.contains(ModelItem.ITEM_PATH_SEPARATOR))
256                {
257                    finalTagName = dataPath.substring(dataPath.lastIndexOf(ModelItem.ITEM_PATH_SEPARATOR));
258                }
259                else
260                {
261                    finalTagName = dataPath;
262                }
263            }
264            
265            // Generates SAX events
266            type.valueToSAX(contentHandler, finalTagName, value, locale);
267        }
268    }
269}