/*
* 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.configuration.plist;
import java.io.File;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.configuration.AbstractHierarchicalFileConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.commons.configuration.MapConfiguration;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Property list file (plist) in XML format as used by Mac OS X (http://www.apple.com/DTDs/PropertyList-1.0.dtd).
* This configuration doesn't support the binary format used in OS X 10.4.
*
* <p>Example:</p>
* <pre>
* <?xml version="1.0"?>
* <!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
* <plist version="1.0">
* <dict>
* <key>string</key>
* <string>value1</string>
*
* <key>integer</key>
* <integer>12345</integer>
*
* <key>real</key>
* <real>-123.45E-1</real>
*
* <key>boolean</key>
* <true/>
*
* <key>date</key>
* <date>2005-01-01T12:00:00Z</date>
*
* <key>data</key>
* <data>RHJhY28gRG9ybWllbnMgTnVucXVhbSBUaXRpbGxhbmR1cw==</data>
*
* <key>array</key>
* <array>
* <string>value1</string>
* <string>value2</string>
* <string>value3</string>
* </array>
*
* <key>dictionnary</key>
* <dict>
* <key>key1</key>
* <string>value1</string>
* <key>key2</key>
* <string>value2</string>
* <key>key3</key>
* <string>value3</string>
* </dict>
*
* <key>nested</key>
* <dict>
* <key>node1</key>
* <dict>
* <key>node2</key>
* <dict>
* <key>node3</key>
* <string>value</string>
* </dict>
* </dict>
* </dict>
*
* </dict>
* </plist>
* </pre>
*
* @since 1.2
*
* @author Emmanuel Bourg
* @version $Revision: 902596 $, $Date: 2010-01-24 17:28:55 +0100 (So, 24. Jan 2010) $
*/
public class XMLPropertyListConfiguration extends AbstractHierarchicalFileConfiguration
{
/**
* The serial version UID.
*/
private static final long serialVersionUID = -3162063751042475985L;
/** Size of the indentation for the generated file. */
private static final int INDENT_SIZE = 4;
/**
* Creates an empty XMLPropertyListConfiguration object which can be
* used to synthesize a new plist file by adding values and
* then saving().
*/
public XMLPropertyListConfiguration()
{
initRoot();
}
/**
* Creates a new instance of <code>XMLPropertyListConfiguration</code> and
* copies the content of the specified configuration into this object.
*
* @param configuration the configuration to copy
* @since 1.4
*/
public XMLPropertyListConfiguration(HierarchicalConfiguration configuration)
{
super(configuration);
}
/**
* Creates and loads the property list from the specified file.
*
* @param fileName The name of the plist file to load.
* @throws org.apache.commons.configuration.ConfigurationException Error
* while loading the plist file
*/
public XMLPropertyListConfiguration(String fileName) throws ConfigurationException
{
super(fileName);
}
/**
* Creates and loads the property list from the specified file.
*
* @param file The plist file to load.
* @throws ConfigurationException Error while loading the plist file
*/
public XMLPropertyListConfiguration(File file) throws ConfigurationException
{
super(file);
}
/**
* Creates and loads the property list from the specified URL.
*
* @param url The location of the plist file to load.
* @throws ConfigurationException Error while loading the plist file
*/
public XMLPropertyListConfiguration(URL url) throws ConfigurationException
{
super(url);
}
public void setProperty(String key, Object value)
{
// special case for byte arrays, they must be stored as is in the configuration
if (value instanceof byte[])
{
fireEvent(EVENT_SET_PROPERTY, key, value, true);
setDetailEvents(false);
try
{
clearProperty(key);
addPropertyDirect(key, value);
}
finally
{
setDetailEvents(true);
}
fireEvent(EVENT_SET_PROPERTY, key, value, false);
}
else
{
super.setProperty(key, value);
}
}
public void addProperty(String key, Object value)
{
if (value instanceof byte[])
{
fireEvent(EVENT_ADD_PROPERTY, key, value, true);
addPropertyDirect(key, value);
fireEvent(EVENT_ADD_PROPERTY, key, value, false);
}
else
{
super.addProperty(key, value);
}
}
public void load(Reader in) throws ConfigurationException
{
// We have to make sure that the root node is actually a PListNode.
// If this object was not created using the standard constructor, the
// root node is a plain Node.
if (!(getRootNode() instanceof PListNode))
{
initRoot();
}
// set up the DTD validation
EntityResolver resolver = new EntityResolver()
{
public InputSource resolveEntity(String publicId, String systemId)
{
return new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
}
};
// parse the file
XMLPropertyListHandler handler = new XMLPropertyListHandler(getRoot());
try
{
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(true);
SAXParser parser = factory.newSAXParser();
parser.getXMLReader().setEntityResolver(resolver);
parser.getXMLReader().setContentHandler(handler);
parser.getXMLReader().parse(new InputSource(in));
}
catch (Exception e)
{
throw new ConfigurationException("Unable to parse the configuration file", e);
}
}
public void save(Writer out) throws ConfigurationException
{
PrintWriter writer = new PrintWriter(out);
if (getEncoding() != null)
{
writer.println("<?xml version=\"1.0\" encoding=\"" + getEncoding() + "\"?>");
}
else
{
writer.println("<?xml version=\"1.0\"?>");
}
writer.println("<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">");
writer.println("<plist version=\"1.0\">");
printNode(writer, 1, getRoot());
writer.println("</plist>");
writer.flush();
}
/**
* Append a node to the writer, indented according to a specific level.
*/
private void printNode(PrintWriter out, int indentLevel, Node node)
{
String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
if (node.getName() != null)
{
out.println(padding + "<key>" + StringEscapeUtils.escapeXml(node.getName()) + "</key>");
}
List children = node.getChildren();
if (!children.isEmpty())
{
out.println(padding + "<dict>");
Iterator it = children.iterator();
while (it.hasNext())
{
Node child = (Node) it.next();
printNode(out, indentLevel + 1, child);
if (it.hasNext())
{
out.println();
}
}
out.println(padding + "</dict>");
}
else if (node.getValue() == null)
{
out.println(padding + "<dict/>");
}
else
{
Object value = node.getValue();
printValue(out, indentLevel, value);
}
}
/**
* Append a value to the writer, indented according to a specific level.
*/
private void printValue(PrintWriter out, int indentLevel, Object value)
{
String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
if (value instanceof Date)
{
synchronized (PListNode.format)
{
out.println(padding + "<date>" + PListNode.format.format((Date) value) + "</date>");
}
}
else if (value instanceof Calendar)
{
printValue(out, indentLevel, ((Calendar) value).getTime());
}
else if (value instanceof Number)
{
if (value instanceof Double || value instanceof Float || value instanceof BigDecimal)
{
out.println(padding + "<real>" + value.toString() + "</real>");
}
else
{
out.println(padding + "<integer>" + value.toString() + "</integer>");
}
}
else if (value instanceof Boolean)
{
if (((Boolean) value).booleanValue())
{
out.println(padding + "<true/>");
}
else
{
out.println(padding + "<false/>");
}
}
else if (value instanceof List)
{
out.println(padding + "<array>");
Iterator it = ((List) value).iterator();
while (it.hasNext())
{
printValue(out, indentLevel + 1, it.next());
}
out.println(padding + "</array>");
}
else if (value instanceof HierarchicalConfiguration)
{
printNode(out, indentLevel, ((HierarchicalConfiguration) value).getRoot());
}
else if (value instanceof Configuration)
{
// display a flat Configuration as a dictionary
out.println(padding + "<dict>");
Configuration config = (Configuration) value;
Iterator it = config.getKeys();
while (it.hasNext())
{
// create a node for each property
String key = (String) it.next();
Node node = new Node(key);
node.setValue(config.getProperty(key));
// print the node
printNode(out, indentLevel + 1, node);
if (it.hasNext())
{
out.println();
}
}
out.println(padding + "</dict>");
}
else if (value instanceof Map)
{
// display a Map as a dictionary
Map map = (Map) value;
printValue(out, indentLevel, new MapConfiguration(map));
}
else if (value instanceof byte[])
{
String base64 = new String(Base64.encodeBase64((byte[]) value));
out.println(padding + "<data>" + StringEscapeUtils.escapeXml(base64) + "</data>");
}
else if (value != null)
{
out.println(padding + "<string>" + StringEscapeUtils.escapeXml(String.valueOf(value)) + "</string>");
}
else
{
out.println(padding + "<string/>");
}
}
/**
* Helper method for initializing the configuration's root node.
*/
private void initRoot()
{
setRootNode(new PListNode());
}
/**
* SAX Handler to build the configuration nodes while the document is being parsed.
*/
private static class XMLPropertyListHandler extends DefaultHandler
{
/** The buffer containing the text node being read */
private StringBuffer buffer = new StringBuffer();
/** The stack of configuration nodes */
private List stack = new ArrayList();
public XMLPropertyListHandler(Node root)
{
push(root);
}
/**
* Return the node on the top of the stack.
*/
private Node peek()
{
if (!stack.isEmpty())
{
return (Node) stack.get(stack.size() - 1);
}
else
{
return null;
}
}
/**
* Remove and return the node on the top of the stack.
*/
private Node pop()
{
if (!stack.isEmpty())
{
return (Node) stack.remove(stack.size() - 1);
}
else
{
return null;
}
}
/**
* Put a node on the top of the stack.
*/
private void push(Node node)
{
stack.add(node);
}
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
{
if ("array".equals(qName))
{
push(new ArrayNode());
}
else if ("dict".equals(qName))
{
if (peek() instanceof ArrayNode)
{
// create the configuration
XMLPropertyListConfiguration config = new XMLPropertyListConfiguration();
// add it to the ArrayNode
ArrayNode node = (ArrayNode) peek();
node.addValue(config);
// push the root on the stack
push(config.getRoot());
}
}
}
public void endElement(String uri, String localName, String qName) throws SAXException
{
if ("key".equals(qName))
{
// create a new node, link it to its parent and push it on the stack
PListNode node = new PListNode();
node.setName(buffer.toString());
peek().addChild(node);
push(node);
}
else if ("dict".equals(qName))
{
// remove the root of the XMLPropertyListConfiguration previously pushed on the stack
pop();
}
else
{
if ("string".equals(qName))
{
((PListNode) peek()).addValue(buffer.toString());
}
else if ("integer".equals(qName))
{
((PListNode) peek()).addIntegerValue(buffer.toString());
}
else if ("real".equals(qName))
{
((PListNode) peek()).addRealValue(buffer.toString());
}
else if ("true".equals(qName))
{
((PListNode) peek()).addTrueValue();
}
else if ("false".equals(qName))
{
((PListNode) peek()).addFalseValue();
}
else if ("data".equals(qName))
{
((PListNode) peek()).addDataValue(buffer.toString());
}
else if ("date".equals(qName))
{
((PListNode) peek()).addDateValue(buffer.toString());
}
else if ("array".equals(qName))
{
ArrayNode array = (ArrayNode) pop();
((PListNode) peek()).addList(array);
}
// remove the plist node on the stack once the value has been parsed,
// array nodes remains on the stack for the next values in the list
if (!(peek() instanceof ArrayNode))
{
pop();
}
}
buffer.setLength(0);
}
public void characters(char[] ch, int start, int length) throws SAXException
{
buffer.append(ch, start, length);
}
}
/**
* Node extension with addXXX methods to parse the typed data passed by the SAX handler.
* <b>Do not use this class !</b> It is used internally by XMLPropertyConfiguration
* to parse the configuration file, it may be removed at any moment in the future.
*/
public static class PListNode extends Node
{
/**
* The serial version UID.
*/
private static final long serialVersionUID = -7614060264754798317L;
/** The MacOS format of dates in plist files. */
private static DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
static
{
format.setTimeZone(TimeZone.getTimeZone("UTC"));
}
/** The GNUstep format of dates in plist files. */
private static DateFormat gnustepFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
/**
* Update the value of the node. If the existing value is null, it's
* replaced with the new value. If the existing value is a list, the
* specified value is appended to the list. If the existing value is
* not null, a list with the two values is built.
*
* @param value the value to be added
*/
public void addValue(Object value)
{
if (getValue() == null)
{
setValue(value);
}
else if (getValue() instanceof Collection)
{
Collection collection = (Collection) getValue();
collection.add(value);
}
else
{
List list = new ArrayList();
list.add(getValue());
list.add(value);
setValue(list);
}
}
/**
* Parse the specified string as a date and add it to the values of the node.
*
* @param value the value to be added
*/
public void addDateValue(String value)
{
try
{
if (value.indexOf(' ') != -1)
{
// parse the date using the GNUstep format
synchronized (gnustepFormat)
{
addValue(gnustepFormat.parse(value));
}
}
else
{
// parse the date using the MacOS X format
synchronized (format)
{
addValue(format.parse(value));
}
}
}
catch (ParseException e)
{
// ignore
;
}
}
/**
* Parse the specified string as a byte array in base 64 format
* and add it to the values of the node.
*
* @param value the value to be added
*/
public void addDataValue(String value)
{
addValue(Base64.decodeBase64(value.getBytes()));
}
/**
* Parse the specified string as an Interger and add it to the values of the node.
*
* @param value the value to be added
*/
public void addIntegerValue(String value)
{
addValue(new BigInteger(value));
}
/**
* Parse the specified string as a Double and add it to the values of the node.
*
* @param value the value to be added
*/
public void addRealValue(String value)
{
addValue(new BigDecimal(value));
}
/**
* Add a boolean value 'true' to the values of the node.
*/
public void addTrueValue()
{
addValue(Boolean.TRUE);
}
/**
* Add a boolean value 'false' to the values of the node.
*/
public void addFalseValue()
{
addValue(Boolean.FALSE);
}
/**
* Add a sublist to the values of the node.
*
* @param node the node whose value will be added to the current node value
*/
public void addList(ArrayNode node)
{
addValue(node.getValue());
}
}
/**
* Container for array elements. <b>Do not use this class !</b>
* It is used internally by XMLPropertyConfiguration to parse the
* configuration file, it may be removed at any moment in the future.
*/
public static class ArrayNode extends PListNode
{
/**
* The serial version UID.
*/
private static final long serialVersionUID = 5586544306664205835L;
/** The list of values in the array. */
private List list = new ArrayList();
/**
* Add an object to the array.
*
* @param value the value to be added
*/
public void addValue(Object value)
{
list.add(value);
}
/**
* Return the list of values in the array.
*
* @return the {@link List} of values
*/
public Object getValue()
{
return list;
}
}
}
|