Development Class Java

/*BEGIN_COPYRIGHT_BLOCK
 *
 * Copyright (c) 2001-2007, JavaPLT group at Rice University (javaplt@rice.edu)
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software is Open Source Initiative approved Open Source Software.
 * Open Source Initative Approved is a trademark of the Open Source Initiative.
 * 
 * This file is part of DrJava.  Download the current version of this project
 * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
 * 
 * END_COPYRIGHT_BLOCK*/
import org.w3c.dom.*;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.util.*;
/**
 * XML configuration management.
 * 


 * This class uses DOM paths of a specific form to refer to nodes in the XML document.
 * Consider this XML structure:
 * 
 *   abc
 *   def
 * 
 * The path "foo/bar" refers to the value "abc".
 * The path "foo/fum" refers to the value "def".
 * If this form is used, there may be only #text or #comment nodes in the node. All #text nodes will be
 * concatenated and then stripped of whitespace at the beginning and the end.
 * The path "foo/fum.fee" refers to the value "xyz".
 * The path "foo.a" refers to the value "foo.a".
 *
 * When using getMultiple, any node or attribute name can be substituted with "*" to get all elements:
 * The path "foo/*" returns both the value "abc" and "def".
 * @author Mathias Ricken
 */
public class XMLConfig {
  /** Newline string.
   */
  public static final String NL = System.getProperty("line.separator");
  
  /** XML document.
   */
  private Document _document;
  
  /** XMLConfig to delegate to, or null.
   */
  private XMLConfig _parent = null;
  
  /** Node where this XMLConfig starts if delegation is used, or null.
   */
  private Node _startNode = null;
  
  /** Creates an empty configuration.
   */
  public XMLConfig() {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    try {
      DocumentBuilder builder = factory.newDocumentBuilder();
      _document = builder.newDocument();  // Create from whole cloth
      // NOTE: not 1.4 compatible -- _document.setXmlStandalone(true);
    }
    catch(ParserConfigurationException e) {
      e.printStackTrace();
    }
  }
  
  /** Creates a configuration from an input stream.
   * @param is input stream
   */
  public XMLConfig(InputStream is) {
    init(new InputSource(is));
  }
  
  /** Creates a configuration from a reader.
   * @param r reader
   */
  public XMLConfig(Reader r) {
    init(new InputSource(r));
  }
  
  /** Creates a configuration that is a part of another configuration, starting at the specified node.
   * @param parent the configuration that contains this part
   * @param node the node in the parent configuration where this part starts
   */
  public XMLConfig(XMLConfig parent, Node node) {
    if ((parent==null) || (node==null)) { throw new XMLConfigException("Error in ctor: parent or node is null"); }
    _parent = parent;
    _startNode = node;
    _document = null;
  }
  
  /** Initialize this XML configuration.
   * @param is the XML input source
   */
  private void init(InputSource is) {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    DocumentBuilder builder = null;
    try {
      builder = factory.newDocumentBuilder();
      _document = builder.parse(is);
      // NOTE: not 1.4 compatible -- _document.setXmlStandalone(true);
    }
    catch(Exception e) {
      throw new XMLConfigException("Error in ctor", e);
    }
    _document.normalize();
  }
  
  /** Creates a configuration from a file.
   * @param f file
   */
  public XMLConfig(File f) {
    try {
      init(new InputSource(new FileInputStream(f)));
    }
    catch(FileNotFoundException e) {
      throw new XMLConfigException("Error in ctor", e);
    }
  }
  
  /** Creates a configuration from a file name.
   * @param filename file name
   */
  public XMLConfig(String filename)  {
    try {
      init(new InputSource(new FileInputStream(filename)));
    }
    catch(FileNotFoundException e) {
      throw new XMLConfigException("Error in ctor", e);
    }
  }
  
  public boolean isDelegated() { return (_parent!=null); }
  
