org.nuxeo.theme.themes.ThemeParser.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.theme.themes.ThemeParser.java

Source

/*
 * (C) Copyright 2006-2007 Nuxeo SAS <http://nuxeo.com> and others
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Jean-Marc Orliaguet, Chalmers
 *
 * $Id$
 */

package org.nuxeo.theme.themes;

import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.theme.Manager;
import org.nuxeo.theme.elements.Element;
import org.nuxeo.theme.elements.ElementFactory;
import org.nuxeo.theme.elements.ElementFormatter;
import org.nuxeo.theme.elements.ThemeElement;
import org.nuxeo.theme.formats.Format;
import org.nuxeo.theme.formats.FormatFactory;
import org.nuxeo.theme.formats.styles.Style;
import org.nuxeo.theme.fragments.Fragment;
import org.nuxeo.theme.fragments.FragmentFactory;
import org.nuxeo.theme.nodes.NodeException;
import org.nuxeo.theme.perspectives.PerspectiveType;
import org.nuxeo.theme.presets.CustomPresetType;
import org.nuxeo.theme.presets.PresetManager;
import org.nuxeo.theme.presets.PresetType;
import org.nuxeo.theme.properties.FieldIO;
import org.nuxeo.theme.resources.ResourceBank;
import org.nuxeo.theme.types.TypeFamily;
import org.nuxeo.theme.types.TypeRegistry;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class ThemeParser {

    private static final Log log = LogFactory.getLog(ThemeParser.class);

    private static final String DOCROOT_NAME = "theme";

    private static final XPath xpath = XPathFactory.newInstance().newXPath();

    public static void registerTheme(final ThemeDescriptor themeDescriptor, final boolean preload)
            throws ThemeIOException {
        registerTheme(themeDescriptor, null, preload);
    }

    public static void registerTheme(final ThemeDescriptor themeDescriptor, final String xmlSource,
            final boolean preload) throws ThemeIOException {
        final String src = themeDescriptor.getSrc();
        InputStream in = null;
        try {
            if (xmlSource == null) {
                URL url = null;
                try {
                    url = new URL(src);
                } catch (MalformedURLException e) {
                    if (themeDescriptor.getContext() != null) {
                        url = themeDescriptor.getContext().getResource(src);
                    } else {
                        url = Thread.currentThread().getContextClassLoader().getResource(src);
                    }
                }
                if (url == null) {
                    throw new ThemeIOException("Incorrect theme URL: " + src);
                }
                in = url.openStream();
            } else {
                in = new ByteArrayInputStream(xmlSource.getBytes());
            }
            registerThemeFromInputStream(themeDescriptor, in, preload);
        } catch (FileNotFoundException e) {
            throw new ThemeIOException("File not found: " + src, e);
        } catch (IOException e) {
            throw new ThemeIOException("Could not open file: " + src, e);
        } catch (ThemeException e) {
            throw new ThemeIOException("Parsing error: " + src, e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    log.error(e);
                } finally {
                    in = null;
                }
            }
        }
    }

    private static void registerThemeFromInputStream(final ThemeDescriptor themeDescriptor, final InputStream in,
            boolean preload) throws ThemeIOException, ThemeException {
        String themeName = null;

        final InputSource is = new InputSource(in);
        final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        try {
            dbf.setFeature("http://xml.org/sax/features/validation", false);
            dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        } catch (ParserConfigurationException e) {
            log.debug("Could not set DTD non-validation feature");
        }

        DocumentBuilder db;
        try {
            db = dbf.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new ThemeIOException(e);
        }
        Document document;
        try {
            document = db.parse(is);
        } catch (SAXException e) {
            throw new ThemeIOException(e);
        } catch (IOException e) {
            throw new ThemeIOException(e);
        }
        final org.w3c.dom.Element docElem = document.getDocumentElement();
        if (!docElem.getNodeName().equals(DOCROOT_NAME)) {
            throw new ThemeIOException("No <" + DOCROOT_NAME + "> document tag found in " + in.toString()
                    + ", ignoring the resource.");
        }

        themeName = docElem.getAttributes().getNamedItem("name").getNodeValue();
        if (!ThemeManager.validateThemeName(themeName)) {
            throw new ThemeIOException(
                    "Theme names may only contain alpha-numeric characters, underscores and hyphens: " + themeName);
        }
        themeDescriptor.setName(themeName);

        loadTheme(themeDescriptor, docElem, preload);
    }

    private static void loadTheme(ThemeDescriptor themeDescriptor, org.w3c.dom.Element docElem, boolean preload)
            throws ThemeException, ThemeIOException {
        final ThemeManager themeManager = Manager.getThemeManager();

        // remove old theme
        String themeName = themeDescriptor.getName();
        ThemeElement oldTheme = themeManager.getThemeByName(themeName);
        if (oldTheme != null) {
            try {
                themeManager.destroyElement(oldTheme);
            } catch (NodeException e) {
                throw new ThemeIOException("Failed to destroy theme: " + themeName, e);
            }
        }

        Node baseNode = getBaseNode(docElem);

        final Map<Integer, String> inheritanceMap = new HashMap<Integer, String>();
        final Map<Style, Map<String, Properties>> commonStyles = new LinkedHashMap<Style, Map<String, Properties>>();

        // create a new theme
        ThemeElement theme = (ThemeElement) ElementFactory.create("theme");
        theme.setName(themeName);
        Node description = docElem.getAttributes().getNamedItem("description");
        if (description != null) {
            theme.setDescription(description.getNodeValue());
        }

        String resourceBankName = null;
        Node resourceBankNode = docElem.getAttributes().getNamedItem("resource-bank");
        if (resourceBankNode != null) {
            resourceBankName = resourceBankNode.getNodeValue();
            themeDescriptor.setResourceBankName(resourceBankName);
        }

        Node templateEngines = docElem.getAttributes().getNamedItem("template-engines");
        if (templateEngines != null) {
            themeDescriptor.setTemplateEngines(Arrays.asList(templateEngines.getNodeValue().split(",")));
        }

        if (preload) {
            // Only register pages
            registerThemePages(theme, baseNode);

        } else {
            // Register resources from remote bank
            if (resourceBankName != null) {
                try {
                    ResourceBank resourceBank = ThemeManager.getResourceBank(resourceBankName);
                    resourceBank.connect(themeName);
                } catch (ThemeException e) {
                    log.warn("Resource bank not found: " + resourceBankName);
                }
            }

            // register custom presets
            for (Node n : getChildElementsByTagName(docElem, "presets")) {
                parsePresets(theme, n);
            }

            // register formats
            for (Node n : getChildElementsByTagName(docElem, "formats")) {
                parseFormats(theme, docElem, commonStyles, inheritanceMap, n);
            }

            // setup style inheritance
            for (Map.Entry<Integer, String> entry : inheritanceMap.entrySet()) {
                Integer styleUid = entry.getKey();
                String inheritedStyleName = entry.getValue();
                Format style = ThemeManager.getFormatById(styleUid);
                Format inheritedStyle = (Format) themeManager.getNamedObject(themeName, "style",
                        inheritedStyleName);
                if (inheritedStyle == null) {
                    log.warn("Cannot make style inherit from unknown style : " + inheritedStyleName);
                    continue;
                }
                themeManager.makeFormatInherit(style, inheritedStyle);
            }

            // styles created by the parser
            createCommonStyles(themeName, commonStyles);

            // register element properties
            for (Node n : getChildElementsByTagName(docElem, "properties")) {
                parseProperties(docElem, n);
            }

            parseLayout(theme, baseNode);

            themeManager.removeOrphanedFormats();
        }

        if (preload) {
            log.debug("Registered THEME: " + themeName);
            themeDescriptor.setLastLoaded(null);
        } else {
            log.debug("Loaded THEME: " + themeName);
            themeDescriptor.setLastLoaded(new Date());
        }

        // Register in the type registry
        themeManager.registerTheme(theme);
    }

    public static boolean checkElementName(String name) throws ThemeIOException {
        return name.matches("[a-z][a-z0-9_\\-\\s]+");
    }

    public static void registerThemePages(final Element parent, Node node) throws ThemeIOException, ThemeException {
        for (Node n : getChildElements(node)) {
            String nodeName = n.getNodeName();
            NamedNodeMap attributes = n.getAttributes();
            Element elem;
            if ("page".equals(nodeName)) {
                elem = ElementFactory.create(nodeName);

                Node nameAttr = attributes.getNamedItem("name");
                if (nameAttr != null) {
                    String elementName = nameAttr.getNodeValue();
                    if (checkElementName(elementName)) {
                        elem.setName(elementName);
                    } else {
                        throw new ThemeIOException("Page name not allowed: " + elementName);
                    }
                }

                try {
                    parent.addChild(elem);
                } catch (NodeException e) {
                    throw new ThemeIOException("Failed to parse layout.", e);
                }
            }
        }
    }

    public static void parseLayout(final Element parent, Node node) throws ThemeIOException, ThemeException {
        TypeRegistry typeRegistry = Manager.getTypeRegistry();
        ThemeManager themeManager = Manager.getThemeManager();
        for (String formatName : typeRegistry.getTypeNames(TypeFamily.FORMAT)) {
            Format format = (Format) node.getUserData(formatName);
            if (format != null) {
                if (ElementFormatter.getElementsFor(format).isEmpty()) {
                    ElementFormatter.setFormat(parent, format);
                } else {
                    Format duplicatedFormat = themeManager.duplicateFormat(format);
                    ElementFormatter.setFormat(parent, duplicatedFormat);
                }
            }
        }

        Properties properties = (Properties) node.getUserData("properties");
        if (properties != null) {
            FieldIO.updateFieldsFromProperties(parent, properties);
        }

        for (Node n : getChildElements(node)) {
            String nodeName = n.getNodeName();
            NamedNodeMap attributes = n.getAttributes();
            Element elem;

            if ("fragment".equals(nodeName)) {
                String fragmentType = attributes.getNamedItem("type").getNodeValue();
                elem = FragmentFactory.create(fragmentType);
                if (elem == null) {
                    log.error("Could not create fragment: " + fragmentType);
                    continue;
                }
                Fragment fragment = (Fragment) elem;
                Node perspectives = attributes.getNamedItem("perspectives");
                if (perspectives != null) {
                    for (String perspectiveName : perspectives.getNodeValue().split(",")) {

                        PerspectiveType perspective = (PerspectiveType) typeRegistry.lookup(TypeFamily.PERSPECTIVE,
                                perspectiveName);

                        if (perspective == null) {
                            log.warn("Could not find perspective: " + perspectiveName);
                        } else {
                            fragment.setVisibleInPerspective(perspective);
                        }
                    }
                }
            } else {
                elem = ElementFactory.create(nodeName);
            }

            if (elem == null) {
                throw new ThemeIOException("Could not parse node: " + nodeName);
            }

            Node nameAttr = attributes.getNamedItem("name");
            if (nameAttr != null) {
                String elementName = nameAttr.getNodeValue();
                if (checkElementName(elementName)) {
                    elem.setName(elementName);
                } else {
                    log.warn(
                            "Element names may only contain lower-case alpha-numeric characters, digits, underscores, spaces and dashes: "
                                    + elementName);
                }
            }

            Node classAttr = attributes.getNamedItem("class");
            if (classAttr != null) {
                elem.setCssClassName(classAttr.getNodeValue());
            }

            String description = getCommentAssociatedTo(n);
            if (description != null) {
                elem.setDescription(description);
            }

            try {
                parent.addChild(elem);
            } catch (NodeException e) {
                throw new ThemeIOException("Failed to parse layout.", e);
            }
            parseLayout(elem, n);
        }
    }

    public static void parsePresets(final ThemeElement theme, Node node) {
        final TypeRegistry typeRegistry = Manager.getTypeRegistry();
        final String themeName = theme.getName();
        PresetManager.clearCustomPresets(themeName);
        for (Node n : getChildElements(node)) {
            NamedNodeMap attrs = n.getAttributes();
            final String name = attrs.getNamedItem("name").getNodeValue();
            final String category = attrs.getNamedItem("category").getNodeValue();
            final String value = n.getTextContent();
            final String group = themeName; // use the theme's name as
            // group name

            final Node labelAttr = attrs.getNamedItem("label");
            String label = "";
            if (labelAttr != null) {
                label = labelAttr.getNodeValue();
            }

            final Node descriptionAttr = attrs.getNamedItem("description");
            String description = "";
            if (descriptionAttr != null) {
                description = descriptionAttr.getNodeValue();
            }

            PresetType preset = new CustomPresetType(name, value, group, category, label, description);
            typeRegistry.register(preset);
        }
    }

    public static void parseFormats(final ThemeElement theme, org.w3c.dom.Element doc,
            Map<Style, Map<String, Properties>> commonStyles, Map<Integer, String> inheritanceMap, Node node)
            throws ThemeIOException, ThemeException {
        Node baseNode = getBaseNode(doc);
        String themeName = theme.getName();

        String resourceBankName = null;
        ThemeDescriptor themeDescriptor = ThemeManager.getThemeDescriptorByThemeName(themeName);
        if (themeDescriptor != null) {
            resourceBankName = themeDescriptor.getResourceBankName();
        }

        ThemeManager themeManager = Manager.getThemeManager();

        for (Node n : getChildElements(node)) {
            String nodeName = n.getNodeName();
            NamedNodeMap attributes = n.getAttributes();
            Node elementItem = attributes.getNamedItem("element");
            String elementXPath = null;
            if (elementItem != null) {
                elementXPath = elementItem.getNodeValue();
            }

            Format format;
            try {
                format = FormatFactory.create(nodeName);
            } catch (ThemeException e) {
                throw new ThemeIOException(e);
            }
            format.setProperties(getPropertiesFromNode(n));

            String description = getCommentAssociatedTo(n);
            if (description != null) {
                format.setDescription(description);
            }

            if ("widget".equals(nodeName)) {
                List<Node> viewNodes = getChildElementsByTagName(n, "view");
                if (!viewNodes.isEmpty()) {
                    format.setName(viewNodes.get(0).getTextContent());
                }

            } else if ("layout".equals(nodeName)) {
                // TODO: validate layout properties

            } else if ("style".equals(nodeName)) {
                Node nameAttr = attributes.getNamedItem("name");
                Style style = (Style) format;

                // register the style name
                String styleName = null;
                if (nameAttr != null) {
                    styleName = nameAttr.getNodeValue();
                    // the style may have been registered already
                    Style registeredStyle = (Style) themeManager.getNamedObject(themeName, "style", styleName);
                    if (registeredStyle == null) {
                        style.setName(styleName);
                        themeManager.setNamedObject(themeName, "style", style);
                    } else {
                        style = registeredStyle;
                    }
                }

                Node inheritedAttr = attributes.getNamedItem("inherit");
                if (inheritedAttr != null) {
                    String inheritedName = inheritedAttr.getNodeValue();
                    if ("".equals(inheritedName)) {
                        continue;
                    }
                    inheritanceMap.put(style.getUid(), inheritedName);
                }

                Node remoteAttr = attributes.getNamedItem("remote");
                if (remoteAttr != null) {
                    Boolean remote = Boolean.valueOf(remoteAttr.getNodeValue());
                    if (style.isNamed()) {
                        style.setRemote(remote);
                    } else {
                        log.warn("Only named styles can be remote, ignoring remote attribute on" + style.getUid());
                    }
                }

                if (styleName != null && elementXPath != null) {
                    log.warn("Style parser: named style '" + styleName + "' cannot have an 'element' attribute: '"
                            + elementXPath + "'.");
                    continue;
                }

                List<Node> selectorNodes = getChildElementsByTagName(n, "selector");

                if (style.isRemote() && resourceBankName != null) {
                    if (!selectorNodes.isEmpty()) {
                        style.setCustomized(true);
                    }
                }

                // Use style properties from the theme
                for (Node selectorNode : selectorNodes) {
                    NamedNodeMap attrs = selectorNode.getAttributes();
                    Node pathAttr = attrs.getNamedItem("path");
                    if (pathAttr == null) {
                        log.warn(String.format(
                                "Style parser: named style '%s' has a selector with no path: ignored", styleName));
                        continue;
                    }
                    String path = pathAttr.getNodeValue();

                    String viewName = null;
                    Node viewAttr = attrs.getNamedItem("view");
                    if (viewAttr != null) {
                        viewName = viewAttr.getNodeValue();
                    }

                    String selectorDescription = getCommentAssociatedTo(selectorNode);
                    if (selectorDescription != null) {
                        style.setSelectorDescription(path, viewName, selectorDescription);
                    }

                    // BBB: remove in a later release
                    if (elementXPath != null && (viewName == null || viewName.equals("*"))) {
                        log.warn("Style parser: trying to guess the view name for: " + elementXPath);
                        viewName = guessViewNameFor(doc, elementXPath);
                        if (viewName == null) {
                            if (!commonStyles.containsKey(style)) {
                                commonStyles.put(style, new LinkedHashMap<String, Properties>());
                            }
                            commonStyles.get(style).put(path, getPropertiesFromNode(selectorNode));
                        }
                    }

                    if (styleName != null) {
                        if (viewName != null) {
                            log.info("Style parser: ignoring view name '" + viewName + "' in named style '"
                                    + styleName + "'.");
                        }
                        viewName = "*";
                    }

                    if (viewName != null) {
                        style.setPropertiesFor(viewName, path, getPropertiesFromNode(selectorNode));
                    }
                }
            }

            themeManager.registerFormat(format);
            if (elementXPath != null) {
                if ("".equals(elementXPath)) {
                    baseNode.setUserData(nodeName, format, null);
                } else {
                    for (Node element : getNodesByXPath(baseNode, elementXPath)) {
                        element.setUserData(nodeName, format, null);
                    }
                }
            }
        }

    }

    public static void createCommonStyles(String themeName, Map<Style, Map<String, Properties>> commonStyles)
            throws ThemeException {
        ThemeManager themeManager = Manager.getThemeManager();
        int count = 1;
        for (Style parent : commonStyles.keySet()) {
            Style s = (Style) FormatFactory.create("style");
            String name = "";
            while (true) {
                name = String.format("common style %s", count);
                if (themeManager.getNamedObject(themeName, "style", name) == null) {
                    break;
                }
                count += 1;
            }
            s.setName(name);
            themeManager.registerFormat(s);
            themeManager.setNamedObject(themeName, "style", s);
            Map<String, Properties> map = commonStyles.get(parent);
            for (Map.Entry<String, Properties> entry : map.entrySet()) {
                s.setPropertiesFor("*", entry.getKey(), entry.getValue());
            }
            // if the style already inherits, preserve the inheritance
            Style ancestor = (Style) ThemeManager.getAncestorFormatOf(parent);
            if (ancestor != null) {
                themeManager.makeFormatInherit(s, ancestor);
            }

            themeManager.makeFormatInherit(parent, s);
            log.info("Created extra style: " + s.getName());
        }
    }

    public static void parseProperties(org.w3c.dom.Element doc, Node node) throws ThemeIOException {
        NamedNodeMap attributes = node.getAttributes();
        Node elementAttr = attributes.getNamedItem("element");
        if (elementAttr == null) {
            throw new ThemeIOException("<properties> node has no 'element' attribute.");
        }
        String elementXPath = elementAttr.getNodeValue();

        Node baseNode = getBaseNode(doc);
        Node element = null;
        try {
            element = (Node) xpath.evaluate(elementXPath, baseNode, XPathConstants.NODE);
        } catch (XPathExpressionException e) {
            throw new ThemeIOException(e);
        }
        if (element == null) {
            throw new ThemeIOException("Could not find the element associated to: " + elementXPath);
        }
        Properties properties = getPropertiesFromNode(node);
        if (properties != null) {
            element.setUserData("properties", properties, null);
        }
    }

    private static Properties getPropertiesFromNode(Node node) {
        Properties properties = new Properties();
        for (Node n : getChildElements(node)) {
            String textContent = n.getTextContent();
            Node presetAttr = n.getAttributes().getNamedItem("preset");
            if (presetAttr != null) {
                String presetName = presetAttr.getNodeValue();
                if (presetName != null) {
                    textContent = String.format("\"%s\"", presetName);
                }
            }
            properties.setProperty(n.getNodeName(), Framework.expandVars(textContent));
        }
        return properties;
    }

    private static List<Node> getChildElements(Node node) {
        List<Node> nodes = new ArrayList<Node>();
        NodeList childNodes = node.getChildNodes();
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node n = childNodes.item(i);
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                nodes.add(n);
            }
        }
        return nodes;
    }

    public static List<Node> getChildElementsByTagName(Node node, String tagName) {
        List<Node> nodes = new ArrayList<Node>();
        NodeList childNodes = node.getChildNodes();
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node n = childNodes.item(i);
            if (n.getNodeType() == Node.ELEMENT_NODE && tagName.equals(n.getNodeName())) {
                nodes.add(n);
            }
        }
        return nodes;
    }

    public static Node getBaseNode(org.w3c.dom.Element doc) throws ThemeIOException {
        Node baseNode = null;
        try {
            baseNode = (Node) xpath.evaluate('/' + DOCROOT_NAME + "/layout", doc, XPathConstants.NODE);
        } catch (XPathExpressionException e) {
            throw new ThemeIOException(e);
        }
        if (baseNode == null) {
            throw new ThemeIOException("No <layout> section found.");
        }
        return baseNode;
    }

    private static String getCommentAssociatedTo(Node node) {
        Node n = node;
        while (true) {
            n = n.getPreviousSibling();
            if (n == null) {
                break;
            }
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                break;
            }
            if (n.getNodeType() == Node.COMMENT_NODE) {
                return n.getNodeValue().trim();
            }
        }
        return null;
    }

    // BBB shouldn't have to guess view names
    private static String guessViewNameFor(org.w3c.dom.Element doc, String elementXPath) {
        NodeList widgetNodes = doc.getElementsByTagName("widget");
        Set<String> candidates = new HashSet<String>();
        String[] elements = elementXPath.split("\\|");
        for (int i = 0; i < widgetNodes.getLength(); i++) {
            Node node = widgetNodes.item(i);
            NamedNodeMap attributes = node.getAttributes();
            Node elementAttr = attributes.getNamedItem("element");
            if (elementAttr != null) {
                String[] widgetElements = elementAttr.getNodeValue().split("\\|");
                for (String element : elements) {
                    for (String widgetElement : widgetElements) {
                        if (element.equals(widgetElement)) {
                            List<Node> viewNodes = getChildElementsByTagName(node, "view");
                            if (!viewNodes.isEmpty()) {
                                candidates.add(viewNodes.get(0).getTextContent());
                            }
                        }
                    }
                }
            }
        }
        if (candidates.size() == 1) {
            return candidates.iterator().next();
        }
        return null;
    }

    private static List<Node> getNodesByXPath(Node baseNode, String elementXPath) throws ThemeIOException {
        final List<Node> nodes = new ArrayList<Node>();
        if (elementXPath != null) {
            try {
                NodeList elementNodes = (NodeList) xpath.evaluate(elementXPath, baseNode, XPathConstants.NODESET);
                for (int i = 0; i < elementNodes.getLength(); i++) {
                    nodes.add(elementNodes.item(i));
                }
            } catch (XPathExpressionException e) {
                throw new ThemeIOException(e);
            }
        }
        return nodes;
    }
}