| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.commons.configuration2.plist; |
| |
| import java.io.PrintWriter; |
| import java.io.Reader; |
| import java.io.Writer; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.TimeZone; |
| |
| import org.apache.commons.codec.binary.Hex; |
| import org.apache.commons.configuration2.BaseHierarchicalConfiguration; |
| import org.apache.commons.configuration2.Configuration; |
| import org.apache.commons.configuration2.FileBasedConfiguration; |
| import org.apache.commons.configuration2.HierarchicalConfiguration; |
| import org.apache.commons.configuration2.ImmutableConfiguration; |
| import org.apache.commons.configuration2.MapConfiguration; |
| import org.apache.commons.configuration2.ex.ConfigurationException; |
| import org.apache.commons.configuration2.tree.ImmutableNode; |
| import org.apache.commons.configuration2.tree.InMemoryNodeModel; |
| import org.apache.commons.configuration2.tree.NodeHandler; |
| import org.apache.commons.lang3.StringUtils; |
| |
| /** |
| * NeXT / OpenStep style configuration. This configuration can read and write |
| * ASCII plist files. It supports the GNUStep extension to specify date objects. |
| * <p> |
| * References: |
| * <ul> |
| * <li><a |
| * href="http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html"> |
| * Apple Documentation - Old-Style ASCII Property Lists</a></li> |
| * <li><a |
| * href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html"> |
| * GNUStep Documentation</a></li> |
| * </ul> |
| * |
| * <p>Example:</p> |
| * <pre> |
| * { |
| * foo = "bar"; |
| * |
| * array = ( value1, value2, value3 ); |
| * |
| * data = <4f3e0145ab>; |
| * |
| * date = <*D2007-05-05 20:05:00 +0100>; |
| * |
| * nested = |
| * { |
| * key1 = value1; |
| * key2 = value; |
| * nested = |
| * { |
| * foo = bar |
| * } |
| * } |
| * } |
| * </pre> |
| * |
| * @since 1.2 |
| * |
| */ |
| public class PropertyListConfiguration extends BaseHierarchicalConfiguration |
| implements FileBasedConfiguration |
| { |
| /** Constant for the separator parser for the date part. */ |
| private static final DateComponentParser DATE_SEPARATOR_PARSER = new DateSeparatorParser( |
| "-"); |
| |
| /** Constant for the separator parser for the time part. */ |
| private static final DateComponentParser TIME_SEPARATOR_PARSER = new DateSeparatorParser( |
| ":"); |
| |
| /** Constant for the separator parser for blanks between the parts. */ |
| private static final DateComponentParser BLANK_SEPARATOR_PARSER = new DateSeparatorParser( |
| " "); |
| |
| /** An array with the component parsers for dealing with dates. */ |
| private static final DateComponentParser[] DATE_PARSERS = |
| {new DateSeparatorParser("<*D"), new DateFieldParser(Calendar.YEAR, 4), |
| DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.MONTH, 2, 1), |
| DATE_SEPARATOR_PARSER, new DateFieldParser(Calendar.DATE, 2), |
| BLANK_SEPARATOR_PARSER, |
| new DateFieldParser(Calendar.HOUR_OF_DAY, 2), |
| TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.MINUTE, 2), |
| TIME_SEPARATOR_PARSER, new DateFieldParser(Calendar.SECOND, 2), |
| BLANK_SEPARATOR_PARSER, new DateTimeZoneParser(), |
| new DateSeparatorParser(">")}; |
| |
| /** Constant for the ID prefix for GMT time zones. */ |
| private static final String TIME_ZONE_PREFIX = "GMT"; |
| |
| /** Constant for the milliseconds of a minute.*/ |
| private static final int MILLIS_PER_MINUTE = 1000 * 60; |
| |
| /** Constant for the minutes per hour.*/ |
| private static final int MINUTES_PER_HOUR = 60; |
| |
| /** Size of the indentation for the generated file. */ |
| private static final int INDENT_SIZE = 4; |
| |
| /** Constant for the length of a time zone.*/ |
| private static final int TIME_ZONE_LENGTH = 5; |
| |
| /** Constant for the padding character in the date format.*/ |
| private static final char PAD_CHAR = '0'; |
| |
| /** |
| * Creates an empty PropertyListConfiguration object which can be |
| * used to synthesize a new plist file by adding values and |
| * then saving(). |
| */ |
| public PropertyListConfiguration() |
| { |
| } |
| |
| /** |
| * Creates a new instance of {@code PropertyListConfiguration} and |
| * copies the content of the specified configuration into this object. |
| * |
| * @param c the configuration to copy |
| * @since 1.4 |
| */ |
| public PropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> c) |
| { |
| super(c); |
| } |
| |
| /** |
| * Creates a new instance of {@code PropertyListConfiguration} with the |
| * given root node. |
| * |
| * @param root the root node |
| */ |
| PropertyListConfiguration(final ImmutableNode root) |
| { |
| super(new InMemoryNodeModel(root)); |
| } |
| |
| @Override |
| protected void setPropertyInternal(final String key, final Object value) |
| { |
| // special case for byte arrays, they must be stored as is in the configuration |
| if (value instanceof byte[]) |
| { |
| setDetailEvents(false); |
| try |
| { |
| clearProperty(key); |
| addPropertyDirect(key, value); |
| } |
| finally |
| { |
| setDetailEvents(true); |
| } |
| } |
| else |
| { |
| super.setPropertyInternal(key, value); |
| } |
| } |
| |
| @Override |
| protected void addPropertyInternal(final String key, final Object value) |
| { |
| if (value instanceof byte[]) |
| { |
| addPropertyDirect(key, value); |
| } |
| else |
| { |
| super.addPropertyInternal(key, value); |
| } |
| } |
| |
| @Override |
| public void read(final Reader in) throws ConfigurationException |
| { |
| final PropertyListParser parser = new PropertyListParser(in); |
| try |
| { |
| final PropertyListConfiguration config = parser.parse(); |
| getModel().setRootNode( |
| config.getNodeModel().getNodeHandler().getRootNode()); |
| } |
| catch (final ParseException e) |
| { |
| throw new ConfigurationException(e); |
| } |
| } |
| |
| @Override |
| public void write(final Writer out) throws ConfigurationException |
| { |
| final PrintWriter writer = new PrintWriter(out); |
| final NodeHandler<ImmutableNode> handler = getModel().getNodeHandler(); |
| printNode(writer, 0, handler.getRootNode(), handler); |
| writer.flush(); |
| } |
| |
| /** |
| * Append a node to the writer, indented according to a specific level. |
| */ |
| private void printNode(final PrintWriter out, final int indentLevel, |
| final ImmutableNode node, final NodeHandler<ImmutableNode> handler) |
| { |
| final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); |
| |
| if (node.getNodeName() != null) |
| { |
| out.print(padding + quoteString(node.getNodeName()) + " = "); |
| } |
| |
| final List<ImmutableNode> children = new ArrayList<>(node.getChildren()); |
| if (!children.isEmpty()) |
| { |
| // skip a line, except for the root dictionary |
| if (indentLevel > 0) |
| { |
| out.println(); |
| } |
| |
| out.println(padding + "{"); |
| |
| // display the children |
| final Iterator<ImmutableNode> it = children.iterator(); |
| while (it.hasNext()) |
| { |
| final ImmutableNode child = it.next(); |
| |
| printNode(out, indentLevel + 1, child, handler); |
| |
| // add a semi colon for elements that are not dictionaries |
| final Object value = child.getValue(); |
| if (value != null && !(value instanceof Map) && !(value instanceof Configuration)) |
| { |
| out.println(";"); |
| } |
| |
| // skip a line after arrays and dictionaries |
| if (it.hasNext() && (value == null || value instanceof List)) |
| { |
| out.println(); |
| } |
| } |
| |
| out.print(padding + "}"); |
| |
| // line feed if the dictionary is not in an array |
| if (handler.getParent(node) != null) |
| { |
| out.println(); |
| } |
| } |
| else if (node.getValue() == null) |
| { |
| out.println(); |
| out.print(padding + "{ };"); |
| |
| // line feed if the dictionary is not in an array |
| if (handler.getParent(node) != null) |
| { |
| out.println(); |
| } |
| } |
| else |
| { |
| // display the leaf value |
| final Object value = node.getValue(); |
| printValue(out, indentLevel, value); |
| } |
| } |
| |
| /** |
| * Append a value to the writer, indented according to a specific level. |
| */ |
| private void printValue(final PrintWriter out, final int indentLevel, final Object value) |
| { |
| final String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE); |
| |
| if (value instanceof List) |
| { |
| out.print("( "); |
| final Iterator<?> it = ((List<?>) value).iterator(); |
| while (it.hasNext()) |
| { |
| printValue(out, indentLevel + 1, it.next()); |
| if (it.hasNext()) |
| { |
| out.print(", "); |
| } |
| } |
| out.print(" )"); |
| } |
| else if (value instanceof PropertyListConfiguration) |
| { |
| final NodeHandler<ImmutableNode> handler = |
| ((PropertyListConfiguration) value).getModel() |
| .getNodeHandler(); |
| printNode(out, indentLevel, handler.getRootNode(), handler); |
| } |
| else if (value instanceof ImmutableConfiguration) |
| { |
| // display a flat Configuration as a dictionary |
| out.println(); |
| out.println(padding + "{"); |
| |
| final ImmutableConfiguration config = (ImmutableConfiguration) value; |
| final Iterator<String> it = config.getKeys(); |
| while (it.hasNext()) |
| { |
| final String key = it.next(); |
| final ImmutableNode node = |
| new ImmutableNode.Builder().name(key) |
| .value(config.getProperty(key)).create(); |
| final InMemoryNodeModel tempModel = new InMemoryNodeModel(node); |
| printNode(out, indentLevel + 1, node, tempModel.getNodeHandler()); |
| out.println(";"); |
| } |
| out.println(padding + "}"); |
| } |
| else if (value instanceof Map) |
| { |
| // display a Map as a dictionary |
| final Map<String, Object> map = transformMap((Map<?, ?>) value); |
| printValue(out, indentLevel, new MapConfiguration(map)); |
| } |
| else if (value instanceof byte[]) |
| { |
| out.print("<" + new String(Hex.encodeHex((byte[]) value)) + ">"); |
| } |
| else if (value instanceof Date) |
| { |
| out.print(formatDate((Date) value)); |
| } |
| else if (value != null) |
| { |
| out.print(quoteString(String.valueOf(value))); |
| } |
| } |
| |
| /** |
| * Quote the specified string if necessary, that's if the string contains: |
| * <ul> |
| * <li>a space character (' ', '\t', '\r', '\n')</li> |
| * <li>a quote '"'</li> |
| * <li>special characters in plist files ('(', ')', '{', '}', '=', ';', ',')</li> |
| * </ul> |
| * Quotes within the string are escaped. |
| * |
| * <p>Examples:</p> |
| * <ul> |
| * <li>abcd -> abcd</li> |
| * <li>ab cd -> "ab cd"</li> |
| * <li>foo"bar -> "foo\"bar"</li> |
| * <li>foo;bar -> "foo;bar"</li> |
| * </ul> |
| */ |
| String quoteString(String s) |
| { |
| if (s == null) |
| { |
| return null; |
| } |
| |
| if (s.indexOf(' ') != -1 |
| || s.indexOf('\t') != -1 |
| || s.indexOf('\r') != -1 |
| || s.indexOf('\n') != -1 |
| || s.indexOf('"') != -1 |
| || s.indexOf('(') != -1 |
| || s.indexOf(')') != -1 |
| || s.indexOf('{') != -1 |
| || s.indexOf('}') != -1 |
| || s.indexOf('=') != -1 |
| || s.indexOf(',') != -1 |
| || s.indexOf(';') != -1) |
| { |
| s = s.replace("\"", "\\\""); |
| s = "\"" + s + "\""; |
| } |
| |
| return s; |
| } |
| |
| /** |
| * Parses a date in a format like |
| * {@code <*D2002-03-22 11:30:00 +0100>}. |
| * |
| * @param s the string with the date to be parsed |
| * @return the parsed date |
| * @throws ParseException if an error occurred while parsing the string |
| */ |
| static Date parseDate(final String s) throws ParseException |
| { |
| final Calendar cal = Calendar.getInstance(); |
| cal.clear(); |
| int index = 0; |
| |
| for (final DateComponentParser parser : DATE_PARSERS) |
| { |
| index += parser.parseComponent(s, index, cal); |
| } |
| |
| return cal.getTime(); |
| } |
| |
| /** |
| * Returns a string representation for the date specified by the given |
| * calendar. |
| * |
| * @param cal the calendar with the initialized date |
| * @return a string for this date |
| */ |
| static String formatDate(final Calendar cal) |
| { |
| final StringBuilder buf = new StringBuilder(); |
| |
| for (final DateComponentParser element : DATE_PARSERS) |
| { |
| element.formatComponent(buf, cal); |
| } |
| |
| return buf.toString(); |
| } |
| |
| /** |
| * Returns a string representation for the specified date. |
| * |
| * @param date the date |
| * @return a string for this date |
| */ |
| static String formatDate(final Date date) |
| { |
| final Calendar cal = Calendar.getInstance(); |
| cal.setTime(date); |
| return formatDate(cal); |
| } |
| |
| /** |
| * Transform a map of arbitrary types into a map with string keys and object |
| * values. All keys of the source map which are not of type String are |
| * dropped. |
| * |
| * @param src the map to be converted |
| * @return the resulting map |
| */ |
| private static Map<String, Object> transformMap(final Map<?, ?> src) |
| { |
| final Map<String, Object> dest = new HashMap<>(); |
| for (final Map.Entry<?, ?> e : src.entrySet()) |
| { |
| if (e.getKey() instanceof String) |
| { |
| dest.put((String) e.getKey(), e.getValue()); |
| } |
| } |
| return dest; |
| } |
| |
| /** |
| * A helper class for parsing and formatting date literals. Usually we would |
| * use {@code SimpleDateFormat} for this purpose, but in Java 1.3 the |
| * functionality of this class is limited. So we have a hierarchy of parser |
| * classes instead that deal with the different components of a date |
| * literal. |
| */ |
| private abstract static class DateComponentParser |
| { |
| /** |
| * Parses a component from the given input string. |
| * |
| * @param s the string to be parsed |
| * @param index the current parsing position |
| * @param cal the calendar where to store the result |
| * @return the length of the processed component |
| * @throws ParseException if the component cannot be extracted |
| */ |
| public abstract int parseComponent(String s, int index, Calendar cal) |
| throws ParseException; |
| |
| /** |
| * Formats a date component. This method is used for converting a date |
| * in its internal representation into a string literal. |
| * |
| * @param buf the target buffer |
| * @param cal the calendar with the current date |
| */ |
| public abstract void formatComponent(StringBuilder buf, Calendar cal); |
| |
| /** |
| * Checks whether the given string has at least {@code length} |
| * characters starting from the given parsing position. If this is not |
| * the case, an exception will be thrown. |
| * |
| * @param s the string to be tested |
| * @param index the current index |
| * @param length the minimum length after the index |
| * @throws ParseException if the string is too short |
| */ |
| protected void checkLength(final String s, final int index, final int length) |
| throws ParseException |
| { |
| final int len = s == null ? 0 : s.length(); |
| if (index + length > len) |
| { |
| throw new ParseException("Input string too short: " + s |
| + ", index: " + index); |
| } |
| } |
| |
| /** |
| * Adds a number to the given string buffer and adds leading '0' |
| * characters until the given length is reached. |
| * |
| * @param buf the target buffer |
| * @param num the number to add |
| * @param length the required length |
| */ |
| protected void padNum(final StringBuilder buf, final int num, final int length) |
| { |
| buf.append(StringUtils.leftPad(String.valueOf(num), length, |
| PAD_CHAR)); |
| } |
| } |
| |
| /** |
| * A specialized date component parser implementation that deals with |
| * numeric calendar fields. The class is able to extract fields from a |
| * string literal and to format a literal from a calendar. |
| */ |
| private static class DateFieldParser extends DateComponentParser |
| { |
| /** Stores the calendar field to be processed. */ |
| private final int calendarField; |
| |
| /** Stores the length of this field. */ |
| private final int length; |
| |
| /** An optional offset to add to the calendar field. */ |
| private final int offset; |
| |
| /** |
| * Creates a new instance of {@code DateFieldParser}. |
| * |
| * @param calFld the calendar field code |
| * @param len the length of this field |
| */ |
| public DateFieldParser(final int calFld, final int len) |
| { |
| this(calFld, len, 0); |
| } |
| |
| /** |
| * Creates a new instance of {@code DateFieldParser} and fully |
| * initializes it. |
| * |
| * @param calFld the calendar field code |
| * @param len the length of this field |
| * @param ofs an offset to add to the calendar field |
| */ |
| public DateFieldParser(final int calFld, final int len, final int ofs) |
| { |
| calendarField = calFld; |
| length = len; |
| offset = ofs; |
| } |
| |
| @Override |
| public void formatComponent(final StringBuilder buf, final Calendar cal) |
| { |
| padNum(buf, cal.get(calendarField) + offset, length); |
| } |
| |
| @Override |
| public int parseComponent(final String s, final int index, final Calendar cal) |
| throws ParseException |
| { |
| checkLength(s, index, length); |
| try |
| { |
| cal.set(calendarField, Integer.parseInt(s.substring(index, |
| index + length)) |
| - offset); |
| return length; |
| } |
| catch (final NumberFormatException nfex) |
| { |
| throw new ParseException("Invalid number: " + s + ", index " |
| + index); |
| } |
| } |
| } |
| |
| /** |
| * A specialized date component parser implementation that deals with |
| * separator characters. |
| */ |
| private static class DateSeparatorParser extends DateComponentParser |
| { |
| /** Stores the separator. */ |
| private final String separator; |
| |
| /** |
| * Creates a new instance of {@code DateSeparatorParser} and sets |
| * the separator string. |
| * |
| * @param sep the separator string |
| */ |
| public DateSeparatorParser(final String sep) |
| { |
| separator = sep; |
| } |
| |
| @Override |
| public void formatComponent(final StringBuilder buf, final Calendar cal) |
| { |
| buf.append(separator); |
| } |
| |
| @Override |
| public int parseComponent(final String s, final int index, final Calendar cal) |
| throws ParseException |
| { |
| checkLength(s, index, separator.length()); |
| if (!s.startsWith(separator, index)) |
| { |
| throw new ParseException("Invalid input: " + s + ", index " |
| + index + ", expected " + separator); |
| } |
| return separator.length(); |
| } |
| } |
| |
| /** |
| * A specialized date component parser implementation that deals with the |
| * time zone part of a date component. |
| */ |
| private static class DateTimeZoneParser extends DateComponentParser |
| { |
| @Override |
| public void formatComponent(final StringBuilder buf, final Calendar cal) |
| { |
| final TimeZone tz = cal.getTimeZone(); |
| int ofs = tz.getRawOffset() / MILLIS_PER_MINUTE; |
| if (ofs < 0) |
| { |
| buf.append('-'); |
| ofs = -ofs; |
| } |
| else |
| { |
| buf.append('+'); |
| } |
| final int hour = ofs / MINUTES_PER_HOUR; |
| final int min = ofs % MINUTES_PER_HOUR; |
| padNum(buf, hour, 2); |
| padNum(buf, min, 2); |
| } |
| |
| @Override |
| public int parseComponent(final String s, final int index, final Calendar cal) |
| throws ParseException |
| { |
| checkLength(s, index, TIME_ZONE_LENGTH); |
| final TimeZone tz = TimeZone.getTimeZone(TIME_ZONE_PREFIX |
| + s.substring(index, index + TIME_ZONE_LENGTH)); |
| cal.setTimeZone(tz); |
| return TIME_ZONE_LENGTH; |
| } |
| } |
| } |