/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * Granite Data Services is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * Granite Data Services is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA, or see <http://www.gnu.org/licenses/>. */ package org.granite.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import org.granite.logging.Logger; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.EntityResolver; import org.xml.sax.SAXException; /** * Utility class that makes XML fragment tree manipulation easier. * <br /> * This class relies on JDK DOM & XPath built-in implementations. * * @author Franck WOLFF */ public class XMap implements Serializable { private static final Logger log = Logger.getLogger(XMap.class); private static final long serialVersionUID = 1L; protected static final String DEFAULT_ROOT_NAME = "root"; /** * An empty and unmodifiable XMap instance. */ public static final XMap EMPTY_XMAP = new XMap(null, null, false) { private static final long serialVersionUID = 1L; @Override public String put(String key, String value) { throw new RuntimeException("Immutable XMap"); } @Override public String remove(String key) { throw new RuntimeException("Immutable XMap"); } }; private transient Element root = null; private transient XMLUtil xmlUtil = null; /** * Constructs a new XMap instance. */ public XMap() { this(null, null, false); } /** * Constructs a new XMap instance. * * @param root the name of the root element (may be null). */ public XMap(String root) { if (root != null) { this.root = getXMLUtil().newDocument(root).getDocumentElement(); } } /** * Constructs a new XMap instance from an XML input stream. * * @param input an XML input stream. */ public XMap(InputStream input) throws IOException, SAXException { this.root = getXMLUtil().loadDocument(input).getDocumentElement(); } /** * Constructs a new XMap instance from an XML input stream. * * @param input an XML input stream. */ public XMap(InputStream input, EntityResolver resolver) throws IOException, SAXException { this.root = getXMLUtil().loadDocument(input, resolver, null).getDocumentElement(); } /** * Constructs a new XMap instance. * * @param root a DOM element (may be null). */ public XMap(Element root) { this(null, root, true); } /** * Constructs a new XMap instance based on an existing XMap and clone its content. * * @param map the map to duplicate (root element is cloned so modification to this * new instance won't modify the original XMap). */ public XMap(XMap map) { this((map == null ? null : map.xmlUtil), (map == null ? null : map.root), true); } /** * Constructs a new XMap instance. * * @param root the root element (may be null). * @param clone should we clone the root element (prevent original node modification). */ protected XMap(XMLUtil xmlUtil, Element root, boolean clone) { this.xmlUtil = xmlUtil; this.root = (clone && root != null ? (Element)root.cloneNode(true) : root); } private XMLUtil getXMLUtil() { if (xmlUtil == null) xmlUtil = XMLUtilFactory.getXMLUtil(); return xmlUtil; } /** * Allows direct manipulation of the root element. * * @return the root element of this XMap instance. */ public Element getRoot() { return root; } /** * Returns true if the supplied key XPath expression matches at least one element, attribute * or text in the root element of this XMap. * * @param key an XPath expression. * @return true if the supplied key XPath expression matches at least one element, attribute * or text in the root element of this XMap, false otherwise. * @throws RuntimeException if the XPath expression isn't correct. */ public boolean containsKey(String key) { if (root == null) return false; try { Node result = getXMLUtil().selectSingleNode(root, key); return ( result != null && ( result.getNodeType() == Node.ELEMENT_NODE || result.getNodeType() == Node.TEXT_NODE || result.getNodeType() == Node.ATTRIBUTE_NODE ) ); } catch (Exception e) { throw new RuntimeException(e); } } /** * Returns the text value of the element (or attribute or text) that matches the supplied * XPath expression. * * @param key an XPath expression. * @return the text value of the matched element or null if the element does not exist or have * no value. * @throws RuntimeException if the XPath expression isn't correct. */ public String get(String key) { if (root == null) return null; try { return getXMLUtil().getNormalizedValue(getXMLUtil().selectSingleNode(root, key)); } catch (Exception e) { throw new RuntimeException(e); } } public <T> T get(String key, Class<T> clazz, T defaultValue) { return get(key, clazz, defaultValue, false, true); } public <T> T get(String key, Class<T> clazz, T defaultValue, boolean required, boolean warn) { String sValue = get(key); if (required && sValue == null) throw new RuntimeException(key + " value is required in XML file:\n" + toString()); Object oValue = defaultValue; boolean unsupported = false; if (sValue != null) { try { if (clazz == String.class) oValue = sValue; else if (clazz == Integer.class || clazz == Integer.TYPE) oValue = Integer.valueOf(sValue); else if (clazz == Long.class || clazz == Long.TYPE) oValue = Long.valueOf(sValue); else if (clazz == Boolean.class || clazz == Boolean.TYPE) { if (!Boolean.TRUE.toString().equalsIgnoreCase(sValue) && !Boolean.FALSE.toString().equalsIgnoreCase(sValue)) throw new NumberFormatException(sValue); oValue = Boolean.valueOf(sValue); } else if (clazz == Double.class || clazz == Double.TYPE) oValue = Double.valueOf(sValue); else if (clazz == Float.class || clazz == Float.TYPE) oValue = Float.valueOf(sValue); else if (clazz == Short.class || clazz == Short.TYPE) oValue = Short.valueOf(sValue); else if (clazz == Byte.class || clazz == Byte.TYPE) oValue = Byte.valueOf(sValue); else unsupported = true; } catch (Exception e) { if (warn) log.warn(e, "Illegal %s value for %s=%s (using default: %s)", clazz.getSimpleName(), key, sValue, defaultValue); } } if (unsupported) throw new UnsupportedOperationException("Unsupported value type: " + clazz.getName()); @SuppressWarnings("unchecked") T tValue = (T)oValue; return tValue; } /** * Returns a list of XMap instances with all elements that match the * supplied XPath expression. Note that XPath result nodes that are not instance of * Element are ignored. Note also that returned XMaps contain original child elements of * the root element of this XMap so modifications made to child elements affect this XMap * instance as well. * * @param key an XPath expression. * @return an unmodifiable list of XMap instances. * @throws RuntimeException if the XPath expression isn't correct. */ public List<XMap> getAll(String key) { if (root == null) return new ArrayList<XMap>(0); try { List<Node> result = getXMLUtil().selectNodeSet(root, key); List<XMap> xMaps = new ArrayList<XMap>(result.size()); for (Node node : result) { if (node.getNodeType() == Node.ELEMENT_NODE) xMaps.add(new XMap(this.xmlUtil, (Element)node, false)); } return xMaps; } catch (Exception e) { throw new RuntimeException(e); } } /** * Returns a new XMap instance with the first element that matches the * supplied XPath expression or null if this XMap root element is null, or if XPath evaluation * result is null, or this result is not an Element. Returned XMap contains original child element of * the root element of this XMap so modifications made to the child element affect this XMap * instance as well. * * @param key an XPath expression. * @return a single new XMap instance. * @throws RuntimeException if the XPath expression isn't correct. */ public XMap getOne(String key) { if (root == null) return null; try { Node node = getXMLUtil().selectSingleNode(root, key); if (node == null || node.getNodeType() != Node.ELEMENT_NODE) return null; return new XMap(xmlUtil, (Element)node, false); } catch (Exception e) { throw new RuntimeException(e); } } /** * Creates or updates the text value of the element (or text or attribute) matched by * the supplied XPath expression. If the matched element (or text or attribute) does not exist, * it is created with the last segment of the XPath expression (but its parent must already exist). * * @param key an XPath expression. * @param value the value to set (may be null). * @return the previous value of the matched element (may be null). * @throws RuntimeException if the root element of this XMap is null, if the XPath expression is not valid, * or (creation case) if the parent node does not exist or is not an element instance. */ public String put(String key, String value) { return put(key, value, false); } /** * Creates or updates the text value of the element (or text or attribute) matched by * the supplied XPath expression. If the matched element (or text or attribute) does not exist or if append * is <tt>true</tt>, it is created with the last segment of the XPath expression (but its parent must already * exist). * * @param key an XPath expression. * @param value the value to set (may be null). * @param append should the new element be appended (created) next to a possibly existing element(s) of * the same name? * @return the previous value of the matched element (may be null). * @throws RuntimeException if the root element of this XMap is null, if the XPath expression is not valid, * or (creation case) if the parent node does not exist or is not an element instance. */ public String put(String key, String value, boolean append) { if (root == null) root = getXMLUtil().newDocument(DEFAULT_ROOT_NAME).getDocumentElement(); if (!append) { try { Node selectResult = getXMLUtil().selectSingleNode(root, key); if (selectResult != null) return getXMLUtil().setValue(selectResult, value); } catch(RuntimeException e) { throw e; } catch(Exception e) { throw new RuntimeException(e); } } Element parent = root; String name = key; int iLastSlash = key.lastIndexOf('/'); if (iLastSlash != -1) { name = key.substring(iLastSlash + 1); Node selectResult = null; try { selectResult = getXMLUtil().selectSingleNode(root, key.substring(0, iLastSlash)); } catch (Exception e) { throw new RuntimeException(e); } if (selectResult == null) throw new RuntimeException("Parent node does not exist: " + key.substring(0, iLastSlash)); if (!(selectResult instanceof Element)) throw new RuntimeException("Parent node must be an Element: " + key.substring(0, iLastSlash) + " -> " + selectResult); parent = (Element)selectResult; } if (name.length() > 0 && name.charAt(0) == '@') parent.setAttribute(name.substring(1), value); else getXMLUtil().newElement(parent, name, value); return null; } /** * Removes the element, text or attribute that matches the supplied XPath expression. * * @param key an XPath expression. * @return the previous value of the matched node if any. * @throws RuntimeException if the XPath expression isn't valid. */ public String remove(String key) { if (root == null) return null; try { Node node = getXMLUtil().selectSingleNode(root, key); if (node != null) { String value = getXMLUtil().getNormalizedValue(node); if (node.getNodeType() == Node.ATTRIBUTE_NODE) ((Attr)node).getOwnerElement().removeAttribute(node.getNodeName()); else node.getParentNode().removeChild(node); return value; } } catch(Exception e) { throw new RuntimeException(e); } return null; } /** * Returns a "pretty" XML representation of the root element of this XMap (may be null). * * @return a "pretty" XML representation of the root element of this XMap (may be null). */ @Override public String toString() { return getXMLUtil().toNodeString(root); } /** * Write java.io.Serializable method. * * @param out the ObjectOutputStream where to write this XMap. * @throws IOException if writing fails. */ private void writeObject(ObjectOutputStream out) throws IOException { if (root == null) out.writeInt(0); else { ByteArrayOutputStream output = new ByteArrayOutputStream(); try { getXMLUtil().saveDocument(root.getOwnerDocument(), output); } catch (Exception e) { IOException ioe = new IOException("Could not serialize this XMap"); ioe.initCause(e); throw ioe; } out.writeInt(output.size()); out.write(output.toByteArray()); } } /** * Read java.io.Serializable method. * * @param in the ObjectInputStream from which to read this XMap. * @throws IOException if readind fails. */ private void readObject(ObjectInputStream in) throws IOException { int size = in.readInt(); if (size > 0) { byte[] content = new byte[size]; in.readFully(content); Document doc = null; try { doc = getXMLUtil().loadDocument(new ByteArrayInputStream(content)); } catch (Exception e) { IOException ioe = new IOException("Could not deserialize this XMap"); ioe.initCause(e); throw ioe; } if (doc != null && doc.getDocumentElement() != null) this.root = doc.getDocumentElement(); } } }