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.util.HashMap;
019import java.util.List;
020import java.util.Map;
021import java.util.Optional;
022
023import org.apache.commons.lang3.StringUtils;
024import org.apache.xpath.XPathAPI;
025import org.w3c.dom.Element;
026
027import org.ametys.core.util.dom.DOMUtils;
028import org.ametys.plugins.repository.data.extractor.ModelLessValuesExtractor;
029import org.ametys.runtime.model.ModelItem;
030import org.ametys.runtime.model.type.ElementType;
031import org.ametys.runtime.model.type.ModelItemType;
032import org.ametys.runtime.plugin.component.AbstractThreadSafeComponentExtensionPoint;
033
034/**
035 * This class provides methods to extract values from an XML document
036 */
037public class ModelLessXMLValuesExtractor implements ModelLessValuesExtractor
038{
039    /** The DOM element containing the XML values */
040    protected Element _element;
041    
042    /** The getter that retrieves needed additional data by types */
043    protected XMLValuesExtractorAdditionalDataGetter _additionalDataGetter;
044    
045    /** Extension point providing available data types */
046    protected AbstractThreadSafeComponentExtensionPoint<? extends ModelItemType> _dataTypesExtensionPoint;
047
048    /**
049     * Creates a model less XML values extractor
050     * @param element the DOM element containing the XML values
051     * @param additionalDataGetter the getter that retrieves needed additional data by types
052     * @param dataTypesExtensionPoint the extension point providing available data types
053     */
054    public ModelLessXMLValuesExtractor(Element element, XMLValuesExtractorAdditionalDataGetter additionalDataGetter, AbstractThreadSafeComponentExtensionPoint<? extends ModelItemType> dataTypesExtensionPoint)
055    {
056        _element = element;
057        _additionalDataGetter = additionalDataGetter;
058        _dataTypesExtensionPoint = dataTypesExtensionPoint;
059    }
060    
061    public Map<String, Object> extractValues() throws Exception
062    {
063        return _extractValues(_element, "");
064    }
065    
066    /**
067     * Extracts all the values in the current element
068     * @param currentElement the current element
069     * @param prefix the path of the item represented by the current element (prefix of all contained items)
070     * @return the values
071     * @throws Exception if an error occurs
072     */
073    protected Map<String, Object> _extractValues(Element currentElement, String prefix) throws Exception
074    {
075        Map<String, Object> values = new HashMap<>();
076        
077        List<Element> children = DOMUtils.getUniqueChildElements(currentElement);
078        for (Element child : children)
079        {
080            String dataName = child.getNodeName();
081            Object value = _extractValue(currentElement, dataName, prefix);
082            if (value != null)
083            {
084                values.put(dataName, value);
085            }
086        }
087        
088        return values;
089    }
090
091    public <T> T extractValue(String dataPath) throws Exception
092    {
093        return _extractValue(_element, dataPath, "");
094    }
095    
096    /**
097     * Extracts the value at the given path
098     * @param <T> type of the value to retrieve
099     * @param currentElement the DOM element containing the items' values 
100     * @param relativeDataPath The data path relative to the current element
101     * @param prefix the path of the item represented by the current element (prefix of all contained items)
102     * @return the value
103     * @throws Exception if an error occurs
104     */
105    @SuppressWarnings("unchecked")
106    protected <T> T _extractValue(Element currentElement, String relativeDataPath, String prefix) throws Exception
107    {
108        String[] pathSegments = StringUtils.split(relativeDataPath, ModelItem.ITEM_PATH_SEPARATOR);
109
110        if (pathSegments == null || pathSegments.length < 1)
111        {
112            throw new IllegalArgumentException("Unable to extract the value of the data at the given path. This path is empty.");
113        }
114        else if (pathSegments.length == 1)
115        {
116            String dataName = relativeDataPath;
117            String dataTypeId = XPathAPI.eval(currentElement, dataName + "/@typeId").str();
118            ModelItemType type = _dataTypesExtensionPoint.getExtension(dataTypeId);
119            
120            if (type instanceof ElementType)
121            {
122                ElementType elementType = (ElementType) type;
123                String absoluteDataPath = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName;
124                Optional<Object> additionalData = _additionalDataGetter.getAdditionalData(absoluteDataPath, elementType);
125                
126                return (T) elementType.valueFromXML(currentElement, dataName, additionalData);
127            }
128            else
129            {
130                String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + dataName : dataName;
131
132                Element compositeNode = (Element) XPathAPI.selectSingleNode(currentElement, dataName);
133                return compositeNode != null ? (T) _extractValues(compositeNode, newPrefix) : null;
134            }
135        }
136        else
137        {
138            String firstSegmentDataName = pathSegments[0];
139            String newRelativeDataPath = StringUtils.join(pathSegments, ModelItem.ITEM_PATH_SEPARATOR, 1, pathSegments.length);
140            String newPrefix = StringUtils.isNotEmpty(prefix) ? prefix + ModelItem.ITEM_PATH_SEPARATOR + firstSegmentDataName : firstSegmentDataName;
141            
142            Element compositeNode = (Element) XPathAPI.selectSingleNode(currentElement, firstSegmentDataName);
143            return compositeNode != null ? _extractValue(compositeNode, newRelativeDataPath, newPrefix) : null;
144        }
145    }
146}