001/*
002 * Copyright 2011 Atteo.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014package org.atteo.evo.filtering;
015
016import java.util.ArrayList;
017
018import javax.annotation.Nullable;
019
020import org.w3c.dom.Element;
021import org.w3c.dom.NodeList;
022
023/**
024 * Get property value from XML by taking text content of a node pointed by property name.
025 *
026 * <p>
027 * The resolver tries to search for an element with a given property name. If none is found
028 * it tries to interpret dots (".") as separator between parent and children element names.
029 * For instance for document:
030 * <pre>
031 * {@code
032 * <a>
033 *     <b>
034 *         <c>test</c>
035 *     </b>
036 *     <e><f>test3</f></e>
037 *     <e.f>test2</e.f>
038 * </a>
039 * }
040 * </pre>
041 * property {@code ${a.b.c}} will return 'test' value and property {@code ${a.e.f}} will return 'test2' value.
042 * </p>
043 */
044public class XmlPropertyResolver extends SimplePropertyResolver {
045    private Element rootElement;
046    private boolean matchRoot;
047
048    /**
049     * Create new property resolver based on XML tree.
050     * @param rootElement root element of an XML tree to search for property value
051     * @param matchRoot whether root element should match, or matching should start from rootElement children
052     */
053    public XmlPropertyResolver(@Nullable Element rootElement, boolean matchRoot) {
054        this.rootElement = rootElement;
055        this.matchRoot = matchRoot;
056    }
057
058    @Override
059    public String getProperty(String name) throws PropertyNotFoundException {
060        String value = getValue(name);
061        if (value == null) {
062            return null;
063        }
064        return value;
065    }
066
067    private String getValue(String name) {
068        if (rootElement == null) {
069            return null;
070        }
071        int position = 0;
072        ArrayList<Integer> dots = new ArrayList<Integer>();
073        while (true) {
074            int index = name.indexOf(".", position);
075            if (index == -1) {
076                break;
077            }
078            dots.add(index);
079            position = index + 1;
080        }
081        dots.add(name.length());
082
083        Element element = rootElement;
084        int dotIndex;
085
086        if (matchRoot) {
087            if (!rootElement.getNodeName().equals(name.substring(0, dots.get(0)))) {
088                return null;
089            }
090
091            position = dots.get(0) + 1;
092            dotIndex = 0;
093        } else {
094            position = 0;
095            dotIndex = -1;
096        }
097
098        outer: while (position < name.length()) {
099            String key = name.substring(position);
100            if (element.hasAttribute(key)) {
101                return element.getAttribute(key);
102            }
103            for (int i = dots.size() - 1; i > dotIndex; i--) {
104                key = name.substring(position, dots.get(i));
105                NodeList list = element.getElementsByTagName(key);
106                if (list.getLength() == 1) {
107                    element = (Element) list.item(0);
108                    position = dots.get(i) + 1;
109                    dotIndex = i;
110                    continue outer;
111                }
112            }
113            return null;
114        }
115        // getTextContext() returns text content of all the children
116        // should we return only direct Text nodes elements content?
117        return element.getTextContent();
118    }
119
120}