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
021 package org.granite.util;
022
023 import java.io.ByteArrayInputStream;
024 import java.io.ByteArrayOutputStream;
025 import java.io.IOException;
026 import java.io.InputStream;
027 import java.io.ObjectInputStream;
028 import java.io.ObjectOutputStream;
029 import java.io.Serializable;
030 import java.util.ArrayList;
031 import java.util.List;
032
033 import org.granite.logging.Logger;
034 import org.w3c.dom.Attr;
035 import org.w3c.dom.Document;
036 import org.w3c.dom.Element;
037 import org.w3c.dom.Node;
038 import org.xml.sax.EntityResolver;
039 import 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 */
048 public 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 }