View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17
    18  package org.apache.commons.configuration.plist;
    19
    20  import java.io.File;
    21  import java.io.PrintWriter;
    22  import java.io.Reader;
    23  import java.io.Writer;
    24  import java.math.BigDecimal;
    25  import java.net.URL;
    26  import java.text.DateFormat;
    27  import java.text.ParseException;
    28  import java.text.SimpleDateFormat;
    29  import java.util.ArrayList;
    30  import java.util.Calendar;
    31  import java.util.Date;
    32  import java.util.Iterator;
    33  import java.util.List;
    34  import java.util.Map;
    35  import javax.xml.parsers.SAXParser;
    36  import javax.xml.parsers.SAXParserFactory;
    37
    38  import org.apache.commons.codec.binary.Base64;
    39  import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
    40  import org.apache.commons.configuration.Configuration;
    41  import org.apache.commons.configuration.ConfigurationException;
    42  import org.apache.commons.configuration.HierarchicalConfiguration;
    43  import org.apache.commons.configuration.MapConfiguration;
    44  import org.apache.commons.lang.StringEscapeUtils;
    45  import org.apache.commons.lang.StringUtils;
    46
    47  import org.xml.sax.Attributes;
    48  import org.xml.sax.EntityResolver;
    49  import org.xml.sax.InputSource;
    50  import org.xml.sax.SAXException;
    51  import org.xml.sax.helpers.DefaultHandler;
    52
    53  /***
    54   * Property list file (plist) in XML format as used by Mac OS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd).
    55   * This configuration doesn't support the binary format used in OS X 10.4.
    56   *
    57   * <p>Example:</p>
    58   * <pre>
    59   * &lt;?xml version="1.0"?>
    60   * &lt;!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
    61   * &lt;plist version="1.0">
    62   *     &lt;dict>
    63   *         &lt;key>string&lt;/key>
    64   *         &lt;string>value1&lt;/string>
    65   *
    66   *         &lt;key>integer&lt;/key>
    67   *         &lt;integer>12345&lt;/integer>
    68   *
    69   *         &lt;key>real&lt;/key>
    70   *         &lt;real>-123.45E-1&lt;/real>
    71   *
    72   *         &lt;key>boolean&lt;/key>
    73   *         &lt;true/>
    74   *
    75   *         &lt;key>date&lt;/key>
    76   *         &lt;date>2005-01-01T12:00:00-0700&lt;/date>
    77   *
    78   *         &lt;key>data&lt;/key>
    79   *         &lt;data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==&lt;/data>
    80   *
    81   *         &lt;key>array&lt;/key>
    82   *         &lt;array>
    83   *             &lt;string>value1&lt;/string>
    84   *             &lt;string>value2&lt;/string>
    85   *             &lt;string>value3&lt;/string>
    86   *         &lt;/array>
    87   *
    88   *         &lt;key>dictionnary&lt;/key>
    89   *         &lt;dict>
    90   *             &lt;key>key1&lt;/key>
    91   *             &lt;string>value1&lt;/string>
    92   *             &lt;key>key2&lt;/key>
    93   *             &lt;string>value2&lt;/string>
    94   *             &lt;key>key3&lt;/key>
    95   *             &lt;string>value3&lt;/string>
    96   *         &lt;/dict>
    97   *
    98   *         &lt;key>nested&lt;/key>
    99   *         &lt;dict>
    100  *             &lt;key>node1&lt;/key>
    101  *             &lt;dict>
    102  *                 &lt;key>node2&lt;/key>
    103  *                 &lt;dict>
    104  *                     &lt;key>node3&lt;/key>
    105  *                     &lt;string>value&lt;/string>
    106  *                 &lt;/dict>
    107  *             &lt;/dict>
    108  *         &lt;/dict>
    109  *
    110  *     &lt;/dict>
    111  * &lt;/plist>
    112  * </pre>
    113  *
    114  * @since 1.2
    115  *
    116  * @author Emmanuel Bourg
    117  * @version $Revision: 532336 $, $Date: 2007-04-25 14:23:01 +0200 (Mi, 25 Apr 2007) $
    118  */
    119 public class XMLPropertyListConfiguration extends AbstractHierarchicalFileConfiguration
    120 {
    121     /***
    122      * The serial version UID.
    123      */
    124     private static final long serialVersionUID = -3162063751042475985L;
    125
    126     /*** Size of the indentation for the generated file. */
    127     private static final int INDENT_SIZE = 4;
    128
    129     /***
    130      * Creates an empty XMLPropertyListConfiguration object which can be
    131      * used to synthesize a new plist file by adding values and
    132      * then saving().
    133      */
    134     public XMLPropertyListConfiguration()
    135     {
    136     }
    137
    138     /***
    139      * Creates a new instance of <code>XMLPropertyListConfiguration</code> and
    140      * copies the content of the specified configuration into this object.
    141      *
    142      * @param configuration the configuration to copy
    143      * @since 1.4
    144      */
    145     public XMLPropertyListConfiguration(HierarchicalConfiguration configuration)
    146     {
    147         super(configuration);
    148     }
    149
    150     /***
    151      * Creates and loads the property list from the specified file.
    152      *
    153      * @param fileName The name of the plist file to load.
    154      * @throws org.apache.commons.configuration.ConfigurationException Error
    155      * while loading the plist file
    156      */
    157     public XMLPropertyListConfiguration(String fileName) throws ConfigurationException
    158     {
    159         super(fileName);
    160     }
    161
    162     /***
    163      * Creates and loads the property list from the specified file.
    164      *
    165      * @param file The plist file to load.
    166      * @throws ConfigurationException Error while loading the plist file
    167      */
    168     public XMLPropertyListConfiguration(File file) throws ConfigurationException
    169     {
    170         super(file);
    171     }
    172
    173     /***
    174      * Creates and loads the property list from the specified URL.
    175      *
    176      * @param url The location of the plist file to load.
    177      * @throws ConfigurationException Error while loading the plist file
    178      */
    179     public XMLPropertyListConfiguration(URL url) throws ConfigurationException
    180     {
    181         super(url);
    182     }
    183
    184     public void setProperty(String key, Object value)
    185     {
    186         // special case for byte arrays, they must be stored as is in the configuration
    187         if (value instanceof byte[])
    188         {
    189             fireEvent(EVENT_SET_PROPERTY, key, value, true);
    190             setDetailEvents(false);
    191             try
    192             {
    193                 clearProperty(key);
    194                 addPropertyDirect(key, value);
    195             }
    196             finally
    197             {
    198                 setDetailEvents(true);
    199             }
    200             fireEvent(EVENT_SET_PROPERTY, key, value, false);
    201         }
    202         else
    203         {
    204             super.setProperty(key, value);
    205         }
    206     }
    207
    208     public void addProperty(String key, Object value)
    209     {
    210         if (value instanceof byte[])
    211         {
    212             fireEvent(EVENT_ADD_PROPERTY, key, value, true);
    213             addPropertyDirect(key, value);
    214             fireEvent(EVENT_ADD_PROPERTY, key, value, false);
    215         }
    216         else
    217         {
    218             super.addProperty(key, value);
    219         }
    220     }
    221
    222     public void load(Reader in) throws ConfigurationException
    223     {
    224         // set up the DTD validation
    225         EntityResolver resolver = new EntityResolver()
    226         {
    227             public InputSource resolveEntity(String publicId, String systemId)
    228             {
    229                 return new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
    230             }
    231         };
    232
    233         // parse the file
    234         XMLPropertyListHandler handler = new XMLPropertyListHandler(getRoot());
    235         try
    236         {
    237             SAXParserFactory factory = SAXParserFactory.newInstance();
    238             factory.setValidating(true);
    239
    240             SAXParser parser = factory.newSAXParser();
    241             parser.getXMLReader().setEntityResolver(resolver);
    242             parser.getXMLReader().setContentHandler(handler);
    243             parser.getXMLReader().parse(new InputSource(in));
    244         }
    245         catch (Exception e)
    246         {
    247             throw new ConfigurationException("Unable to parse the configuration file", e);
    248         }
    249     }
    250
    251     public void save(Writer out) throws ConfigurationException
    252     {
    253         PrintWriter writer = new PrintWriter(out);
    254
    255         if (getEncoding() != null)
    256         {
    257             writer.println("<?xml version=\"1.0\" encoding=\"" + getEncoding() + "\"?>");
    258         }
    259         else
    260         {
    261             writer.println("<?xml version=\"1.0\"?>");
    262         }
    263
    264         writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
    265         writer.println("<plist version=\"1.0\">");
    266
    267         printNode(writer, 1, getRoot());
    268
    269         writer.println("</plist>");
    270         writer.flush();
    271     }
    272
    273     /***
    274      * Append a node to the writer, indented according to a specific level.
    275      */
    276     private void printNode(PrintWriter out, int indentLevel, Node node)
    277     {
    278         String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    279
    280         if (node.getName() != null)
    281         {
    282             out.println(padding + "<key>" + StringEscapeUtils.escapeXml(node.getName()) + "</key>");
    283         }
    284
    285         List children = node.getChildren();
    286         if (!children.isEmpty())
    287         {
    288             out.println(padding + "<dict>");
    289
    290             Iterator it = children.iterator();
    291             while (it.hasNext())
    292             {
    293                 Node child = (Node) it.next();
    294                 printNode(out, indentLevel + 1, child);
    295
    296                 if (it.hasNext())
    297                 {
    298                     out.println();
    299                 }
    300             }
    301
    302             out.println(padding + "</dict>");
    303         }
    304         else
    305         {
    306             Object value = node.getValue();
    307             printValue(out, indentLevel, value);
    308         }
    309     }
    310
    311     /***
    312      * Append a value to the writer, indented according to a specific level.
    313      */
    314     private void printValue(PrintWriter out, int indentLevel, Object value)
    315     {
    316         String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    317
    318         if (value instanceof Date)
    319         {
    320             out.println(padding + "<date>" + PListNode.format.format((Date) value) + "</date>");
    321         }
    322         else if (value instanceof Calendar)
    323         {
    324             printValue(out, indentLevel, ((Calendar) value).getTime());
    325         }
    326         else if (value instanceof Number)
    327         {
    328             if (value instanceof Double || value instanceof Float || value instanceof BigDecimal)
    329             {
    330                 out.println(padding + "<real>" + value.toString() + "</real>");
    331             }
    332             else
    333             {
    334                 out.println(padding + "<integer>" + value.toString() + "</integer>");
    335             }
    336         }
    337         else if (value instanceof Boolean)
    338         {
    339             if (((Boolean) value).booleanValue())
    340             {
    341                 out.println(padding + "<true/>");
    342             }
    343             else
    344             {
    345                 out.println(padding + "<false/>");
    346             }
    347         }
    348         else if (value instanceof List)
    349         {
    350             out.println(padding + "<array>");
    351             Iterator it = ((List) value).iterator();
    352             while (it.hasNext())
    353             {
    354                 printValue(out, indentLevel + 1, it.next());
    355             }
    356             out.println(padding + "</array>");
    357         }
    358         else if (value instanceof HierarchicalConfiguration)
    359         {
    360             printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
    361         }
    362         else if (value instanceof Configuration)
    363         {
    364             // display a flat Configuration as a dictionary
    365             out.println(padding + "<dict>");
    366
    367             Configuration config = (Configuration) value;
    368             Iterator it = config.getKeys();
    369             while (it.hasNext())
    370             {
    371                 // create a node for each property
    372                 String key = (String) it.next();
    373                 Node node = new Node(key);
    374                 node.setValue(config.getProperty(key));
    375
    376                 // print the node
    377                 printNode(out, indentLevel + 1, node);
    378
    379                 if (it.hasNext())
    380                 {
    381                     out.println();
    382                 }
    383             }
    384             out.println(padding + "</dict>");
    385         }
    386         else if (value instanceof Map)
    387         {
    388             // display a Map as a dictionary
    389             Map map = (Map) value;
    390             printValue(out, indentLevel, new MapConfiguration(map));
    391         }
    392         else if (value instanceof byte[])
    393         {
    394             String base64 = new String(Base64.encodeBase64((byte[]) value));
    395             out.println(padding + "<data>" + StringEscapeUtils.escapeXml(base64) + "</data>");
    396         }
    397         else
    398         {
    399             out.println(padding + "<string>" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "</string>");
    400         }
    401     }
    402
    403     /***
    404      * SAX Handler to build the configuration nodes while the document is being parsed.
    405      */
    406     private class XMLPropertyListHandler extends DefaultHandler
    407     {
    408         /*** The buffer containing the text node being read */
    409         private StringBuffer buffer = new StringBuffer();
    410
    411         /*** The stack of configuration nodes */
    412         private List stack = new ArrayList();
    413
    414         public XMLPropertyListHandler(Node root)
    415         {
    416             push(root);
    417         }
    418
    419         /***
    420          * Return the node on the top of the stack.
    421          */
    422         private Node peek()
    423         {
    424             if (!stack.isEmpty())
    425             {
    426                 return (Node) stack.get(stack.size() - 1);
    427             }
    428             else
    429             {
    430                 return null;
    431             }
    432         }
    433
    434         /***
    435          * Remove and return the node on the top of the stack.
    436          */
    437         private Node pop()
    438         {
    439             if (!stack.isEmpty())
    440             {
    441                 return (Node) stack.remove(stack.size() - 1);
    442             }
    443             else
    444             {
    445                 return null;
    446             }
    447         }
    448
    449         /***
    450          * Put a node on the top of the stack.
    451          */
    452         private void push(Node node)
    453         {
    454             stack.add(node);
    455         }
    456
    457         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
    458         {
    459             if ("array".equals(qName))
    460             {
    461                 push(new ArrayNode());
    462             }
    463             else if ("dict".equals(qName))
    464             {
    465                 if (peek() instanceof ArrayNode)
    466                 {
    467                     // create the configuration
    468                     XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
    469
    470                     // add it to the ArrayNode
    471                     ArrayNode node = (ArrayNode) peek();
    472                     node.addValue(config);
    473
    474                     // push the root on the stack
    475                     push(config.getRoot());
    476                 }
    477             }
    478         }
    479
    480         public void endElement(String uri, String localName, String qName) throws SAXException
    481         {
    482             if ("key".equals(qName))
    483             {
    484                 // create a new node, link it to its parent and push it on the stack
    485                 PListNode node = new PListNode();
    486                 node.setName(buffer.toString());
    487                 peek().addChild(node);
    488                 push(node);
    489             }
    490             else if ("dict".equals(qName))
    491             {
    492                 // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
    493                 pop();
    494             }
    495             else
    496             {
    497                 if ("string".equals(qName))
    498                 {
    499                     ((PListNode) peek()).addValue(buffer.toString());
    500                 }
    501                 else if ("integer".equals(qName))
    502                 {
    503                     ((PListNode) peek()).addIntegerValue(buffer.toString());
    504                 }
    505                 else if ("real".equals(qName))
    506                 {
    507                     ((PListNode) peek()).addRealValue(buffer.toString());
    508                 }
    509                 else if ("true".equals(qName))
    510                 {
    511                     ((PListNode) peek()).addTrueValue();
    512                 }
    513                 else if ("false".equals(qName))
    514                 {
    515                     ((PListNode) peek()).addFalseValue();
    516                 }
    517                 else if ("data".equals(qName))
    518                 {
    519                     ((PListNode) peek()).addDataValue(buffer.toString());
    520                 }
    521                 else if ("date".equals(qName))
    522                 {
    523                     ((PListNode) peek()).addDateValue(buffer.toString());
    524                 }
    525                 else if ("array".equals(qName))
    526                 {
    527                     ArrayNode array = (ArrayNode) pop();
    528                     ((PListNode) peek()).addList(array);
    529                 }
    530
    531                 // remove the plist node on the stack once the value has been parsed,
    532                 // array nodes remains on the stack for the next values in the list
    533                 if (!(peek() instanceof ArrayNode))
    534                 {
    535                     pop();
    536                 }
    537             }
    538
    539             buffer.setLength(0);
    540         }
    541
    542         public void characters(char[] ch, int start, int length) throws SAXException
    543         {
    544             buffer.append(ch, start, length);
    545         }
    546     }
    547
    548     /***
    549      * Node extension with addXXX methods to parse the typed data passed by the SAX handler.
    550      * <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
    551      * to parse the configuration file, it may be removed at any moment in the future.
    552      */
    553     public static class PListNode extends Node
    554     {
    555         /***
    556          * The serial version UID.
    557          */
    558         private static final long serialVersionUID = -7614060264754798317L;
    559
    560         /*** The standard format of dates in plist files. */
    561         private static DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
    562
    563         /***
    564          * Update the value of the node. If the existing value is null, it's
    565          * replaced with the new value. If the existing value is a list, the
    566          * specified value is appended to the list. If the existing value is
    567          * not null, a list with the two values is built.
    568          *
    569          * @param value the value to be added
    570          */
    571         public void addValue(Object value)
    572         {
    573             if (getValue() == null)
    574             {
    575                 setValue(value);
    576             }
    577             else if (getValue() instanceof List)
    578             {
    579                 List list = (List) getValue();
    580                 list.add(value);
    581             }
    582             else
    583             {
    584                 List list = new ArrayList();
    585                 list.add(getValue());
    586                 list.add(value);
    587                 setValue(list);
    588             }
    589         }
    590
    591         /***
    592          * Parse the specified string as a date and add it to the values of the node.
    593          *
    594          * @param value the value to be added
    595          */
    596         public void addDateValue(String value)
    597         {
    598             try
    599             {
    600                 addValue(format.parse(value));
    601             }
    602             catch (ParseException e)
    603             {
    604                 // ignore
    605                 ;
    606             }
    607         }
    608
    609         /***
    610          * Parse the specified string as a byte array in base 64 format
    611          * and add it to the values of the node.
    612          *
    613          * @param value the value to be added
    614          */
    615         public void addDataValue(String value)
    616         {
    617             addValue(Base64.decodeBase64(value.getBytes()));
    618         }
    619
    620         /***
    621          * Parse the specified string as an Interger and add it to the values of the node.
    622          *
    623          * @param value the value to be added
    624          */
    625         public void addIntegerValue(String value)
    626         {
    627             addValue(new Integer(value));
    628         }
    629
    630         /***
    631          * Parse the specified string as a Double and add it to the values of the node.
    632          *
    633          * @param value the value to be added
    634          */
    635         public void addRealValue(String value)
    636         {
    637             addValue(new Double(value));
    638         }
    639
    640         /***
    641          * Add a boolean value 'true' to the values of the node.
    642          */
    643         public void addTrueValue()
    644         {
    645             addValue(Boolean.TRUE);
    646         }
    647
    648         /***
    649          * Add a boolean value 'false' to the values of the node.
    650          */
    651         public void addFalseValue()
    652         {
    653             addValue(Boolean.FALSE);
    654         }
    655
    656         /***
    657          * Add a sublist to the values of the node.
    658          *
    659          * @param node the node whose value will be added to the current node value
    660          */
    661         public void addList(ArrayNode node)
    662         {
    663             addValue(node.getValue());
    664         }
    665     }
    666
    667     /***
    668      * Container for array elements. <b>Do not use this class !</b>
    669      * It is used internally by XMLPropertyConfiguration to parse the
    670      * configuration file, it may be removed at any moment in the future.
    671      */
    672     public static class ArrayNode extends PListNode
    673     {
    674         /***
    675          * The serial version UID.
    676          */
    677         private static final long serialVersionUID = 5586544306664205835L;
    678
    679         /*** The list of values in the array. */
    680         private List list = new ArrayList();
    681
    682         /***
    683          * Add an object to the array.
    684          *
    685          * @param value the value to be added
    686          */
    687         public void addValue(Object value)
    688         {
    689             list.add(value);
    690         }
    691
    692         /***
    693          * Return the list of values in the array.
    694          *
    695          * @return the {@link List} of values
    696          */
    697         public Object getValue()
    698         {
    699             return list;
    700         }
    701     }
    702 }