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