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.net.URL;
    25  import java.util.ArrayList;
    26  import java.util.Calendar;
    27  import java.util.Date;
    28  import java.util.Iterator;
    29  import java.util.List;
    30  import java.util.Map;
    31  import java.util.TimeZone;
    32
    33  import org.apache.commons.codec.binary.Hex;
    34  import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
    35  import org.apache.commons.configuration.Configuration;
    36  import org.apache.commons.configuration.ConfigurationException;
    37  import org.apache.commons.configuration.HierarchicalConfiguration;
    38  import org.apache.commons.configuration.MapConfiguration;
    39  import org.apache.commons.lang.StringUtils;
    40
    41  /***
    42   * NeXT / OpenStep style configuration. This configuration can read and write
    43   * ASCII plist files. It support the GNUStep extension to specify date objects.
    44   * <p>
    45   * References:
    46   * <ul>
    47   *   <li><a
    48   * href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/Articles/OldStylePListsConcept.html">
    49   * Apple Documentation - Old-Style ASCII Property Lists</a></li>
    50   *   <li><a
    51   * href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html">
    52   * GNUStep Documentation</a></li>
    53   * </ul>
    54   *
    55   * <p>Example:</p>
    56   * <pre>
    57   * {
    58   *     foo = "bar";
    59   *
    60   *     array = ( value1, value2, value3 );
    61   *
    62   *     data = &lt;4f3e0145ab>;
    63   *
    64   *     date = &lt;*D2007-05-05 20:05:00 +0100>;
    65   *
    66   *     nested =
    67   *     {
    68   *         key1 = value1;
    69   *         key2 = value;
    70   *         nested =
    71   *         {
    72   *             foo = bar
    73   *         }
    74   *     }
    75   * }
    76   * </pre>
    77   *
    78   * @since 1.2
    79   *
    80   * @author Emmanuel Bourg
    81   * @version $Revision: 590474 $, $Date: 2007-10-30 22:35:11 +0100 (Di, 30 Okt 2007) $
    82   */
    83  public class PropertyListConfiguration extends AbstractHierarchicalFileConfiguration
    84  {
    85      /*** Constant for the separator parser for the date part. */
    86      private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser(
    87              "-");
    88
    89      /*** Constant for the separator parser for the time part. */
    90      private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser(
    91              ":");
    92
    93      /*** Constant for the separator parser for blanks between the parts. */
    94      private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser(
    95              " ");
    96
    97      /*** An array with the component parsers for dealing with dates. */
    98      private static final DateComponentParser[] DATE_PARSERS =
    99      {new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4),
    100             DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.MONTH, 2, 1),
    101             DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2),
    102             BLANK_SEPARATOR_PARSER,
    103             new DateFieldParser(Calendar.HOUR_OF_DAY, 2),
    104             TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2),
    105             TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.SECOND, 2),
    106             BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(),
    107             new DateSeparatorParser(">")};
    108
    109     /*** Constant for the ID prefix for GMT time zones. */
    110     private static final String TIME_ZONE_PREFIX = "GMT";
    111
    112     /*** The serial version UID. */
    113     private static final long serialVersionUID = 3227248503779092127L;
    114
    115     /*** Constant for the milliseconds of a minute.*/
    116     private static final int MILLIS_PER_MINUTE = 1000 * 60;
    117
    118     /*** Constant for the minutes per hour.*/
    119     private static final int MINUTES_PER_HOUR = 60;
    120
    121     /*** Size of the indentation for the generated file. */
    122     private static final int INDENT_SIZE = 4;
    123
    124     /*** Constant for the length of a time zone.*/
    125     private static final int TIME_ZONE_LENGTH = 5;
    126
    127     /*** Constant for the padding character in the date format.*/
    128     private static final char PAD_CHAR = '0';
    129
    130     /***
    131      * Creates an empty PropertyListConfiguration object which can be
    132      * used to synthesize a new plist file by adding values and
    133      * then saving().
    134      */
    135     public PropertyListConfiguration()
    136     {
    137     }
    138
    139     /***
    140      * Creates a new instance of <code>PropertyListConfiguration</code> and
    141      * copies the content of the specified configuration into this object.
    142      *
    143      * @param c the configuration to copy
    144      * @since 1.4
    145      */
    146     public PropertyListConfiguration(HierarchicalConfiguration c)
    147     {
    148         super(c);
    149     }
    150
    151     /***
    152      * Creates and loads the property list from the specified file.
    153      *
    154      * @param fileName The name of the plist file to load.
    155      * @throws ConfigurationException Error while loading the plist file
    156      */
    157     public PropertyListConfiguration(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 PropertyListConfiguration(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 PropertyListConfiguration(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         PropertyListParser parser = new PropertyListParser(in);
    225         try
    226         {
    227             HierarchicalConfiguration config = parser.parse();
    228             setRoot(config.getRoot());
    229         }
    230         catch (ParseException e)
    231         {
    232             throw new ConfigurationException(e);
    233         }
    234     }
    235
    236     public void save(Writer out) throws ConfigurationException
    237     {
    238         PrintWriter writer = new PrintWriter(out);
    239         printNode(writer, 0, getRoot());
    240         writer.flush();
    241     }
    242
    243     /***
    244      * Append a node to the writer, indented according to a specific level.
    245      */
    246     private void printNode(PrintWriter out, int indentLevel, Node node)
    247     {
    248         String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    249
    250         if (node.getName() != null)
    251         {
    252             out.print(padding + quoteString(node.getName()) + " = ");
    253         }
    254
    255         // get all non trivial nodes
    256         List children = new ArrayList(node.getChildren());
    257         Iterator it = children.iterator();
    258         while (it.hasNext())
    259         {
    260             Node child = (Node) it.next();
    261             if (child.getValue() == null && (child.getChildren() == null || child.getChildren().isEmpty()))
    262             {
    263                 it.remove();
    264             }
    265         }
    266
    267         if (!children.isEmpty())
    268         {
    269             // skip a line, except for the root dictionary
    270             if (indentLevel > 0)
    271             {
    272                 out.println();
    273             }
    274
    275             out.println(padding + "{");
    276
    277             // display the children
    278             it = children.iterator();
    279             while (it.hasNext())
    280             {
    281                 Node child = (Node) it.next();
    282
    283                 printNode(out, indentLevel + 1, child);
    284
    285                 // add a semi colon for elements that are not dictionaries
    286                 Object value = child.getValue();
    287                 if (value != null && !(value instanceof Map) && !(value instanceof Configuration))
    288                 {
    289                     out.println(";");
    290                 }
    291
    292                 // skip a line after arrays and dictionaries
    293                 if (it.hasNext() && (value == null || value instanceof List))
    294                 {
    295                     out.println();
    296                 }
    297             }
    298
    299             out.print(padding + "}");
    300
    301             // line feed if the dictionary is not in an array
    302             if (node.getParent() != null)
    303             {
    304                 out.println();
    305             }
    306         }
    307         else
    308         {
    309             // display the leaf value
    310             Object value = node.getValue();
    311             printValue(out, indentLevel, value);
    312         }
    313     }
    314
    315     /***
    316      * Append a value to the writer, indented according to a specific level.
    317      */
    318     private void printValue(PrintWriter out, int indentLevel, Object value)
    319     {
    320         String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
    321
    322         if (value instanceof List)
    323         {
    324             out.print("( ");
    325             Iterator it = ((List) value).iterator();
    326             while (it.hasNext())
    327             {
    328                 printValue(out, indentLevel + 1, it.next());
    329                 if (it.hasNext())
    330                 {
    331                     out.print(", ");
    332                 }
    333             }
    334             out.print(" )");
    335         }
    336         else if (value instanceof HierarchicalConfiguration)
    337         {
    338             printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
    339         }
    340         else if (value instanceof Configuration)
    341         {
    342             // display a flat Configuration as a dictionary
    343             out.println();
    344             out.println(padding + "{");
    345
    346             Configuration config = (Configuration) value;
    347             Iterator it = config.getKeys();
    348             while (it.hasNext())
    349             {
    350                 String key = (String) it.next();
    351                 Node node = new Node(key);
    352                 node.setValue(config.getProperty(key));
    353
    354                 printNode(out, indentLevel + 1, node);
    355                 out.println(";");
    356             }
    357             out.println(padding + "}");
    358         }
    359         else if (value instanceof Map)
    360         {
    361             // display a Map as a dictionary
    362             Map map = (Map) value;
    363             printValue(out, indentLevel, new MapConfiguration(map));
    364         }
    365         else if (value instanceof byte[])
    366         {
    367             out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">");
    368         }
    369         else if (value instanceof Date)
    370         {
    371             out.print(formatDate((Date) value));
    372         }
    373         else if (value != null)
    374         {
    375             out.print(quoteString(String.valueOf(value)));
    376         }
    377     }
    378
    379     /***
    380      * Quote the specified string if necessary, that's if the string contains:
    381      * <ul>
    382      *   <li>a space character (' ', '\t', '\r', '\n')</li>
    383      *   <li>a quote '"'</li>
    384      *   <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li>
    385      * </ul>
    386      * Quotes within the string are escaped.
    387      *
    388      * <p>Examples:</p>
    389      * <ul>
    390      *   <li>abcd -> abcd</li>
    391      *   <li>ab cd -> "ab cd"</li>
    392      *   <li>foo"bar -> "foo\"bar"</li>
    393      *   <li>foo;bar -> "foo;bar"</li>
    394      * </ul>
    395      */
    396     String quoteString(String s)
    397     {
    398         if (s == null)
    399         {
    400             return null;
    401         }
    402
    403         if (s.indexOf(' ') != -1
    404                 || s.indexOf('\t') != -1
    405                 || s.indexOf('\r') != -1
    406                 || s.indexOf('\n') != -1
    407                 || s.indexOf('"') != -1
    408                 || s.indexOf('(') != -1
    409                 || s.indexOf(')') != -1
    410                 || s.indexOf('{') != -1
    411                 || s.indexOf('}') != -1
    412                 || s.indexOf('=') != -1
    413                 || s.indexOf(',') != -1
    414                 || s.indexOf(';') != -1)
    415         {
    416             s = StringUtils.replace(s, "\"", "//\"");
    417             s = "\"" + s + "\"";
    418         }
    419
    420         return s;
    421     }
    422
    423     /***
    424      * Parses a date in a format like
    425      * <code>&lt;*D2002-03-22 11:30:00 +0100&gt;</code>.
    426      *
    427      * @param s the string with the date to be parsed
    428      * @return the parsed date
    429      * @throws ParseException if an error occurred while parsing the string
    430      */
    431     static Date parseDate(String s) throws ParseException
    432     {
    433         Calendar cal = Calendar.getInstance();
    434         cal.clear();
    435         int index = 0;
    436
    437         for (int i = 0; i < DATE_PARSERS.length; i++)
    438         {
    439             index += DATE_PARSERS[i].parseComponent(s, index, cal);
    440         }
    441
    442         return cal.getTime();
    443     }
    444
    445     /***
    446      * Returns a string representation for the date specified by the given
    447      * calendar.
    448      *
    449      * @param cal the calendar with the initialized date
    450      * @return a string for this date
    451      */
    452     static String formatDate(Calendar cal)
    453     {
    454         StringBuffer buf = new StringBuffer();
    455
    456         for (int i = 0; i < DATE_PARSERS.length; i++)
    457         {
    458             DATE_PARSERS[i].formatComponent(buf, cal);
    459         }
    460
    461         return buf.toString();
    462     }
    463
    464     /***
    465      * Returns a string representation for the specified date.
    466      *
    467      * @param date the date
    468      * @return a string for this date
    469      */
    470     static String formatDate(Date date)
    471     {
    472         Calendar cal = Calendar.getInstance();
    473         cal.setTime(date);
    474         return formatDate(cal);
    475     }
    476
    477     /***
    478      * A helper class for parsing and formatting date literals. Usually we would
    479      * use <code>SimpleDateFormat</code> for this purpose, but in Java 1.3 the
    480      * functionality of this class is limited. So we have a hierarchy of parser
    481      * classes instead that deal with the different components of a date
    482      * literal.
    483      */
    484     private abstract static class DateComponentParser
    485     {
    486         /***
    487          * Parses a component from the given input string.
    488          *
    489          * @param s the string to be parsed
    490          * @param index the current parsing position
    491          * @param cal the calendar where to store the result
    492          * @return the length of the processed component
    493          * @throws ParseException if the component cannot be extracted
    494          */
    495         public abstract int parseComponent(String s, int index, Calendar cal)
    496                 throws ParseException;
    497
    498         /***
    499          * Formats a date component. This method is used for converting a date
    500          * in its internal representation into a string literal.
    501          *
    502          * @param buf the target buffer
    503          * @param cal the calendar with the current date
    504          */
    505         public abstract void formatComponent(StringBuffer buf, Calendar cal);
    506
    507         /***
    508          * Checks whether the given string has at least <code>length</code>
    509          * characters starting from the given parsing position. If this is not
    510          * the case, an exception will be thrown.
    511          *
    512          * @param s the string to be tested
    513          * @param index the current index
    514          * @param length the minimum length after the index
    515          * @throws ParseException if the string is too short
    516          */
    517         protected void checkLength(String s, int index, int length)
    518                 throws ParseException
    519         {
    520             int len = (s == null) ? 0 : s.length();
    521             if (index + length > len)
    522             {
    523                 throw new ParseException("Input string too short: " + s
    524                         + ", index: " + index);
    525             }
    526         }
    527
    528         /***
    529          * Adds a number to the given string buffer and adds leading '0'
    530          * characters until the given length is reached.
    531          *
    532          * @param buf the target buffer
    533          * @param num the number to add
    534          * @param length the required length
    535          */
    536         protected void padNum(StringBuffer buf, int num, int length)
    537         {
    538             buf.append(StringUtils.leftPad(String.valueOf(num), length,
    539                     PAD_CHAR));
    540         }
    541     }
    542
    543     /***
    544      * A specialized date component parser implementation that deals with
    545      * numeric calendar fields. The class is able to extract fields from a
    546      * string literal and to format a literal from a calendar.
    547      */
    548     private static class DateFieldParser extends DateComponentParser
    549     {
    550         /*** Stores the calendar field to be processed. */
    551         private int calendarField;
    552
    553         /*** Stores the length of this field. */
    554         private int length;
    555
    556         /*** An optional offset to add to the calendar field. */
    557         private int offset;
    558
    559         /***
    560          * Creates a new instance of <code>DateFieldParser</code>.
    561          *
    562          * @param calFld the calendar field code
    563          * @param len the length of this field
    564          */
    565         public DateFieldParser(int calFld, int len)
    566         {
    567             this(calFld, len, 0);
    568         }
    569
    570         /***
    571          * Creates a new instance of <code>DateFieldParser</code> and fully
    572          * initializes it.
    573          *
    574          * @param calFld the calendar field code
    575          * @param len the length of this field
    576          * @param ofs an offset to add to the calendar field
    577          */
    578         public DateFieldParser(int calFld, int len, int ofs)
    579         {
    580             calendarField = calFld;
    581             length = len;
    582             offset = ofs;
    583         }
    584
    585         public void formatComponent(StringBuffer buf, Calendar cal)
    586         {
    587             padNum(buf, cal.get(calendarField) + offset, length);
    588         }
    589
    590         public int parseComponent(String s, int index, Calendar cal)
    591                 throws ParseException
    592         {
    593             checkLength(s, index, length);
    594             try
    595             {
    596                 cal.set(calendarField, Integer.parseInt(s.substring(index,
    597                         index + length))
    598                         - offset);
    599                 return length;
    600             }
    601             catch (NumberFormatException nfex)
    602             {
    603                 throw new ParseException("Invalid number: " + s + ", index "
    604                         + index);
    605             }
    606         }
    607     }
    608
    609     /***
    610      * A specialized date component parser implementation that deals with
    611      * separator characters.
    612      */
    613     private static class DateSeparatorParser extends DateComponentParser
    614     {
    615         /*** Stores the separator. */
    616         private String separator;
    617
    618         /***
    619          * Creates a new instance of <code>DateSeparatorParser</code> and sets
    620          * the separator string.
    621          *
    622          * @param sep the separator string
    623          */
    624         public DateSeparatorParser(String sep)
    625         {
    626             separator = sep;
    627         }
    628
    629         public void formatComponent(StringBuffer buf, Calendar cal)
    630         {
    631             buf.append(separator);
    632         }
    633
    634         public int parseComponent(String s, int index, Calendar cal)
    635                 throws ParseException
    636         {
    637             checkLength(s, index, separator.length());
    638             if (!s.startsWith(separator, index))
    639             {
    640                 throw new ParseException("Invalid input: " + s + ", index "
    641                         + index + ", expected " + separator);
    642             }
    643             return separator.length();
    644         }
    645     }
    646
    647     /***
    648      * A specialized date component parser implementation that deals with the
    649      * time zone part of a date component.
    650      */
    651     private static class DateTimeZoneParser extends DateComponentParser
    652     {
    653         public void formatComponent(StringBuffer buf, Calendar cal)
    654         {
    655             TimeZone tz = cal.getTimeZone();
    656             int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE;
    657             if (ofs < 0)
    658             {
    659                 buf.append('-');
    660                 ofs = -ofs;
    661             }
    662             else
    663             {
    664                 buf.append('+');
    665             }
    666             int hour = ofs / MINUTES_PER_HOUR;
    667             int min = ofs % MINUTES_PER_HOUR;
    668             padNum(buf, hour, 2);
    669             padNum(buf, min, 2);
    670         }
    671
    672         public int parseComponent(String s, int index, Calendar cal)
    673                 throws ParseException
    674         {
    675             checkLength(s, index, TIME_ZONE_LENGTH);
    676             TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX
    677                     + s.substring(index, index + TIME_ZONE_LENGTH));
    678             cal.setTimeZone(tz);
    679             return TIME_ZONE_LENGTH;
    680         }
    681     }
    682 }