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            Optional<ModelItem> optionalModelItem = _getModelItemFromNodeName(element, child.getNodeName(), modelItemContainers);
125            if (optionalModelItem.isPresent())
126            {
127                ModelItem modelItem = optionalModelItem.get();
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(groupNode, "entry");
136                    }
137                    
138                    modelViewItem = new ModelViewItemGroup();
139                    
140                    if (groupNode != null)
141                    {
142                        _fillViewItemContainerFromXML(groupNode, (ModelViewItemGroup) modelViewItem, (ModelItemGroup) modelItem);
143                    }
144                }
145                else
146                {
147                    modelViewItem = new ViewElement();
148                }
149
150                modelViewItem.setDefinition(modelItem);
151                if (!viewItemContainer.hasModelViewItem(modelViewItem))
152                {
153                    viewItemContainer.addViewItem(modelViewItem);
154                }
155            }
156        }
157    }
158    
159    /**
160     * Retrieves the model item corresponding to the given node name
161     * @param parent the DOM parent element
162     * @param nodeName the node name
163     * @param modelItemContainers the model item containers where to search for the model item
164     * @return the model item corresponding to the given node name
165     */
166    protected Optional<ModelItem> _getModelItemFromNodeName(Element parent, String nodeName, Collection<? extends ModelItemContainer> modelItemContainers)
167    {
168        if (ModelHelper.hasModelItem(nodeName, modelItemContainers))
169        {
170            return Optional.of(ModelHelper.getModelItem(nodeName, modelItemContainers));
171        }
172        else
173        {
174            return Optional.empty();
175        }
176    }
177    
178    public Map<String, Object> extractValues(View view) throws Exception
179    {
180        return _extractValues(_element, view, "");
181    }
182    
183    /**
184     * Extracts the values of all items in the given view item container
185     * @param currentElement the DOM element containing the values of the items in the container
186     * @param viewItemContainer the view item container
187     * @param prefix the path of the item represented by the view item container (prefix of all contained items)
188     * @return the values of all items in the given view item containers
189     * @throws Exception if an error occurs
190     */
191    protected Map<String, Object> _extractValues(Element currentElement, ViewItemContainer viewItemContainer, String prefix) throws Exception
192    {
193        Map<String, Object> values = new HashMap<>();
194        for (ViewItem viewItem : viewItemContainer.getViewItems())
195        {
196            if (viewItem instanceof ModelViewItem)
197            {
198                ModelItem modelItem = ((ModelViewItem) viewItem).getDefinition();
199                String dataName = modelItem.getName();
200                if (_hasChildForAttribute(currentElement, dataName))
201                {
202                    Object value;
203                    if (viewItem instanceof ModelViewItemGroup)
204                    {
205                        value = _extractGroupValues(currentElement, (ModelViewItemGroup) viewItem, dataName, prefix);
206                    }
207                    else
208                    {
209                        value = _extractElementValue(currentElement, (ElementDefinition) modelItem, prefix);
210                    }
211                        
212                    values.put(dataName, value);
213                }
214            }
215            else if (viewItem instanceof ViewItemContainer)
216            {
217                values.putAll(_extractValues(currentElement, (ViewItemContainer) viewItem, prefix));
218            }
219        }
220        
221        return values;
222    }
223    
224    /**
225     * Checks if the given element contains a child for the given attribute name
226     * @param element the element to check
227     * @param attributeName the name of the attribute to search
228     * @return <code>true</code> if the element contains a child corresponding to the given attribute name, <code>false</code> otherwise
229     */
230    protected boolean _hasChildForAttribute(Element element, String attributeName)
231    {
232        return DOMUtils.hasChildElement(element, attributeName);
233    }
234
235    public <T> T extractValue(String dataPath) throws Exception
236    {
237        // Check that there is an item at the given path
238        if (!ModelHelper.hasModelItem(dataPath, _modelItemContainers))
239        {
240            throw new UndefinedItemPathException("Unable to retrieve the value at path '" + dataPath + "'. There is no such item defined by the model.");
241        }
242        
243        return _extractValue(_modelItemContainers, _element, dataPath, StringUtils.EMPTY);
244    }
245    
246    /**
247     * Extracts the value at the given path
248     * @param <T> type of the value to retrieve
249     * @param modelItemContainer The model item containing the item of the value to extract
250     * @param currentElement the DOM element containing the model item container's values 
251     * @param relativeDataPath The data path relative to the model item container
252     * @param prefix the path of the item represented by the model item container (prefix of all contained items)
253     * @return the value
254     * @throws Exception if an error occurs
255     */
256    protected <T> T _extractValue(ModelItemContainer modelItemContainer, Element currentElement, String relativeDataPath, String prefix) throws Exception
257    {
258        return _extractValue(List.of(modelItemContainer), currentElement, relativeDataPath, prefix);
259    }
260    
261    /**
262     * Extracts the value at the given path
263     * @param <T> type of the value to retrieve
264     * @param modelItemContainers The model items containing the item of the value to extract
265     * @param currentElement the DOM element containing the model item containers' values 
266     * @param relativeDataPath The data path relative to the model item containers
267     * @param prefix the path of the item represented by the model item containers (prefix of all contained items)
268     * @return the value
269     * @throws Exception if an error occurs
270     */
271    protected <T> T _extractValue(Collection<? extends ModelItemContainer> modelItemContainers, Element currentElement, String relativeDataPath, String prefix) throws Exception
272    {
273        String[] pathSegments = StringUtils.split(relativeDataPath, ModelItem.ITEM_PATH_SEPARATOR);
274
275        if (pathSegments == null || pathSegments.length < 1)
276        {
277            throw new IllegalArgumentException("Unable to extract the value of the data at the given path. This path is empty.");
278        }
279        else if (pathSegments.length == 1)
280        {
281            String dataName = relativeDataPath;
282            ModelItem modelItem = ModelHelper.getModelItem(dataName, modelItemContainers);
283            if (modelItem instanceof ElementDefinition)
284            {
285                return _extractElementValue(currentElement, (ElementDefinition) modelItem, prefix);
286            }
287            else
288            {
289                ModelViewItemGroup modelViewItemGroup = ModelViewItemGroup.of((ModelItemGroup) modelItem);
290                return _extractGroupValues(currentElement, modelViewItemGroup, dataName, prefix);
291            }
292        }
293        else
294        {
295            String firstSegmentDataName = pathSegments[0];
296            String newRelativeDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
297            String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + firstSegmentDataName : firstSegmentDataName;
298
299            ModelItem modelItem = ModelHelper.getModelItem(firstSegmentDataName, modelItemContainers);
300            
301            if (modelItem instanceof RepeaterDefinition)
302            {
303                if (DataHolderHelper.isRepeaterEntryPath(firstSegmentDataName))
304                {
305                    Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(firstSegmentDataName);
306                    Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(currentElement, repeaterNameAndEntryPosition.getLeft() + "/entry[@name='" + repeaterNameAndEntryPosition.getRight() + "']");
307                    if (repeaterEntryElement != null)
308                    {
309                        return _extractValue((RepeaterDefinition) modelItem, repeaterEntryElement, newRelativeDataPath, newPrefix);
310                    }
311                    else
312                    {
313                        return null;
314                    }
315                }
316                else
317                {
318                    throw new BadDataPathCardinalityException("Unable to extract the value at path '" + relativeDataPath + "'. The segment '" + pathSegments[0] + "' refers to a repeater but not an entry.");
319                }
320            }
321            else if (modelItem instanceof CompositeDefinition)
322            {
323                Element compositeElement = DOMUtils.getChildElementByTagName(currentElement, firstSegmentDataName);
324                return compositeElement != null ? _extractValue((CompositeDefinition) modelItem, compositeElement, newRelativeDataPath, newPrefix) : null;
325            }
326            else
327            {
328                throw new BadItemTypeException("Unable to extract the value at path '" + relativeDataPath + "'. The segment '" + pathSegments[0] + "' does not represent a group item.");
329            }
330        }
331    }
332    
333    /**
334     * Extracts the value of the given element
335     * @param <T> type of the value to retrieve
336     * @param parent the DOM element of the element definition's parent
337     * @param definition the element's definition
338     * @param prefix the path of the element's parent
339     * @return the value
340     * @throws Exception if an error occurs
341     */
342    @SuppressWarnings("unchecked")
343    protected <T> T _extractElementValue(Element parent, ElementDefinition definition, String prefix) throws Exception
344    {
345        ElementType type = definition.getType();
346        String dataName = definition.getName();
347       
348        String absoluteDataPath = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName;
349        Optional<Object> additionalData = _additionalDataGetter.getAdditionalData(absoluteDataPath, type);
350        
351        Object value = _extractElementValue(parent, definition, additionalData); 
352        
353        if (definition.isMultiple() && value != null && !value.getClass().isArray())
354        {
355            // The value is single but should be an array. Create the array with the single value
356            return (T) new Object[] {value};
357        }
358        else if (!definition.isMultiple() && value != null && value.getClass().isArray())
359        {
360            // The value is multiple but should be single. Retrieve the first value of the array
361            return Array.getLength(value) > 0 ? (T) Array.get(value, 0) : null;
362        }
363        else
364        {
365            return (T) value;
366        }
367    }
368    
369    /**
370     * Extracts the value of the given element
371     * @param <T> type of the element definition
372     * @param parent the DOM element of the element definition's parent
373     * @param definition the element's definition
374     * @param additionalData the additional data needed to extract the value
375     * @return the value
376     * @throws Exception if an error occurs
377     */
378    protected <T> Object _extractElementValue(Element parent, ElementDefinition<T> definition, Optional<Object> additionalData) throws Exception
379    {
380        ElementType<T> type = definition.getType();
381        String dataName = definition.getName();
382        return type.valueFromXML(parent, dataName, additionalData);
383    }
384    
385    /**
386     * Extracts the values of the given group
387     * @param <T> type of the values to retrieve (example: {@link Map} for a composite, {@link List} for a repeater
388     * @param parent the DOM element of the group's parent
389     * @param modelViewItemGroup view item corresponding to the group
390     * @param dataName the name of the data to extract
391     * @param prefix the path of the group's parent
392     * @return the value
393     * @throws Exception if an error occurs
394     */
395    @SuppressWarnings("unchecked")
396    protected <T> T _extractGroupValues(Element parent, ModelViewItemGroup modelViewItemGroup, String dataName, String prefix) throws Exception
397    {
398        ModelItemGroup modelItemGroup = modelViewItemGroup.getDefinition();
399        String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName;
400
401        if (modelItemGroup instanceof RepeaterDefinition)
402        {
403            if (DataHolderHelper.isRepeaterEntryPath(dataName))
404            {
405                Pair<String, Integer> repeaterNameAndEntryPosition = DataHolderHelper.getRepeaterNameAndEntryPosition(dataName);
406                Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(parent, repeaterNameAndEntryPosition.getLeft() + "/entry[@name='" + repeaterNameAndEntryPosition.getRight() + "']");
407                return repeaterEntryElement != null ? (T) _extractValues(repeaterEntryElement, modelViewItemGroup, newPrefix) : null;
408            }
409            else
410            {
411                Element repeaterElement = DOMUtils.getChildElementByTagName(parent, dataName);
412                if (repeaterElement != null)
413                {
414                    List<Map<String, Object>> repeaterValues = new ArrayList<>();
415                    int repeaterSize = Integer.valueOf(XPathAPI.eval(repeaterElement, "count(entry)").str());
416                    for (int i = 1; i <= repeaterSize; i++)
417                    {
418                        Element repeaterEntryElement = (Element) XPathAPI.selectSingleNode(repeaterElement, "entry[@name='" + i + "']");
419                        Map<String, Object> repeaterEntryValues = _extractValues(repeaterEntryElement, modelViewItemGroup, newPrefix + "[" + i + "]");
420                        repeaterValues.add(repeaterEntryValues);
421                    }
422                    return (T) repeaterValues;
423                }
424                else
425                {
426                    return null;
427                }
428            }
429        }
430        else
431        {
432            Element compositeElement = DOMUtils.getChildElementByTagName(parent, dataName);
433            return compositeElement != null ? (T) _extractValues(compositeElement, modelViewItemGroup, newPrefix) : null;
434        }
435    }
436}