001/*
002 *  Copyright 2020 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.extractor.xml;
017
018import java.lang.reflect.Array;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026
027import org.apache.commons.lang3.StringUtils;
028import org.apache.commons.lang3.tuple.Pair;
029import org.apache.xpath.XPathAPI;
030import org.w3c.dom.Element;
031
032import org.ametys.core.util.dom.DOMUtils;
033import org.ametys.plugins.repository.data.extractor.ModelAwareValuesExtractor;
034import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
035import org.ametys.plugins.repository.model.CompositeDefinition;
036import org.ametys.plugins.repository.model.RepeaterDefinition;
037import org.ametys.runtime.model.ElementDefinition;
038import org.ametys.runtime.model.Model;
039import org.ametys.runtime.model.ModelHelper;
040import org.ametys.runtime.model.ModelItem;
041import org.ametys.runtime.model.ModelItemContainer;
042import org.ametys.runtime.model.ModelItemGroup;
043import org.ametys.runtime.model.ModelViewItem;
044import org.ametys.runtime.model.ModelViewItemGroup;
045import org.ametys.runtime.model.View;
046import org.ametys.runtime.model.ViewElement;
047import org.ametys.runtime.model.ViewItem;
048import org.ametys.runtime.model.ViewItemContainer;
049import org.ametys.runtime.model.exception.BadDataPathCardinalityException;
050import org.ametys.runtime.model.exception.BadItemTypeException;
051import org.ametys.runtime.model.exception.UndefinedItemPathException;
052import org.ametys.runtime.model.type.ElementType;
053
054/**
055 * This class provides methods to extract values from an XML document, using a model
056 */
057public class ModelAwareXMLValuesExtractor implements ModelAwareValuesExtractor
058{
059    /** The DOM element containing the XML values */
060    protected Element _element;
061    
062    /** The getter that retrieves needed additional data by types */
063    protected XMLValuesExtractorAdditionalDataGetter _additionalDataGetter;
064    
065    /** The model of the extracted values */
066    protected Collection<? extends ModelItemContainer> _modelItemContainers;
067
068    /**
069     * Creates a model aware XML values extractor
070     * @param element the DOM element containing the XML values
071     * @param additionalDataGetter the getter that retrieves needed additional data by types
072     * @param models the model of the extracted values
073     */
074    public ModelAwareXMLValuesExtractor(Element element, XMLValuesExtractorAdditionalDataGetter additionalDataGetter, Model... models)
075    {
076        this(element, additionalDataGetter, Arrays.asList(models));
077    }
078
079    /**
080     * Creates a model aware XML values extractor
081     * @param element the DOM element containing the XML values
082     * @param additionalDataGetter the getter that retrieves needed additional data by types
083     * @param modelItemContainers the model of the extracted values
084     */
085    public ModelAwareXMLValuesExtractor(Element element, XMLValuesExtractorAdditionalDataGetter additionalDataGetter, Collection<? extends ModelItemContainer> modelItemContainers)
086    {
087        _element = element;
088        _additionalDataGetter = additionalDataGetter;
089        _modelItemContainers = modelItemContainers;
090    }
091    
092    public Map<String, Object> extractValues() throws Exception
093    {
094        View view = new View();
095        _fillViewItemContainerFromXML(_element, view, _modelItemContainers);
096        return extractValues(view);
097    }
098    
099    /**
100     * Fill the given view item container with the data found in the given element.
101     * @param element The DOM element containing the values
102     * @param viewItemContainer The view item container to fill
103     * @param modelItemContainer The model item containing the items that could be in the element
104     * @throws Exception if an error occurs
105     */
106    protected void _fillViewItemContainerFromXML(Element element, ViewItemContainer viewItemContainer, ModelItemContainer modelItemContainer) throws Exception
107    {
108        _fillViewItemContainerFromXML(element, viewItemContainer, List.of(modelItemContainer));
109    }
110    
111    /**
112     * Fill the given view item container with the data found in the given element.
113     * @param element The DOM element containing the values
114     * @param viewItemContainer The view item container to fill
115     * @param modelItemContainers The model items containing the items that could be in the element
116     * @throws Exception if an error occurs
117     */
118    @SuppressWarnings("unchecked")
119    protected void _fillViewItemContainerFromXML(Element element, ViewItemContainer viewItemContainer, Collection<? extends ModelItemContainer> modelItemContainers) throws Exception
120    {
121        List<Element> children = DOMUtils.getUniqueChildElements(element);
122        for (Element child : children)
123        {
124            String dataName = child.getNodeName();
125            if (ModelHelper.hasModelItem(dataName, modelItemContainers))
126            {
127                ModelItem modelItem = ModelHelper.getModelItem(dataName, modelItemContainers);
128                ModelViewItem modelViewItem;
129                if (modelItem instanceof ModelItemGroup)
130                {
131                    Element groupNode = child;
132                    if (modelItem instanceof RepeaterDefinition)
133                    {
134                        // Use the first entry to create the view - assume that all entries will have the same data
135                        groupNode = DOMUtils.getChildElementByTagName(child, "entry");
136                    }
137                    
138                    modelViewItem = new ModelViewItemGroup();
139                    _fillViewItemContainerFromXML(groupNode, (ModelViewItemGroup) modelViewItem, (ModelItemGroup) modelItem);
140                }
141                else
142                {
143                    modelViewItem = new ViewElement();
144                }
145                
146                modelViewItem.setDefinition(modelItem);
147                viewItemContainer.addViewItem(modelViewItem);
148            }
149        }
150        
151    }
152    
153    public Map<String, Object> extractValues(View view) throws Exception
154    {
155        return _extractValues(_element, view, "");
156    }
157    
158    /**
159     * Extracts the values of all items in the given view item container
160     * @param currentElement the DOM element containing the values of the items in the container
161     * @param viewItemContainer the view item container
162     * @param prefix the path of the item represented by the view item container (prefix of all contained items)
163     * @return the values of all items in the given view item containers
164     * @throws Exception if an error occurs
165     */
166    protected Map<String, Object> _extractValues(Element currentElement, ViewItemContainer viewItemContainer, String prefix) throws Exception
167    {
168        Map<String, Object> values = new HashMap<>();
169        for (ViewItem viewItem : viewItemContainer.getViewItems())
170        {
171            if (viewItem instanceof ModelViewItem)
172            {
173                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
174                String dataName = modelItem.getName();
175                if (DOMUtils.hasChildElement(currentElement, dataName))
176                {
177                    Object value;
178                    if (viewItem instanceof ModelViewItemGroup)
179                    {
180                        value = _extractGroupValues(currentElement, (ModelViewItemGroup) viewItem, dataName, prefix);
181                    }
182                    else
183                    {
184                        value = _extractElementValue(currentElement, (ElementDefinition) modelItem, prefix);
185                    }
186                        
187                    values.put(dataName, value);
188                }
189            }
190            else if (viewItem instanceof ViewItemContainer)
191            {
192                values.putAll(_extractValues(currentElement, (ViewItemContainer) viewItem, prefix));
193            }
194        }
195        
196        return values;
197    }
198
199    public <T> T extractValue(String dataPath) throws Exception
200    {
201        // Check that there is an item at the given path
202        if (!ModelHelper.hasModelItem(dataPath, _modelItemContainers))
203        {
204            throw new UndefinedItemPathException("Unable to retrieve the value at path '" + dataPath + "'. There is no such item defined by the model.");
205        }
206        
207        return _extractValue(_modelItemContainers, _element, dataPath, "");
208    }
209    
210    /**
211     * Extracts the value at the given path
212     * @param <T> type of the value to retrieve
213     * @param modelItemContainer The model item containing the item of the value to extract
214     * @param currentElement the DOM element containing the model item container's values 
215     * @param relativeDataPath The data path relative to the model item container
216     * @param prefix the path of the item represented by the model item container (prefix of all contained items)
217     * @return the value
218     * @throws Exception if an error occurs
219     */
220    protected <T> T _extractValue(ModelItemContainer modelItemContainer, Element currentElement, String relativeDataPath, String prefix) throws Exception
221    {
222        return _extractValue(List.of(modelItemContainer), currentElement, relativeDataPath, prefix);
223    }
224    
225    /**
226     * Extracts the value at the given path
227     * @param <T> type of the value to retrieve
228     * @param modelItemContainers The model items containing the item of the value to extract
229     * @param currentElement the DOM element containing the model item containers' values 
230     * @param relativeDataPath The data path relative to the model item containers
231     * @param prefix the path of the item represented by the model item containers (prefix of all contained items)
232     * @return the value
233     * @throws Exception if an error occurs
234     */
235    protected <T> T _extractValue(Collection<? extends ModelItemContainer> modelItemContainers, Element currentElement, String relativeDataPath, String prefix) throws Exception
236    {
237        String[] pathSegments = StringUtils.split(relativeDataPath, ModelItem.ITEM_PATH_SEPARATOR);
238
239        if (pathSegments == null || pathSegments.length < 1)
240        {
241            throw new IllegalArgumentException("Unable to extract the value of the data at the given path. This path is empty.");
242        }
243        else if (pathSegments.length == 1)
244        {
245            String dataName = relativeDataPath;
246            ModelItem modelItem = ModelHelper.getModelItem(dataName, modelItemContainers);
247            if (modelItem instanceof ElementDefinition)
248            {
249                return _extractElementValue(currentElement, (ElementDefinition) modelItem, prefix);
250            }
251            else
252            {
253                ModelViewItemGroup modelViewItemGroup = ModelViewItemGroup.of((ModelItemGroup) modelItem);
254                return _extractGroupValues(currentElement, modelViewItemGroup, dataName, prefix);
255            }
256        }
257        else
258        {
259            String firstSegmentDataName = pathSegments[0];
260            String newRelativeDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
261            String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + firstSegmentDataName : firstSegmentDataName;
262
263            ModelItem modelItem = ModelHelper.getModelItem(firstSegmentDataName, modelItemContainers);
264            
265            if (modelItem instanceof RepeaterDefinition)
266            {
267                if (DataHolderHelper.isRepeaterEntryPath(firstSegmentDataName))
268                {
269                    Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(firstSegmentDataName);
270                    Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(currentElement, repeaterNameAndEntryPosition.getLeft() + "/entry[@name='" + repeaterNameAndEntryPosition.getRight() + "']");
271                    if (repeaterEntryElement != null)
272                    {
273                        return _extractValue((RepeaterDefinition) modelItem, repeaterEntryElement, newRelativeDataPath, newPrefix);
274                    }
275                    else
276                    {
277                        return null;
278                    }
279                }
280                else
281                {
282                    throw new BadDataPathCardinalityException("Unable to extract the value at path '" + relativeDataPath + "'. The segment '" + pathSegments[0] + "' refers to a repeater but not an entry.");
283                }
284            }
285            else if (modelItem instanceof CompositeDefinition)
286            {
287                Element compositeElement = DOMUtils.getChildElementByTagName(currentElement, firstSegmentDataName);
288                return compositeElement != null ? _extractValue((CompositeDefinition) modelItem, compositeElement, newRelativeDataPath, newPrefix) : null;
289            }
290            else
291            {
292                throw new BadItemTypeException("Unable to extract the value at path '" + relativeDataPath + "'. The segment '" + pathSegments[0] + "' does not represent a group item.");
293            }
294        }
295    }
296    
297    /**
298     * Extracts the value of the given element
299     * @param <T> type of the value to retrieve
300     * @param parent the DOM element of the element definition's parent
301     * @param definition the element's definition
302     * @param prefix the path of the element's parent
303     * @return the value
304     * @throws Exception if an error occurs
305     */
306    @SuppressWarnings("unchecked")
307    protected <T> T _extractElementValue(Element parent, ElementDefinition definition, String prefix) throws Exception
308    {
309        ElementType type = definition.getType();
310        String dataName = definition.getName();
311       
312        String absoluteDataPath = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName;
313        Optional<Object> additionalData = _additionalDataGetter.getAdditionalData(absoluteDataPath, type);
314        
315        Object value = type.valueFromXML(parent, dataName, additionalData); 
316        
317        if (definition.isMultiple() && type.getManagedClass().isInstance(value))
318        {
319            // The value is single but should be an array. Create the array with the single value
320            T arrayValue = (T) Array.newInstance(type.getManagedClass(), 1);
321            Array.set(arrayValue, 0, value);
322            return arrayValue;
323        }
324        else if (!definition.isMultiple() && type.getManagedClassArray().isInstance(value))
325        {
326            // The value is multiple but should be single. Retrieve the first value of the array
327            return Array.getLength(value) > 0 ? (T) Array.get(value, 0) : null;
328        }
329        else
330        {
331            return (T) value;
332        }
333    }
334    
335    /**
336     * Extracts the values of the given group
337     * @param <T> type of the values to retrieve (example: {@link Map} for a composite, {@link List} for a repeater
338     * @param parent the DOM element of the group's parent
339     * @param modelViewItemGroup view item corresponding to the group
340     * @param dataName the name of the data to extract
341     * @param prefix the path of the group's parent
342     * @return the value
343     * @throws Exception if an error occurs
344     */
345    @SuppressWarnings("unchecked")
346    protected <T> T _extractGroupValues(Element parent, ModelViewItemGroup modelViewItemGroup, String dataName, String prefix) throws Exception
347    {
348        ModelItemGroup modelItemGroup = modelViewItemGroup.getDefinition();
349        String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName;
350
351        if (modelItemGroup instanceof RepeaterDefinition)
352        {
353            if (DataHolderHelper.isRepeaterEntryPath(dataName))
354            {
355                Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataName);
356                Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(parent, repeaterNameAndEntryPosition.getLeft() + "/entry[@name='" + repeaterNameAndEntryPosition.getRight() + "']");
357                return repeaterEntryElement != null ? (T) _extractValues(repeaterEntryElement, modelViewItemGroup, newPrefix) : null;
358            }
359            else
360            {
361                Element repeaterElement = DOMUtils.getChildElementByTagName(parent, dataName);
362                if (repeaterElement != null)
363                {
364                    List<Map<String, Object>> repeaterValues = new ArrayList<>();
365                    int repeaterSize = Integer.valueOf(XPathAPI.eval(repeaterElement, "count(entry)").str());
366                    for (int i = 1; i <= repeaterSize; i++)
367                    {
368                        Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(repeaterElement, "entry[@name='" + i + "']");
369                        Map<String, Object> repeaterEntryValues = _extractValues(repeaterEntryElement, modelViewItemGroup, newPrefix + "[" + i + "]");
370                        repeaterValues.add(repeaterEntryValues);
371                    }
372                    return (T) repeaterValues;
373                }
374                else
375                {
376                    return null;
377                }
378            }
379        }
380        else
381        {
382            Element compositeElement = DOMUtils.getChildElementByTagName(parent, dataName);
383            return compositeElement != null ? (T) _extractValues(compositeElement, modelViewItemGroup, newPrefix) : null;
384        }
385    }
386}