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