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