View Javadoc

    1   /*
    2    * Copyright 2004 The Apache Software Foundation.
    3    *
    4    * Licensed under the Apache License, Version 2.0 (the "License")
    5    * you may not use this file except in compliance with the License.
    6    * You may obtain a copy of the License at
    7    *
    8    *     http://www.apache.org/licenses/LICENSE-2.0
    9    *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16
    17  package org.apache.commons.configuration;
    18
    19  import java.io.File;
    20  import java.io.Reader;
    21  import java.io.StringWriter;
    22  import java.io.Writer;
    23  import java.net.URL;
    24  import java.util.ArrayList;
    25  import java.util.Iterator;
    26  import java.util.List;
    27  import javax.xml.parsers.DocumentBuilder;
    28  import javax.xml.parsers.DocumentBuilderFactory;
    29  import javax.xml.parsers.ParserConfigurationException;
    30  import javax.xml.transform.Result;
    31  import javax.xml.transform.Source;
    32  import javax.xml.transform.Transformer;
    33  import javax.xml.transform.TransformerException;
    34  import javax.xml.transform.TransformerFactory;
    35  import javax.xml.transform.dom.DOMSource;
    36  import javax.xml.transform.stream.StreamResult;
    37
    38  import org.apache.commons.lang.StringUtils;
    39  import org.w3c.dom.Attr;
    40  import org.w3c.dom.CDATASection;
    41  import org.w3c.dom.CharacterData;
    42  import org.w3c.dom.Document;
    43  import org.w3c.dom.Element;
    44  import org.w3c.dom.NamedNodeMap;
    45  import org.w3c.dom.Node;
    46  import org.w3c.dom.NodeList;
    47  import org.w3c.dom.Text;
    48  import org.xml.sax.InputSource;
    49
    50  /***
    51   * Reads a XML configuration file.
    52   *
    53   * To retrieve the value of an attribute of an element, use
    54   * <code>X.Y.Z[@attribute]</code>. The '@' symbol was chosen for consistency
    55   * with XPath.
    56   *
    57   * Setting property values will <b>NOT </b> automatically persist changes to
    58   * disk, unless <code>autoSave=true</code>.
    59   *
    60   * @since commons-configuration 1.0
    61   *
    62   * @author J�rg Schaible
    63   * @author <a href="mailto:kelvint@apache.org">Kelvin Tan </a>
    64   * @author <a href="mailto:dlr@apache.org">Daniel Rall </a>
    65   * @author Emmanuel Bourg
    66   * @version $Revision: 1.17 $, $Date: 2004/10/04 19:35:45 $
    67   */
    68  public class XMLConfiguration extends AbstractFileConfiguration
    69  {
    70      // For conformance with xpath
    71      private static final String ATTRIBUTE_START = "[@";
    72
    73      private static final String ATTRIBUTE_END = "]";
    74
    75      /***
    76       * For consistency with properties files. Access nodes via an "A.B.C"
    77       * notation.
    78       */
    79      private static final String NODE_DELIMITER = ".";
    80
    81      /***
    82       * The XML document from our data source.
    83       */
    84      private Document document;
    85
    86      /***
    87       * If true, modifications are immediately persisted.
    88       */
    89      private boolean autoSave = false;
    90
    91      /***
    92       * Creates an empty XML configuration.
    93       */
    94      public XMLConfiguration()
    95      {
    96          // build an empty document.
    97          DocumentBuilder builder = null;
    98          try
    99          {
    100             builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    101         }
    102         catch (ParserConfigurationException e)
    103         {
    104             throw new ConfigurationRuntimeException(e.getMessage(), e);
    105         }
    106
    107         document = builder.newDocument();
    108         document.appendChild(document.createElement("configuration"));
    109     }
    110
    111     /***
    112      * Creates and loads the XML configuration from the specified resource.
    113      *
    114      * @param resource The name of the resource to load.
    115      *
    116      * @throws ConfigurationException Error while loading the XML file
    117      */
    118     public XMLConfiguration(String resource) throws ConfigurationException
    119     {
    120         this.fileName = resource;
    121         url = ConfigurationUtils.locate(resource);
    122         load();
    123     }
    124
    125     /***
    126      * Creates and loads the XML configuration from the specified file.
    127      *
    128      * @param file The XML file to load.
    129      * @throws ConfigurationException Error while loading the XML file
    130      */
    131     public XMLConfiguration(File file) throws ConfigurationException
    132     {
    133         setFile(file);
    134         load();
    135     }
    136
    137     /***
    138      * Creates and loads the XML configuration from the specified URL.
    139      *
    140      * @param url The location of the XML file to load.
    141      * @throws ConfigurationException Error while loading the XML file
    142      */
    143     public XMLConfiguration(URL url) throws ConfigurationException
    144     {
    145         setURL(url);
    146         load();
    147     }
    148
    149     /***
    150      * {@inheritDoc}
    151      */
    152     public void load(Reader in) throws ConfigurationException
    153     {
    154         try
    155         {
    156             DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    157             document = builder.parse(new InputSource(in));
    158         }
    159         catch (Exception e)
    160         {
    161             throw new ConfigurationException(e.getMessage(), e);
    162         }
    163
    164         initProperties(document.getDocumentElement(), new StringBuffer());
    165     }
    166
    167     /***
    168      * Loads and initializes from the XML file.
    169      *
    170      * @param element The element to start processing from. Callers should supply the root element of the document.
    171      * @param hierarchy
    172      */
    173     private void initProperties(Element element, StringBuffer hierarchy)
    174     {
    175         StringBuffer buffer = new StringBuffer();
    176         NodeList list = element.getChildNodes();
    177         for (int i = 0; i < list.getLength(); i++)
    178         {
    179             Node node = list.item(i);
    180             if (node instanceof Element)
    181             {
    182                 Element child = (Element) node;
    183
    184                 StringBuffer subhierarchy = new StringBuffer(hierarchy.toString());
    185                 subhierarchy.append(child.getTagName());
    186                 processAttributes(subhierarchy.toString(), child);
    187                 initProperties(child, subhierarchy.append(NODE_DELIMITER));
    188             }
    189             else if (node instanceof CDATASection || node instanceof Text)
    190             {
    191                 CharacterData data = (CharacterData) node;
    192                 buffer.append(data.getData());
    193             }
    194         }
    195
    196         String text = buffer.toString().trim();
    197         if (text.length() > 0 && hierarchy.length() > 0)
    198         {
    199             super.addProperty(hierarchy.substring(0, hierarchy.length() - 1), text);
    200         }
    201     }
    202
    203     /***
    204      * Helper method for constructing properties for the attributes of the given
    205      * XML element.
    206      *
    207      * @param hierarchy the actual hierarchy
    208      * @param element   the actual XML element
    209      */
    210     private void processAttributes(String hierarchy, Element element)
    211     {
    212         // Add attributes as x.y{ATTRIBUTE_START}att{ATTRIBUTE_END}
    213         NamedNodeMap attributes = element.getAttributes();
    214         for (int i = 0; i < attributes.getLength(); ++i)
    215         {
    216             Attr attr = (Attr) attributes.item(i);
    217             String attrName = hierarchy + ATTRIBUTE_START + attr.getName() + ATTRIBUTE_END;
    218             super.addProperty(attrName, attr.getValue());
    219         }
    220     }
    221
    222     /***
    223      * Calls super method, and also ensures the underlying {@linkDocument} is
    224      * modified so changes are persisted when saved.
    225      *
    226      * @param name
    227      * @param value
    228      */
    229     public void addProperty(String name, Object value)
    230     {
    231         super.addProperty(name, value);
    232         addXmlProperty(name, value);
    233         possiblySave();
    234     }
    235
    236     Object getXmlProperty(String name)
    237     {
    238         // parse the key
    239         String[] nodes = parseElementNames(name);
    240         String attName = parseAttributeName(name);
    241
    242         // get all the matching elements
    243         List children = findElementsForPropertyNodes(nodes);
    244
    245         List properties = new ArrayList();
    246         if (attName == null)
    247         {
    248             // return text contents of elements
    249             Iterator cIter = children.iterator();
    250             while (cIter.hasNext())
    251             {
    252                 Element child = (Element) cIter.next();
    253                 // add non-empty strings
    254                 String text = getChildText(child);
    255                 if (StringUtils.isNotEmpty(text))
    256                 {
    257                     properties.add(text);
    258                 }
    259             }
    260         }
    261         else
    262         {
    263             // return text contents of attributes
    264             Iterator cIter = children.iterator();
    265             while (cIter.hasNext())
    266             {
    267                 Element child = (Element) cIter.next();
    268                 if (child.hasAttribute(attName))
    269                 {
    270                     properties.add(child.getAttribute(attName));
    271                 }
    272             }
    273         }
    274
    275         switch (properties.size())
    276         {
    277             case 0:
    278                 return null;
    279             case 1:
    280                 return properties.get(0);
    281             default:
    282                 return properties;
    283         }
    284     }
    285
    286     /***
    287      * TODO Add comment.
    288      *
    289      * @param nodes
    290      * @return
    291      */
    292     private List findElementsForPropertyNodes(String[] nodes)
    293     {
    294         List children = new ArrayList();
    295         List elements = new ArrayList();
    296
    297         children.add(document.getDocumentElement());
    298         for (int i = 0; i < nodes.length; i++)
    299         {
    300             elements.clear();
    301             elements.addAll(children);
    302             children.clear();
    303
    304             String eName = nodes[i];
    305             Iterator eIter = elements.iterator();
    306             while (eIter.hasNext())
    307             {
    308                 Element element = (Element) eIter.next();
    309                 NodeList list = element.getChildNodes();
    310                 for (int j = 0; j < list.getLength(); j++)
    311                 {
    312                     Node node = list.item(j);
    313                     if (node instanceof Element)
    314                     {
    315                         Element child = (Element) node;
    316                         if (eName.equals(child.getTagName()))
    317                         {
    318                             children.add(child);
    319                         }
    320                     }
    321                 }
    322             }
    323         }
    324
    325         return children;
    326     }
    327
    328     private static String getChildText(Node node)
    329     {
    330         // is there anything to do?
    331         if (node == null)
    332         {
    333             return null;
    334         }
    335
    336         // concatenate children text
    337         StringBuffer str = new StringBuffer();
    338         Node child = node.getFirstChild();
    339         while (child != null)
    340         {
    341             short type = child.getNodeType();
    342             if (type == Node.TEXT_NODE)
    343             {
    344                 str.append(child.getNodeValue());
    345             }
    346             else if (type == Node.CDATA_SECTION_NODE)
    347             {
    348                 str.append(child.getNodeValue());
    349             }
    350             child = child.getNextSibling();
    351         }
    352
    353         // return text value
    354         return StringUtils.trimToNull(str.toString());
    355
    356     }
    357
    358     private Element getChildElementWithName(String eName, Element element)
    359     {
    360         Element child = null;
    361
    362         NodeList list = element.getChildNodes();
    363         for (int j = 0; j < list.getLength(); j++)
    364         {
    365             Node node = list.item(j);
    366             if (node instanceof Element)
    367             {
    368                 child = (Element) node;
    369                 if (eName.equals(child.getTagName()))
    370                 {
    371                     break;
    372                 }
    373                 child = null;
    374             }
    375         }
    376         return child;
    377     }
    378
    379     /***
    380      * Adds the property value in our document tree.
    381      *
    382      * @param name  The name of the element to set a value for.
    383      * @param value The value to set.
    384      */
    385     private void addXmlProperty(String name, Object value)
    386     {
    387         // parse the key
    388         String[] nodes = parseElementNames(name);
    389         String attName = parseAttributeName(name);
    390
    391         Element element = document.getDocumentElement();
    392         Element parent = element;
    393
    394         for (int i = 0; i < nodes.length; i++)
    395         {
    396             if (element == null)
    397             {
    398                 break;
    399             }
    400             parent = element;
    401             String eName = nodes[i];
    402             Element child = getChildElementWithName(eName, element);
    403
    404             element = child;
    405         }
    406
    407         Element child = document.createElement(nodes[nodes.length - 1]);
    408         parent.appendChild(child);
    409         if (attName == null)
    410         {
    411             CharacterData data = document.createTextNode(String.valueOf(value));
    412             child.appendChild(data);
    413         }
    414         else
    415         {
    416             child.setAttribute(attName, String.valueOf(value));
    417         }
    418     }
    419
    420     /***
    421      * Calls super method, and also ensures the underlying {@link Document}is
    422      * modified so changes are persisted when saved.
    423      *
    424      * @param name The name of the property to clear.
    425      */
    426     public void clearProperty(String name)
    427     {
    428         super.clearProperty(name);
    429         clearXmlProperty(name);
    430         possiblySave();
    431     }
    432
    433     private void clearXmlProperty(String name)
    434     {
    435         // parse the key
    436         String[] nodes = parseElementNames(name);
    437         String attName = parseAttributeName(name);
    438
    439         // get all the matching elements
    440         List children = findElementsForPropertyNodes(nodes);
    441
    442         if (attName == null)
    443         {
    444             // remove children with no subelements
    445             Iterator cIter = children.iterator();
    446             while (cIter.hasNext())
    447             {
    448                 Element child = (Element) cIter.next();
    449
    450                 // determine if child has subelments
    451                 boolean hasSubelements = false;
    452                 Node subchild = child.getFirstChild();
    453                 while (subchild != null)
    454                 {
    455                     if (subchild.getNodeType() == Node.ELEMENT_NODE)
    456                     {
    457                         hasSubelements = true;
    458                         break;
    459                     }
    460                     subchild = subchild.getNextSibling();
    461                 }
    462
    463                 if (!hasSubelements)
    464                 {
    465                     // safe to remove
    466                     if (!child.hasAttributes())
    467                     {
    468                         // remove entire node
    469                         Node parent = child.getParentNode();
    470                         parent.removeChild(child);
    471                     }
    472                     else
    473                     {
    474                         // only remove node contents
    475                         subchild = child.getLastChild();
    476                         while (subchild != null)
    477                         {
    478                             child.removeChild(subchild);
    479                             subchild = child.getLastChild();
    480                         }
    481                     }
    482                 }
    483             }
    484         }
    485         else
    486         {
    487             // remove attributes from children
    488             Iterator cIter = children.iterator();
    489             while (cIter.hasNext())
    490             {
    491                 Element child = (Element) cIter.next();
    492                 child.removeAttribute(attName);
    493             }
    494         }
    495     }
    496
    497     /***
    498      * Save the configuration if the automatic persistence is enabled and a file
    499      * is specified.
    500      */
    501     private void possiblySave()
    502     {
    503         if (autoSave && fileName != null)
    504         {
    505             try
    506             {
    507                 save();
    508             }
    509             catch (ConfigurationException ce)
    510             {
    511                 throw new ConfigurationRuntimeException("Failed to auto-save", ce);
    512             }
    513         }
    514     }
    515
    516     /***
    517      * If true, changes are automatically persisted.
    518      *
    519      * @param autoSave
    520      */
    521     public void setAutoSave(boolean autoSave)
    522     {
    523         this.autoSave = autoSave;
    524     }
    525
    526     /***
    527      * {@inheritDoc}
    528      */
    529     public void save(Writer writer) throws ConfigurationException
    530     {
    531         try
    532         {
    533             Transformer transformer = TransformerFactory.newInstance().newTransformer();
    534             Source source = new DOMSource(document);
    535             Result result = new StreamResult(writer);
    536
    537             transformer.setOutputProperty("indent", "yes");
    538             transformer.transform(source, result);
    539         }
    540         catch (TransformerException e)
    541         {
    542             throw new ConfigurationException(e.getMessage(), e);
    543         }
    544     }
    545
    546     public String toString()
    547     {
    548         StringWriter writer = new StringWriter();
    549         try
    550         {
    551             save(writer);
    552         }
    553         catch (ConfigurationException e)
    554         {
    555             e.printStackTrace();
    556         }
    557         return writer.toString();
    558     }
    559
    560     /***
    561      * Parse a property key and return an array of the element hierarchy it
    562      * specifies. For example the key "x.y.z[@abc]" will result in [x, y, z].
    563      *
    564      * @param key the key to parse
    565      *
    566      * @return the elements in the key
    567      */
    568     protected static String[] parseElementNames(String key)
    569     {
    570         if (key == null)
    571         {
    572             return new String[]{};
    573         }
    574         else
    575         {
    576             // find the beginning of the attribute name
    577             int attStart = key.indexOf(ATTRIBUTE_START);
    578
    579             if (attStart > -1)
    580             {
    581                 // remove the attribute part of the key
    582                 key = key.substring(0, attStart);
    583             }
    584
    585             return StringUtils.split(key, NODE_DELIMITER);
    586         }
    587     }
    588
    589     /***
    590      * Parse a property key and return the attribute name if it existst.
    591      *
    592      * @param key the key to parse
    593      *
    594      * @return the attribute name, or null if the key doesn't contain one
    595      */
    596     protected static String parseAttributeName(String key)
    597     {
    598         String name = null;
    599
    600         if (key != null)
    601         {
    602             // find the beginning of the attribute name
    603             int attStart = key.indexOf(ATTRIBUTE_START);
    604
    605             if (attStart > -1)
    606             {
    607                 // find the end of the attribute name
    608                 int attEnd = key.indexOf(ATTRIBUTE_END);
    609                 attEnd = attEnd > -1 ? attEnd : key.length();
    610
    611                 name = key.substring(attStart + ATTRIBUTE_START.length(), attEnd);
    612             }
    613         }
    614
    615         return name;
    616     }
    617 }