  /** Saves configuration to an output stream
   * @param os output stream
   */
  public void save(OutputStream os) {
    if (isDelegated()) { _parent.save(os); return; }
    
    // Prepare the DOM document for writing
    Source source = new DOMSource(_document);
    /*
     // Prepare the output file
     Result result = new StreamResult(os);
     */
    // Write the DOM document to the file
    try {
      TransformerFactory tf = TransformerFactory.newInstance();
      tf.setAttribute("indent-number", Integer.valueOf(2));
      Transformer t = tf.newTransformer();
      t.setOutputProperty(OutputKeys.INDENT, "yes");
      t.transform(source, new StreamResult(new OutputStreamWriter(os, "utf-8")));
      /*            
       Transformer xformer = TransformerFactory.newInstance().newTransformer();
       xformer.setOutputProperty(OutputKeys.INDENT, "yes");
       xformer.transform(source, result);
       */
    }
    catch(TransformerException e) {
      throw new XMLConfigException("Error in save", e);
    }
    catch(UnsupportedEncodingException e) {
      throw new XMLConfigException("Error in save", e);
    }
  }
  
  /** Saves configuration to a file.
   * @param f file
   */
  public void save(File f) {
    if (isDelegated()) { _parent.save(f); return; }
    FileOutputStream fos = null;
    try {
      fos = new FileOutputStream(f);
      save(fos);
    }
    catch(FileNotFoundException e) {
      throw new XMLConfigException("Error in save", e);
    }
    finally {
      try {
        if (fos!=null) fos.close();
      }
      catch(IOException ioe) { /* ignore exception when closing */ }
    }
  }
  
  /** Saves configuration to a file specified by a file name.
   * @param filename file name
   */
  public void save(String filename) {
    save(new File(filename));
  }
  
  // ----- String ------
  
  /** Returns the value as specified by the DOM path.
   * @param path DOM path
   * @return value.
   */
  public String get(String path) {
    List r = getMultiple(path);
    if (r.size()!=1) throw new XMLConfigException("Number of results != 1");
    return r.get(0);
  }
  /** Returns the value as specified by the DOM path.
   * @param path DOM path
   * @param root node where the search should start
   * @return value.
   */
  public String get(String path, Node root) {
    List r = getMultiple(path, root);
    if (r.size()!=1) throw new XMLConfigException("Number of results != 1");
    return r.get(0);
  }
  
    /** Returns the value as specified by the DOM path, or the default value if the value could not be found.
   * @param path DOM path
   * @param defaultVal default value in case value is not in DOM
   * @return value.
   */
  public String get(String path, String defaultVal) {
    try {
      return get(path);
    }
    catch(XMLConfigException e) {
      return defaultVal;
    }
  }
  
  /** Returns the value as specified by the DOM path, or the default value if the value could not be found.
   * @param path DOM path
   * @param root node where the search should start
   * @param defaultVal default value in case value is not in DOM
   * @return value.
   */
  public String get(String path, Node root, String defaultVal) {
    try {
      return get(path, root);
    }
    catch(XMLConfigException e) {
      return defaultVal;
    }
  }
  
  // ----- Integer ------
  
  /** Returns the value as specified by the DOM path.
   * @param path DOM path
   * @return value.
   * @throws IllegalArgumentException
   */
  public int getInt(String path) {
    List r = getMultiple(path);
    if (r.size()!=1) throw new XMLConfigException("Number of results != 1");
    try {
      return Integer.valueOf(r.get(0));
    }
    catch(NumberFormatException nfe) { throw new IllegalArgumentException("Not an integer value.", nfe); }
  }
  /** Returns the value as specified by the DOM path.
   * @param path DOM path
   * @param root node where the search should start
   * @return value.
   * @throws IllegalArgumentException
   */
  public int getInt(String path, Node root) {
    List r = getMultiple(path, root);
    if (r.size()!=1) throw new XMLConfigException("Number of results != 1");
    try {
      return Integer.valueOf(r.get(0));
    }
    catch(NumberFormatException nfe) { throw new IllegalArgumentException("Not an integer value.", nfe); }
  }
  
  /** Returns the value as specified by the DOM path, or the default value if the value could not be found.
   * @param path DOM path
   * @param defaultVal default value in case value is not in DOM
   * @return value.
   * @throws IllegalArgumentException
   */
  public int getInt(String path, int defaultVal) {
    try {
      return getInt(path);
    }
    catch(XMLConfigException e) {
      return defaultVal;
    }
  }
  
