001/* 002 GRANITE DATA SERVICES 003 Copyright (C) 2011 GRANITE DATA SERVICES S.A.S. 004 005 This file is part of Granite Data Services. 006 007 Granite Data Services is free software; you can redistribute it and/or modify 008 it under the terms of the GNU Library General Public License as published by 009 the Free Software Foundation; either version 2 of the License, or (at your 010 option) any later version. 011 012 Granite Data Services is distributed in the hope that it will be useful, but 013 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 014 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License 015 for more details. 016 017 You should have received a copy of the GNU Library General Public License 018 along with this library; if not, see <http://www.gnu.org/licenses/>. 019*/ 020 021package org.granite.util; 022 023import java.io.ByteArrayInputStream; 024import java.io.ByteArrayOutputStream; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.ObjectInputStream; 028import java.io.ObjectOutputStream; 029import java.io.Serializable; 030import java.util.ArrayList; 031import java.util.List; 032 033import org.granite.logging.Logger; 034import org.w3c.dom.Attr; 035import org.w3c.dom.Document; 036import org.w3c.dom.Element; 037import org.w3c.dom.Node; 038import org.xml.sax.EntityResolver; 039import org.xml.sax.SAXException; 040 041/** 042 * Utility class that makes XML fragment tree manipulation easier. 043 * <br /> 044 * This class relies on JDK DOM & XPath built-in implementations. 045 * 046 * @author Franck WOLFF 047 */ 048public class XMap implements Serializable { 049 050 private static final Logger log = Logger.getLogger(XMap.class); 051 052 private static final long serialVersionUID = 1L; 053 054 protected static final String DEFAULT_ROOT_NAME = "root"; 055 056 /** 057 * An empty and unmodifiable XMap instance. 058 */ 059 public static final XMap EMPTY_XMAP = new XMap(null, null, false) { 060 061 private static final long serialVersionUID = 1L; 062 063 @Override 064 public String put(String key, String value) { 065 throw new RuntimeException("Immutable XMap"); 066 } 067 068 @Override 069 public String remove(String key) { 070 throw new RuntimeException("Immutable XMap"); 071 } 072 }; 073 074 private transient Element root = null; 075 private transient XMLUtil xmlUtil = null; 076 077 /** 078 * Constructs a new XMap instance. 079 */ 080 public XMap() { 081 this(null, null, false); 082 } 083 084 /** 085 * Constructs a new XMap instance. 086 * 087 * @param root the name of the root element (may be null). 088 */ 089 public XMap(String root) { 090 if (root != null) { 091 this.root = getXMLUtil().newDocument(root).getDocumentElement(); 092 } 093 } 094 095 /** 096 * Constructs a new XMap instance from an XML input stream. 097 * 098 * @param input an XML input stream. 099 */ 100 public XMap(InputStream input) throws IOException, SAXException { 101 this.root = getXMLUtil().loadDocument(input).getDocumentElement(); 102 } 103 104 /** 105 * Constructs a new XMap instance from an XML input stream. 106 * 107 * @param input an XML input stream. 108 */ 109 public XMap(InputStream input, EntityResolver resolver) throws IOException, SAXException { 110 this.root = getXMLUtil().loadDocument(input, resolver, null).getDocumentElement(); 111 } 112 113 114 /** 115 * Constructs a new XMap instance. 116 * 117 * @param root a DOM element (may be null). 118 */ 119 public XMap(Element root) { 120 this(null, root, true); 121 } 122 123 /** 124 * Constructs a new XMap instance based on an existing XMap and clone its content. 125 * 126 * @param map the map to duplicate (root element is cloned so modification to this 127 * new instance won't modify the original XMap). 128 */ 129 public XMap(XMap map) { 130 this((map == null ? null : map.xmlUtil), (map == null ? null : map.root), true); 131 } 132 133 /** 134 * Constructs a new XMap instance. 135 * 136 * @param root the root element (may be null). 137 * @param clone should we clone the root element (prevent original node modification). 138 */ 139 protected XMap(XMLUtil xmlUtil, Element root, boolean clone) { 140 this.xmlUtil = xmlUtil; 141 this.root = (clone && root != null ? (Element)root.cloneNode(true) : root); 142 143 } 144 145 private XMLUtil getXMLUtil() { 146 if (xmlUtil == null) 147 xmlUtil = XMLUtilFactory.getXMLUtil(); 148 return xmlUtil; 149 } 150 151 /** 152 * Allows direct manipulation of the root element. 153 * 154 * @return the root element of this XMap instance. 155 */ 156 public Element getRoot() { 157 return root; 158 } 159 160 /** 161 * Returns true if the supplied key XPath expression matches at least one element, attribute 162 * or text in the root element of this XMap. 163 * 164 * @param key an XPath expression. 165 * @return true if the supplied key XPath expression matches at least one element, attribute 166 * or text in the root element of this XMap, false otherwise. 167 * @throws RuntimeException if the XPath expression isn't correct. 168 */ 169 public boolean containsKey(String key) { 170 if (root == null) 171 return false; 172 try { 173 Node result = getXMLUtil().selectSingleNode(root, key); 174 return ( 175 result != null && ( 176 result.getNodeType() == Node.ELEMENT_NODE || 177 result.getNodeType() == Node.TEXT_NODE || 178 result.getNodeType() == Node.ATTRIBUTE_NODE 179 ) 180 ); 181 } catch (Exception e) { 182 throw new RuntimeException(e); 183 } 184 } 185 186 /** 187 * Returns the text value of the element (or attribute or text) that matches the supplied 188 * XPath expression. 189 * 190 * @param key an XPath expression. 191 * @return the text value of the matched element or null if the element does not exist or have 192 * no value. 193 * @throws RuntimeException if the XPath expression isn't correct. 194 */ 195 public String get(String key) { 196 if (root == null) 197 return null; 198 try { 199 return getXMLUtil().getNormalizedValue(getXMLUtil().selectSingleNode(root, key)); 200 } catch (Exception e) { 201 throw new RuntimeException(e); 202 } 203 } 204 205 public <T> T get(String key, Class<T> clazz, T defaultValue) { 206 return get(key, clazz, defaultValue, false, true); 207 } 208 209 public <T> T get(String key, Class<T> clazz, T defaultValue, boolean required, boolean warn) { 210 211 String sValue = get(key); 212 213 if (required && sValue == null) 214 throw new RuntimeException(key + " value is required in XML file:\n" + toString()); 215 216 Object oValue = defaultValue; 217 218 boolean unsupported = false; 219 if (sValue != null) { 220 try { 221 if (clazz == String.class) 222 oValue = sValue; 223 else if (clazz == Integer.class || clazz == Integer.TYPE) 224 oValue = Integer.valueOf(sValue); 225 else if (clazz == Long.class || clazz == Long.TYPE) 226 oValue = Long.valueOf(sValue); 227 else if (clazz == Boolean.class || clazz == Boolean.TYPE) { 228 if (!Boolean.TRUE.toString().equalsIgnoreCase(sValue) && !Boolean.FALSE.toString().equalsIgnoreCase(sValue)) 229 throw new NumberFormatException(sValue); 230 oValue = Boolean.valueOf(sValue); 231 } 232 else if (clazz == Double.class || clazz == Double.TYPE) 233 oValue = Double.valueOf(sValue); 234 else if (clazz == Float.class || clazz == Float.TYPE) 235 oValue = Float.valueOf(sValue); 236 else if (clazz == Short.class || clazz == Short.TYPE) 237 oValue = Short.valueOf(sValue); 238 else if (clazz == Byte.class || clazz == Byte.TYPE) 239 oValue = Byte.valueOf(sValue); 240 else 241 unsupported = true; 242 } 243 catch (Exception e) { 244 if (warn) 245 log.warn(e, "Illegal %s value for %s=%s (using default: %s)", clazz.getSimpleName(), key, sValue, defaultValue); 246 } 247 } 248 249 if (unsupported) 250 throw new UnsupportedOperationException("Unsupported value type: " + clazz.getName()); 251 252 @SuppressWarnings("unchecked") 253 T tValue = (T)oValue; 254 255 return tValue; 256 } 257 258 /** 259 * Returns a list of XMap instances with all elements that match the 260 * supplied XPath expression. Note that XPath result nodes that are not instance of 261 * Element are ignored. Note also that returned XMaps contain original child elements of 262 * the root element of this XMap so modifications made to child elements affect this XMap 263 * instance as well. 264 * 265 * @param key an XPath expression. 266 * @return an unmodifiable list of XMap instances. 267 * @throws RuntimeException if the XPath expression isn't correct. 268 */ 269 public List<XMap> getAll(String key) { 270 if (root == null) 271 return new ArrayList<XMap>(0); 272 try { 273 List<Node> result = getXMLUtil().selectNodeSet(root, key); 274 List<XMap> xMaps = new ArrayList<XMap>(result.size()); 275 for (Node node : result) { 276 if (node.getNodeType() == Node.ELEMENT_NODE) 277 xMaps.add(new XMap(this.xmlUtil, (Element)node, false)); 278 } 279 return xMaps; 280 } catch (Exception e) { 281 throw new RuntimeException(e); 282 } 283 } 284 285 /** 286 * Returns a new XMap instance with the first element that matches the 287 * supplied XPath expression or null if this XMap root element is null, or if XPath evaluation 288 * result is null, or this result is not an Element. Returned XMap contains original child element of 289 * the root element of this XMap so modifications made to the child element affect this XMap 290 * instance as well. 291 * 292 * @param key an XPath expression. 293 * @return a single new XMap instance. 294 * @throws RuntimeException if the XPath expression isn't correct. 295 */ 296 public XMap getOne(String key) { 297 if (root == null) 298 return null; 299 try { 300 Node node = getXMLUtil().selectSingleNode(root, key); 301 if (node == null || node.getNodeType() != Node.ELEMENT_NODE) 302 return null; 303 return new XMap(xmlUtil, (Element)node, false); 304 } catch (Exception e) { 305 throw new RuntimeException(e); 306 } 307 } 308 309 /** 310 * Creates or updates the text value of the element (or text or attribute) matched by 311 * the supplied XPath expression. If the matched element (or text or attribute) does not exist, 312 * it is created with the last segment of the XPath expression (but its parent must already exist). 313 * 314 * @param key an XPath expression. 315 * @param value the value to set (may be null). 316 * @return the previous value of the matched element (may be null). 317 * @throws RuntimeException if the root element of this XMap is null, if the XPath expression is not valid, 318 * or (creation case) if the parent node does not exist or is not an element instance. 319 */ 320 public String put(String key, String value) { 321 return put(key, value, false); 322 } 323 324 /** 325 * Creates or updates the text value of the element (or text or attribute) matched by 326 * the supplied XPath expression. If the matched element (or text or attribute) does not exist or if append 327 * is <tt>true</tt>, it is created with the last segment of the XPath expression (but its parent must already 328 * exist). 329 * 330 * @param key an XPath expression. 331 * @param value the value to set (may be null). 332 * @param append should the new element be appended (created) next to a possibly existing element(s) of 333 * the same name? 334 * @return the previous value of the matched element (may be null). 335 * @throws RuntimeException if the root element of this XMap is null, if the XPath expression is not valid, 336 * or (creation case) if the parent node does not exist or is not an element instance. 337 */ 338 public String put(String key, String value, boolean append) { 339 if (root == null) 340 root = getXMLUtil().newDocument(DEFAULT_ROOT_NAME).getDocumentElement(); 341 342 if (!append) { 343 try { 344 Node selectResult = getXMLUtil().selectSingleNode(root, key); 345 if (selectResult != null) 346 return getXMLUtil().setValue(selectResult, value); 347 } catch(RuntimeException e) { 348 throw e; 349 } catch(Exception e) { 350 throw new RuntimeException(e); 351 } 352 } 353 354 Element parent = root; 355 String name = key; 356 357 int iLastSlash = key.lastIndexOf('/'); 358 if (iLastSlash != -1) { 359 name = key.substring(iLastSlash + 1); 360 Node selectResult = null; 361 try { 362 selectResult = getXMLUtil().selectSingleNode(root, key.substring(0, iLastSlash)); 363 } catch (Exception e) { 364 throw new RuntimeException(e); 365 } 366 if (selectResult == null) 367 throw new RuntimeException("Parent node does not exist: " + key.substring(0, iLastSlash)); 368 if (!(selectResult instanceof Element)) 369 throw new RuntimeException("Parent node must be an Element: " + key.substring(0, iLastSlash) + " -> " + selectResult); 370 parent = (Element)selectResult; 371 } 372 373 if (name.length() > 0 && name.charAt(0) == '@') 374 parent.setAttribute(name.substring(1), value); 375 else 376 getXMLUtil().newElement(parent, name, value); 377 378 return null; 379 } 380 381 /** 382 * Removes the element, text or attribute that matches the supplied XPath expression. 383 * 384 * @param key an XPath expression. 385 * @return the previous value of the matched node if any. 386 * @throws RuntimeException if the XPath expression isn't valid. 387 */ 388 public String remove(String key) { 389 if (root == null) 390 return null; 391 try { 392 Node node = getXMLUtil().selectSingleNode(root, key); 393 if (node != null) { 394 String value = getXMLUtil().getNormalizedValue(node); 395 if (node.getNodeType() == Node.ATTRIBUTE_NODE) 396 ((Attr)node).getOwnerElement().removeAttribute(node.getNodeName()); 397 else 398 node.getParentNode().removeChild(node); 399 return value; 400 } 401 } catch(Exception e) { 402 throw new RuntimeException(e); 403 } 404 return null; 405 } 406 407 /** 408 * Returns a "pretty" XML representation of the root element of this XMap (may be null). 409 * 410 * @return a "pretty" XML representation of the root element of this XMap (may be null). 411 */ 412 @Override 413 public String toString() { 414 return getXMLUtil().toNodeString(root); 415 } 416 417 /** 418 * Write java.io.Serializable method. 419 * 420 * @param out the ObjectOutputStream where to write this XMap. 421 * @throws IOException if writing fails. 422 */ 423 private void writeObject(ObjectOutputStream out) throws IOException { 424 if (root == null) 425 out.writeInt(0); 426 else { 427 ByteArrayOutputStream output = new ByteArrayOutputStream(); 428 try { 429 getXMLUtil().saveDocument(root.getOwnerDocument(), output); 430 } 431 catch (Exception e) { 432 IOException ioe = new IOException("Could not serialize this XMap"); 433 ioe.initCause(e); 434 throw ioe; 435 } 436 out.writeInt(output.size()); 437 out.write(output.toByteArray()); 438 } 439 } 440 441 /** 442 * Read java.io.Serializable method. 443 * 444 * @param in the ObjectInputStream from which to read this XMap. 445 * @throws IOException if readind fails. 446 */ 447 @SuppressWarnings("unused") 448 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 449 int size = in.readInt(); 450 if (size > 0) { 451 byte[] content = new byte[size]; 452 in.readFully(content); 453 Document doc = null; 454 try { 455 doc = getXMLUtil().loadDocument(new ByteArrayInputStream(content)); 456 } catch (Exception e) { 457 IOException ioe = new IOException("Could not deserialize this XMap"); 458 ioe.initCause(e); 459 throw ioe; 460 } 461 if (doc != null && doc.getDocumentElement() != null) 462 this.root = doc.getDocumentElement(); 463 } 464 } 465}