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