  /** Returns the value as specified by the DOM path, or the default value if the value could not be found.
   * @param path DOM path
   * @param root node where the search should start
   * @param defaultVal default value in case value is not in DOM
   * @return value.
   * @throws IllegalArgumentException
   */
  public int getInt(String path, Node root, int defaultVal) {
    try {
      return getInt(path, root);
    }
    catch(XMLConfigException e) {
      return defaultVal;
    }
  }
  // ----- Boolean ------
  
  /** Returns the value as specified by the DOM path.
   * @param path DOM path
   * @return value.
   * @throws IllegalArgumentException
   */
  public boolean getBool(String path) {
    List r = getMultiple(path);
    if (r.size()!=1) throw new XMLConfigException("Number of results != 1");
    String s = r.get(0).toLowerCase().trim();
    if ((s.equals("true")) ||
        (s.equals("yes")) ||
        (s.equals("on"))) return true;
    if ((s.equals("false")) ||
        (s.equals("no")) ||
        (s.equals("off"))) return false;
    throw new IllegalArgumentException("Not a Boolean vlaue.");
  }
  /** Returns the value as specified by the DOM path.
   * @param path DOM path
   * @param root node where the search should start
   * @return value.
   * @throws IllegalArgumentException
   */
  public boolean getBool(String path, Node root) {
    List r = getMultiple(path, root);
    if (r.size()!=1) throw new XMLConfigException("Number of results != 1");
    String s = r.get(0).toLowerCase().trim();
    if ((s.equals("true")) ||
        (s.equals("yes")) ||
        (s.equals("on"))) return true;
    if ((s.equals("false")) ||
        (s.equals("no")) ||
        (s.equals("off"))) return false;
    throw new IllegalArgumentException("Not a Boolean vlaue.");
  }
  
  /** Returns the value as specified by the DOM path, or the default value if the value could not be found.
   * @param path DOM path
   * @param defaultVal default value in case value is not in DOM
   * @return value.
   * @throws IllegalArgumentException
   */
  public boolean getBool(String path, boolean defaultVal) {
    try {
      return getBool(path);
    }
    catch(XMLConfigException e) {
      return defaultVal;
    }
  }
  
  /** Returns the value as specified by the DOM path, or the default value if the value could not be found.
   * @param path DOM path
   * @param root node where the search should start
   * @param defaultVal default value in case value is not in DOM
   * @return value.
   * @throws IllegalArgumentException
   */
  public boolean getBool(String path, Node root, boolean defaultVal) {
    try {
      return getBool(path, root);
    }
    catch(XMLConfigException e) {
      return defaultVal;
    }
  }
  
  // ----- Other -----
  
  /** Returns the value as specified by the DOM path.
   * @param path DOM path
   * @return list of values.
   */
  public List getMultiple(String path) {
    if (isDelegated()) { return getMultiple(path, _startNode); }
    return getMultiple(path, _document);
  }
  
  /** Returns the value as specified by the DOM path.
   * @param path DOM path
   * @param root node where the search should start
   * @return list of values.
   */
  public List getMultiple(String path, Node root) {
    List accum = getNodes(path, root);
    List strings = new LinkedList();
    for(Node n: accum) {
      if (n instanceof Attr) {
        strings.add(n.getNodeValue());
      }
      else {
        Node child;
        String acc = "";
        child = n.getFirstChild();
        while(child!=null) {
          if (child.getNodeName().equals("#text")) {
            acc += " " + child.getNodeValue();
          }
          else if (child.getNodeName().equals("#comment")) {
            // ignore
          }
          else {
            throw new XMLConfigException("Node "+n.getNodeName()+" contained node "+child.getNodeName()+", but should only contain #text and #comment.");
          }
          child = child.getNextSibling();
        }
        strings.add(acc.trim());
      }
    }
    return strings;
  }
  
  /** Returns the nodes as specified by the DOM path.
   * @param path DOM path
   * @return list of nodes.
   */
  public List getNodes(String path) {
    if (isDelegated()) { return getNodes(path, _startNode); }
    return getNodes(path, _document);
  }
  
  /** Returns the nodes as specified by the DOM path.
   * @param path DOM path
   * @param root node where the search should start
   * @return list of nodes.
   */
  public List getNodes(String path, Node root) {
    List accum = new LinkedList();
    getMultipleHelper(path, root, accum, false);
    return accum;
  }
  
