001: /**
002: * $RCSfile$
003: * $Revision: 9782 $
004: * $Date: 2008-01-19 09:43:30 -0800 (Sat, 19 Jan 2008) $
005: *
006: * Copyright (C) 2004 Jive Software. All rights reserved.
007: *
008: * This software is published under the terms of the GNU Public License (GPL),
009: * a copy of which is included in this distribution.
010: */package org.jivesoftware.util;
011:
012: import org.dom4j.CDATA;
013: import org.dom4j.Document;
014: import org.dom4j.Element;
015: import org.dom4j.Node;
016: import org.dom4j.io.OutputFormat;
017: import org.dom4j.io.SAXReader;
018: import org.apache.commons.lang.StringEscapeUtils;
019:
020: import java.io.*;
021: import java.util.*;
022:
023: /**
024: * Provides the the ability to use simple XML property files. Each property is
025: * in the form X.Y.Z, which would map to an XML snippet of:
026: * <pre>
027: * <X>
028: * <Y>
029: * <Z>someValue</Z>
030: * </Y>
031: * </X>
032: * </pre>
033: * <p/>
034: * The XML file is passed in to the constructor and must be readable and
035: * writtable. Setting property values will automatically persist those value
036: * to disk. The file encoding used is UTF-8.
037: *
038: * @author Derek DeMoro
039: * @author Iain Shigeoka
040: */
041: public class XMLProperties {
042:
043: private File file;
044: private Document document;
045:
046: /**
047: * Parsing the XML file every time we need a property is slow. Therefore,
048: * we use a Map to cache property values that are accessed more than once.
049: */
050: private Map<String, String> propertyCache = new HashMap<String, String>();
051:
052: /**
053: * Creates a new XMLPropertiesTest object.
054: *
055: * @param fileName the full path the file that properties should be read from
056: * and written to.
057: * @throws IOException if an error occurs loading the properties.
058: */
059: public XMLProperties(String fileName) throws IOException {
060: this (new File(fileName));
061: }
062:
063: /**
064: * Loads XML properties from a stream.
065: *
066: * @param in the input stream of XML.
067: * @throws IOException if an exception occurs when reading the stream.
068: */
069: public XMLProperties(InputStream in) throws IOException {
070: Reader reader = new BufferedReader(new InputStreamReader(in));
071: buildDoc(reader);
072: }
073:
074: /**
075: * Creates a new XMLPropertiesTest object.
076: *
077: * @param file the file that properties should be read from and written to.
078: * @throws IOException if an error occurs loading the properties.
079: */
080: public XMLProperties(File file) throws IOException {
081: this .file = file;
082: if (!file.exists()) {
083: // Attempt to recover from this error case by seeing if the
084: // tmp file exists. It's possible that the rename of the
085: // tmp file failed the last time Jive was running,
086: // but that it exists now.
087: File tempFile;
088: tempFile = new File(file.getParentFile(), file.getName()
089: + ".tmp");
090: if (tempFile.exists()) {
091: Log
092: .error("WARNING: "
093: + file.getName()
094: + " was not found, but temp file from "
095: + "previous write operation was. Attempting automatic recovery."
096: + " Please check file for data consistency.");
097: tempFile.renameTo(file);
098: }
099: // There isn't a possible way to recover from the file not
100: // being there, so throw an error.
101: else {
102: throw new FileNotFoundException(
103: "XML properties file does not exist: "
104: + file.getName());
105: }
106: }
107: // Check read and write privs.
108: if (!file.canRead()) {
109: throw new IOException(
110: "XML properties file must be readable: "
111: + file.getName());
112: }
113: if (!file.canWrite()) {
114: throw new IOException(
115: "XML properties file must be writable: "
116: + file.getName());
117: }
118:
119: FileReader reader = new FileReader(file);
120: buildDoc(reader);
121: }
122:
123: /**
124: * Returns the value of the specified property.
125: *
126: * @param name the name of the property to get.
127: * @return the value of the specified property.
128: */
129: public synchronized String getProperty(String name) {
130: String value = propertyCache.get(name);
131: if (value != null) {
132: return value;
133: }
134:
135: String[] propName = parsePropertyName(name);
136: // Search for this property by traversing down the XML heirarchy.
137: Element element = document.getRootElement();
138: for (String aPropName : propName) {
139: element = element.element(aPropName);
140: if (element == null) {
141: // This node doesn't match this part of the property name which
142: // indicates this property doesn't exist so return null.
143: return null;
144: }
145: }
146: // At this point, we found a matching property, so return its value.
147: // Empty strings are returned as null.
148: value = element.getTextTrim();
149: if ("".equals(value)) {
150: return null;
151: } else {
152: // Add to cache so that getting property next time is fast.
153: propertyCache.put(name, value);
154: return value;
155: }
156: }
157:
158: /**
159: * Return all values who's path matches the given property
160: * name as a String array, or an empty array if the if there
161: * are no children. This allows you to retrieve several values
162: * with the same property name. For example, consider the
163: * XML file entry:
164: * <pre>
165: * <foo>
166: * <bar>
167: * <prop>some value</prop>
168: * <prop>other value</prop>
169: * <prop>last value</prop>
170: * </bar>
171: * </foo>
172: * </pre>
173: * If you call getProperties("foo.bar.prop") will return a string array containing
174: * {"some value", "other value", "last value"}.
175: *
176: * @param name the name of the property to retrieve
177: * @return all child property values for the given node name.
178: */
179: public String[] getProperties(String name) {
180: String[] propName = parsePropertyName(name);
181: // Search for this property by traversing down the XML heirarchy,
182: // stopping one short.
183: Element element = document.getRootElement();
184: for (int i = 0; i < propName.length - 1; i++) {
185: element = element.element(propName[i]);
186: if (element == null) {
187: // This node doesn't match this part of the property name which
188: // indicates this property doesn't exist so return empty array.
189: return new String[] {};
190: }
191: }
192: // We found matching property, return names of children.
193: Iterator iter = element
194: .elementIterator(propName[propName.length - 1]);
195: List<String> props = new ArrayList<String>();
196: String value;
197: while (iter.hasNext()) {
198: // Empty strings are skipped.
199: value = ((Element) iter.next()).getTextTrim();
200: if (!"".equals(value)) {
201: props.add(value);
202: }
203: }
204: String[] childrenNames = new String[props.size()];
205: return props.toArray(childrenNames);
206: }
207:
208: /**
209: * Return all values who's path matches the given property
210: * name as a String array, or an empty array if the if there
211: * are no children. This allows you to retrieve several values
212: * with the same property name. For example, consider the
213: * XML file entry:
214: * <pre>
215: * <foo>
216: * <bar>
217: * <prop>some value</prop>
218: * <prop>other value</prop>
219: * <prop>last value</prop>
220: * </bar>
221: * </foo>
222: * </pre>
223: * If you call getProperties("foo.bar.prop") will return a string array containing
224: * {"some value", "other value", "last value"}.
225: *
226: * @param name the name of the property to retrieve
227: * @return all child property values for the given node name.
228: */
229: public Iterator getChildProperties(String name) {
230: String[] propName = parsePropertyName(name);
231: // Search for this property by traversing down the XML heirarchy,
232: // stopping one short.
233: Element element = document.getRootElement();
234: for (int i = 0; i < propName.length - 1; i++) {
235: element = element.element(propName[i]);
236: if (element == null) {
237: // This node doesn't match this part of the property name which
238: // indicates this property doesn't exist so return empty array.
239: return Collections.EMPTY_LIST.iterator();
240: }
241: }
242: // We found matching property, return values of the children.
243: Iterator iter = element
244: .elementIterator(propName[propName.length - 1]);
245: ArrayList<String> props = new ArrayList<String>();
246: while (iter.hasNext()) {
247: props.add(((Element) iter.next()).getText());
248: }
249: return props.iterator();
250: }
251:
252: /**
253: * Returns the value of the attribute of the given property name or <tt>null</tt>
254: * if it doesn't exist. Note, this
255: *
256: * @param name the property name to lookup - ie, "foo.bar"
257: * @param attribute the name of the attribute, ie "id"
258: * @return the value of the attribute of the given property or <tt>null</tt> if
259: * it doesn't exist.
260: */
261: public String getAttribute(String name, String attribute) {
262: if (name == null || attribute == null) {
263: return null;
264: }
265: String[] propName = parsePropertyName(name);
266: // Search for this property by traversing down the XML heirarchy.
267: Element element = document.getRootElement();
268: for (String child : propName) {
269: element = element.element(child);
270: if (element == null) {
271: // This node doesn't match this part of the property name which
272: // indicates this property doesn't exist so return empty array.
273: break;
274: }
275: }
276: if (element != null) {
277: // Get its attribute values
278: return element.attributeValue(attribute);
279: }
280: return null;
281: }
282:
283: /**
284: * Sets a property to an array of values. Multiple values matching the same property
285: * is mapped to an XML file as multiple elements containing each value.
286: * For example, using the name "foo.bar.prop", and the value string array containing
287: * {"some value", "other value", "last value"} would produce the following XML:
288: * <pre>
289: * <foo>
290: * <bar>
291: * <prop>some value</prop>
292: * <prop>other value</prop>
293: * <prop>last value</prop>
294: * </bar>
295: * </foo>
296: * </pre>
297: *
298: * @param name the name of the property.
299: * @param values the values for the property (can be empty but not null).
300: */
301: public void setProperties(String name, List<String> values) {
302: String[] propName = parsePropertyName(name);
303: // Search for this property by traversing down the XML heirarchy,
304: // stopping one short.
305: Element element = document.getRootElement();
306: for (int i = 0; i < propName.length - 1; i++) {
307: // If we don't find this part of the property in the XML heirarchy
308: // we add it as a new node
309: if (element.element(propName[i]) == null) {
310: element.addElement(propName[i]);
311: }
312: element = element.element(propName[i]);
313: }
314: String childName = propName[propName.length - 1];
315: // We found matching property, clear all children.
316: List<Element> toRemove = new ArrayList<Element>();
317: Iterator iter = element.elementIterator(childName);
318: while (iter.hasNext()) {
319: toRemove.add((Element) iter.next());
320: }
321: for (iter = toRemove.iterator(); iter.hasNext();) {
322: element.remove((Element) iter.next());
323: }
324: // Add the new children.
325: for (String value : values) {
326: Element childElement = element.addElement(childName);
327: if (value.startsWith("<![CDATA[")) {
328: Iterator it = childElement.nodeIterator();
329: while (it.hasNext()) {
330: Node node = (Node) it.next();
331: if (node instanceof CDATA) {
332: childElement.remove(node);
333: break;
334: }
335: }
336: childElement.addCDATA(value.substring(9,
337: value.length() - 3));
338: } else {
339: childElement
340: .setText(StringEscapeUtils.escapeXml(value));
341: }
342: }
343: saveProperties();
344:
345: // Generate event.
346: Map<String, Object> params = new HashMap<String, Object>();
347: params.put("value", values);
348: PropertyEventDispatcher.dispatchEvent(name,
349: PropertyEventDispatcher.EventType.xml_property_set,
350: params);
351: }
352:
353: /**
354: * Return all children property names of a parent property as a String array,
355: * or an empty array if the if there are no children. For example, given
356: * the properties <tt>X.Y.A</tt>, <tt>X.Y.B</tt>, and <tt>X.Y.C</tt>, then
357: * the child properties of <tt>X.Y</tt> are <tt>A</tt>, <tt>B</tt>, and
358: * <tt>C</tt>.
359: *
360: * @param parent the name of the parent property.
361: * @return all child property values for the given parent.
362: */
363: public String[] getChildrenProperties(String parent) {
364: String[] propName = parsePropertyName(parent);
365: // Search for this property by traversing down the XML heirarchy.
366: Element element = document.getRootElement();
367: for (String aPropName : propName) {
368: element = element.element(aPropName);
369: if (element == null) {
370: // This node doesn't match this part of the property name which
371: // indicates this property doesn't exist so return empty array.
372: return new String[] {};
373: }
374: }
375: // We found matching property, return names of children.
376: List children = element.elements();
377: int childCount = children.size();
378: String[] childrenNames = new String[childCount];
379: for (int i = 0; i < childCount; i++) {
380: childrenNames[i] = ((Element) children.get(i)).getName();
381: }
382: return childrenNames;
383: }
384:
385: /**
386: * Sets the value of the specified property. If the property doesn't
387: * currently exist, it will be automatically created.
388: *
389: * @param name the name of the property to set.
390: * @param value the new value for the property.
391: */
392: public synchronized void setProperty(String name, String value) {
393: if (!StringEscapeUtils.escapeXml(name).equals(name)) {
394: throw new IllegalArgumentException(
395: "Property name cannot contain XML entities.");
396: }
397: if (name == null) {
398: return;
399: }
400: if (value == null) {
401: value = "";
402: }
403:
404: // Set cache correctly with prop name and value.
405: propertyCache.put(name, value);
406:
407: String[] propName = parsePropertyName(name);
408: // Search for this property by traversing down the XML heirarchy.
409: Element element = document.getRootElement();
410: for (String aPropName : propName) {
411: // If we don't find this part of the property in the XML heirarchy
412: // we add it as a new node
413: if (element.element(aPropName) == null) {
414: element.addElement(aPropName);
415: }
416: element = element.element(aPropName);
417: }
418: // Set the value of the property in this node.
419: if (value.startsWith("<![CDATA[")) {
420: Iterator it = element.nodeIterator();
421: while (it.hasNext()) {
422: Node node = (Node) it.next();
423: if (node instanceof CDATA) {
424: element.remove(node);
425: break;
426: }
427: }
428: element.addCDATA(value.substring(9, value.length() - 3));
429: } else {
430: element.setText(value);
431: }
432: // Write the XML properties to disk
433: saveProperties();
434:
435: // Generate event.
436: Map<String, Object> params = new HashMap<String, Object>();
437: params.put("value", value);
438: PropertyEventDispatcher.dispatchEvent(name,
439: PropertyEventDispatcher.EventType.xml_property_set,
440: params);
441: }
442:
443: /**
444: * Deletes the specified property.
445: *
446: * @param name the property to delete.
447: */
448: public synchronized void deleteProperty(String name) {
449: // Remove property from cache.
450: propertyCache.remove(name);
451:
452: String[] propName = parsePropertyName(name);
453: // Search for this property by traversing down the XML heirarchy.
454: Element element = document.getRootElement();
455: for (int i = 0; i < propName.length - 1; i++) {
456: element = element.element(propName[i]);
457: // Can't find the property so return.
458: if (element == null) {
459: return;
460: }
461: }
462: // Found the correct element to remove, so remove it...
463: element.remove(element.element(propName[propName.length - 1]));
464: // .. then write to disk.
465: saveProperties();
466:
467: // Generate event.
468: Map<String, Object> params = Collections.emptyMap();
469: PropertyEventDispatcher.dispatchEvent(name,
470: PropertyEventDispatcher.EventType.xml_property_deleted,
471: params);
472: }
473:
474: /**
475: * Builds the document XML model up based the given reader of XML data.
476: * @param in the input stream used to build the xml document
477: * @throws java.io.IOException thrown when an error occurs reading the input stream.
478: */
479: private void buildDoc(Reader in) throws IOException {
480: try {
481: SAXReader xmlReader = new SAXReader();
482: xmlReader.setEncoding("UTF-8");
483: document = xmlReader.read(in);
484: } catch (Exception e) {
485: Log.error("Error reading XML properties", e);
486: throw new IOException(e.getMessage());
487: } finally {
488: if (in != null) {
489: in.close();
490: }
491: }
492: }
493:
494: /**
495: * Saves the properties to disk as an XML document. A temporary file is
496: * used during the writing process for maximum safety.
497: */
498: private synchronized void saveProperties() {
499: boolean error = false;
500: // Write data out to a temporary file first.
501: File tempFile = null;
502: Writer writer = null;
503: try {
504: tempFile = new File(file.getParentFile(), file.getName()
505: + ".tmp");
506: writer = new BufferedWriter(new OutputStreamWriter(
507: new FileOutputStream(tempFile), "UTF-8"));
508: OutputFormat prettyPrinter = OutputFormat
509: .createPrettyPrint();
510: XMLWriter xmlWriter = new XMLWriter(writer, prettyPrinter);
511: xmlWriter.write(document);
512: } catch (Exception e) {
513: Log.error(e);
514: // There were errors so abort replacing the old property file.
515: error = true;
516: } finally {
517: if (writer != null) {
518: try {
519: writer.close();
520: } catch (IOException e1) {
521: Log.error(e1);
522: error = true;
523: }
524: }
525: }
526:
527: // No errors occured, so delete the main file.
528: if (!error) {
529: // Delete the old file so we can replace it.
530: if (!file.delete()) {
531: Log.error("Error deleting property file: "
532: + file.getAbsolutePath());
533: return;
534: }
535: // Copy new contents to the file.
536: try {
537: copy(tempFile, file);
538: } catch (Exception e) {
539: Log.error(e);
540: // There were errors so abort replacing the old property file.
541: error = true;
542: }
543: // If no errors, delete the temp file.
544: if (!error) {
545: tempFile.delete();
546: }
547: }
548: }
549:
550: /**
551: * Returns an array representation of the given Jive property. Jive
552: * properties are always in the format "prop.name.is.this" which would be
553: * represented as an array of four Strings.
554: *
555: * @param name the name of the Jive property.
556: * @return an array representation of the given Jive property.
557: */
558: private String[] parsePropertyName(String name) {
559: List<String> propName = new ArrayList<String>(5);
560: // Use a StringTokenizer to tokenize the property name.
561: StringTokenizer tokenizer = new StringTokenizer(name, ".");
562: while (tokenizer.hasMoreTokens()) {
563: propName.add(tokenizer.nextToken());
564: }
565: return propName.toArray(new String[propName.size()]);
566: }
567:
568: public void setProperties(Map<String, String> propertyMap) {
569: for (String propertyName : propertyMap.keySet()) {
570: String propertyValue = propertyMap.get(propertyName);
571: setProperty(propertyName, propertyValue);
572: }
573: }
574:
575: /**
576: * Copies the inFile to the outFile.
577: *
578: * @param inFile The file to copy from
579: * @param outFile The file to copy to
580: * @throws IOException If there was a problem making the copy
581: */
582: private static void copy(File inFile, File outFile)
583: throws IOException {
584: FileInputStream fin = null;
585: FileOutputStream fout = null;
586: try {
587: fin = new FileInputStream(inFile);
588: fout = new FileOutputStream(outFile);
589: copy(fin, fout);
590: } finally {
591: try {
592: if (fin != null)
593: fin.close();
594: } catch (IOException e) {
595: // do nothing
596: }
597: try {
598: if (fout != null)
599: fout.close();
600: } catch (IOException e) {
601: // do nothing
602: }
603: }
604: }
605:
606: /**
607: * Copies data from an input stream to an output stream
608: *
609: * @param in the stream to copy data from.
610: * @param out the stream to copy data to.
611: * @throws IOException if there's trouble during the copy.
612: */
613: private static void copy(InputStream in, OutputStream out)
614: throws IOException {
615: // Do not allow other threads to intrude on streams during copy.
616: synchronized (in) {
617: synchronized (out) {
618: byte[] buffer = new byte[256];
619: while (true) {
620: int bytesRead = in.read(buffer);
621: if (bytesRead == -1)
622: break;
623: out.write(buffer, 0, bytesRead);
624: }
625: }
626: }
627: }
628: }
|