  /** Returns the value as specified by the DOM path.
   * @param path DOM path
   * @param n node where the search begins
   * @param accum accumulator
   * @param dotRead whether a dot has been read
   */
  private void getMultipleHelper(String path, Node n, List accum, boolean dotRead) {
    int dotPos = path.indexOf('.');
    boolean initialDot = (dotPos==0);
    if ((path.length()>0) && (dotPos == -1) && (!path.endsWith("/"))) {
      path = path + "/";
    }
    int slashPos = path.indexOf('/');
    
    if(dotPos != -1 && path.indexOf('.', dotPos+1) != -1)
      throw new XMLConfigException("An attribute cannot have subparts (foo.bar.fum and foo.bar/fum not allowed)");
    
    if(dotPos != -1 && path.indexOf('/', dotPos+1) != -1)
      throw new XMLConfigException("An attribute cannot have subparts (foo.bar.fum and foo.bar/fum not allowed)");
    
    if (((slashPos > -1) || (dotPos > -1)) && !dotRead || initialDot)  {
      String nodeName;
      if ((slashPos > -1) && ((dotPos == -1) || (slashPos < dotPos))) {
        nodeName = path.substring(0, slashPos);
        path = path.substring(slashPos+1);
      }
      else {
        if (slashPos > -1) {
          throw new XMLConfigException("An attribute cannot have subparts (foo.bar.fum and foo.bar/fum not allowed)");
        }
        if (!initialDot) {
          nodeName = path.substring(0, dotPos);
          path = path.substring(dotPos+1);
          dotRead = true;
        }
        else {
          path = path.substring(1);
          getMultipleAddAttributesHelper(path, n, accum);
          return;
        }
      }
      Node child = n.getFirstChild();
      if (nodeName.equals("*")) {
        while(child!=null) {
          if (!child.getNodeName().equals("#text") && !child.getNodeName().equals("#comment")) {
            if (dotRead) {
              getMultipleAddAttributesHelper(path, child, accum);
            }
            else {
              getMultipleHelper(path, child, accum, false);
            }
          }
          child = child.getNextSibling();
        }
        return;
      }
      else {
        while(child!=null) {
          if (child.getNodeName().equals(nodeName)) {
            // found
            if (dotRead) {
              getMultipleAddAttributesHelper(path, child, accum);
            }
            else {
              getMultipleHelper(path, child, accum, false);
            }
          }
          child = child.getNextSibling();
        }
        return;
      }
    }
    else {
      accum.add(n);
    }
  }
  
  private void getMultipleAddAttributesHelper(String path, Node n, List accum) {
    if ((path.indexOf('.') > -1) || (path.indexOf('/') > -1)) {
      throw new XMLConfigException("An attribute cannot have subparts (foo.bar.fum and foo.bar/fum not allowed)");
    }
    NamedNodeMap attrMap = n.getAttributes();
    if (path.equals("*")) {
      for(int i=0; i        Node attr = attrMap.item(i);
        accum.add(attr);
      }
    }
    else {
      Node attr = attrMap.getNamedItem(path);
      if (attr!=null) {
        accum.add(attr);
      }
    }
  }
  
  /** Set the value of the node or attribute specified by the DOM path.
   * @param path DOM path
   * @param value node or attribute value
   * @return the node that was created, or the parent node of the attribute if it was an attribute
   */
  public Node set(String path, String value) {
    if (isDelegated()) { return set(path, value, _startNode, true); }
    return set(path, value, _document, true);
  }
  
  /** Set the value of the node or attribute specified by the DOM path.
   * @param path DOM path
   * @param value node or attribute value
   * @param overwrite whether to overwrite (true) or add (false)
   * @return the node that was created, or the parent node of the attribute if it was an attribute
   */
  public Node set(String path, String value, boolean overwrite) {
    if (isDelegated()) { return set(path, value, _startNode, overwrite); }
    return set(path, value, _document, overwrite);
  }
  
  
  /** Set the value of the node or attribute specified by the DOM path.
   * @param path DOM path
   * @param value node or attribute value
   * @param n node where the search should start
   * @param overwrite whether to overwrite (true) or add (false) -- only applies for last node!
   * @return the node that was created, or the parent node of the attribute if it was an attribute
   */
  public Node set(String path, String value, Node n, boolean overwrite) {
    if (isDelegated()) { return _parent.set(path, value, n, overwrite); }
    
    int dotPos = path.lastIndexOf('.');
    Node node;
    if (dotPos==0) {
      node = n;
    }
    else {
      node = createNode(path, n, overwrite);
    }
    if (dotPos>=0) {
      Element e = (Element)node;
      e.setAttribute(path.substring(dotPos+1),value);
    }
    else {
      node.appendChild(_document.createTextNode(value));
    }
    return node;
  }
  
  /** Create the node specified by the DOM path.
   * @param path DOM path
   * @return the node that was created, or the parent node of the attribute if it was an attribute
   */
  public Node createNode(String path) {
    if (isDelegated()) { return createNode(path, _startNode, true); }
    
    return createNode(path, _document, true);
  }
  
  /** Create the node specified by the DOM path.
   * @param path DOM path
   * @param n node where the search should start, or null for the root
   * @return the node that was created, or the parent node of the attribute if it was an attribute
   */
  public Node createNode(String path, Node n) {
    return createNode(path, n, true);
  }
  
  /** Create the node specified by the DOM path.
   * @param path DOM path
   * @param n node where the search should start, or null for the root
   * @param overwrite whether to overwrite (true) or add (false) -- only applies for last node!
   * @return the node that was created, or the parent node of the attribute if it was an attribute
   */
  public Node createNode(String path, Node n, boolean overwrite) {
    if (isDelegated()) { return _parent.createNode(path, n, overwrite); }
    if (n==null) { n = _document; }
    while(path.indexOf('/') > -1) {
      Node child = null;
      String nodeName = path.substring(0, path.indexOf('/'));
      path = path.substring(path.indexOf('/')+1);
      child = n.getFirstChild();
      while(child!=null) {
        if (child.getNodeName().equals(nodeName)) {
          // found
          n = child;
          break;
        }
        child = child.getNextSibling();
      }
      if (child==null) {
        // not found
        child = _document.createElement(nodeName);
        n.appendChild(child);
        n = child;
      }
    }
    
    String nodeName;
    if (path.indexOf('.') > -1) {
      nodeName = path.substring(0, path.indexOf('.'));
    }
    else {
      if (path.length()==0) {
        throw new XMLConfigException("Cannot set node with empty name");
      }
      nodeName = path;
    }
    Node child = null;
    if (nodeName.length()>0) {
      if (overwrite) {
        child = n.getFirstChild();
        while(child!=null) {
          if (child.getNodeName().equals(nodeName)) {
            // found
            n = child;
            break;
          }
          child = child.getNextSibling();
        }
        if (child==null) {
          child = _document.createElement(nodeName);
          n.appendChild(child);
          n = child;
        }
      }
      else {
        child = _document.createElement(nodeName);
        n.appendChild(child);
        n = child;
      }
    }
    
    if (path.indexOf('.') > -1) {
      if (!(n instanceof Element)) {
        throw new XMLConfigException("Node "+n.getNodeName()+" should be an element so it can contain attributes");
      }
      return n;
    }
    else {
      if (overwrite) {
        child = n.getFirstChild();
        // remove all children
        while(child!=null) {
          Node temp = child.getNextSibling();
          n.removeChild(child);
          child = temp;
        }
        return n;
      }
      else {
        return child;
      }
    }
  }
  
  
  /** Returns a string representation of the object.
   * @return a string representation of the object.
   */
  public String toString() {
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    save(os);
    return os.toString();
  }
  
  /** Return the path of a node as it is used in XMLConfig.
   * @param n node
   * @return path
   */
  public static String getNodePath(Node n) {
    if (n==null) { return ""; }
    String path = "";
    while(n.getParentNode()!=null) {
      path = n.getNodeName()+"/"+path;
      n = n.getParentNode();
    }
    
    return path.substring(0,path.length()-1);
  }
  
  /** Exception in XMLConfig methods.
   */
  public static class XMLConfigException extends RuntimeException {
    public XMLConfigException() {
      super();
    }
    
    public XMLConfigException(String message) {
      super(message);
    }
    
    public XMLConfigException(String message, Throwable cause) {
      super(message, cause);
    }
    
    public XMLConfigException(Throwable cause) {
      super(cause);
    }
  }
}