XMLPropertyListConfiguration.java
- /*
- * 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.math.BigDecimal;
- import java.math.BigInteger;
- import java.nio.charset.Charset;
- import java.nio.charset.StandardCharsets;
- import java.text.DateFormat;
- import java.text.ParseException;
- import java.text.SimpleDateFormat;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Base64;
- import java.util.Calendar;
- import java.util.Collection;
- import java.util.Date;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.LinkedList;
- 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.configuration2.BaseHierarchicalConfiguration;
- 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.ex.ConfigurationRuntimeException;
- import org.apache.commons.configuration2.io.FileLocator;
- import org.apache.commons.configuration2.io.FileLocatorAware;
- import org.apache.commons.configuration2.tree.ImmutableNode;
- import org.apache.commons.configuration2.tree.InMemoryNodeModel;
- import org.apache.commons.lang3.StringUtils;
- import org.apache.commons.text.StringEscapeUtils;
- 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 macOS 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
- */
- public class XMLPropertyListConfiguration extends BaseHierarchicalConfiguration implements FileBasedConfiguration, FileLocatorAware {
- /**
- * Container for array elements. <strong>Do not use this class !</strong> It is used internally by XMLPropertyConfiguration to
- * parse the configuration file, it may be removed at any moment in the future.
- */
- private static final class ArrayNodeBuilder extends PListNodeBuilder {
- /** The list of values in the array. */
- private final List<Object> list = new ArrayList<>();
- /**
- * Add an object to the array.
- *
- * @param value the value to be added
- */
- @Override
- public void addValue(final Object value) {
- list.add(value);
- }
- /**
- * Return the list of values in the array.
- *
- * @return the {@link List} of values
- */
- @Override
- protected Object getNodeValue() {
- return list;
- }
- }
- /**
- * A specialized builder class with addXXX methods to parse the typed data passed by the SAX handler. It is used for
- * creating the nodes of the configuration.
- */
- private static class PListNodeBuilder {
- /**
- * The MacOS FORMAT of dates in plist files. Note: Because {@code SimpleDateFormat} is not thread-safe, each access has
- * to be synchronized.
- */
- private static final 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. Note: Because {@code SimpleDateFormat} is not thread-safe, each access
- * has to be synchronized.
- */
- private static final DateFormat GNUSTEP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
- /** A collection with child builders of this builder. */
- private final Collection<PListNodeBuilder> childBuilders = new LinkedList<>();
- /** The name of the represented node. */
- private String name;
- /** The current value of the represented node. */
- private Object value;
- /**
- * Adds the given child builder to this builder.
- *
- * @param child the child builder to be added
- */
- public void addChild(final PListNodeBuilder child) {
- childBuilders.add(child);
- }
- /**
- * 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(final String value) {
- addValue(Base64.getMimeDecoder().decode(value.getBytes(DATA_ENCODING)));
- }
- /**
- * Parse the specified string as a date and add it to the values of the node.
- *
- * @param value the value to be added
- * @throws IllegalArgumentException if the date string cannot be parsed
- */
- public void addDateValue(final String value) {
- try {
- if (value.indexOf(' ') != -1) {
- // parse the date using the GNUstep FORMAT
- synchronized (GNUSTEP_FORMAT) {
- addValue(GNUSTEP_FORMAT.parse(value));
- }
- } else {
- // parse the date using the MacOS X FORMAT
- synchronized (FORMAT) {
- addValue(FORMAT.parse(value));
- }
- }
- } catch (final ParseException e) {
- throw new IllegalArgumentException(String.format("'%s' cannot be parsed to a date!", value), e);
- }
- }
- /**
- * Add a boolean value 'false' to the values of the node.
- */
- public void addFalseValue() {
- addValue(Boolean.FALSE);
- }
- /**
- * 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(final String value) {
- addValue(new BigInteger(value));
- }
- /**
- * 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(final ArrayNodeBuilder node) {
- addValue(node.getNodeValue());
- }
- /**
- * 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(final String value) {
- addValue(new BigDecimal(value));
- }
- /**
- * Add a boolean value 'true' to the values of the node.
- */
- public void addTrueValue() {
- addValue(Boolean.TRUE);
- }
- /**
- * 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 v the value to be added
- */
- public void addValue(final Object v) {
- if (value == null) {
- value = v;
- } else if (value instanceof Collection) {
- // This is safe because we create the collections ourselves
- @SuppressWarnings("unchecked")
- final Collection<Object> collection = (Collection<Object>) value;
- collection.add(v);
- } else {
- final List<Object> list = new ArrayList<>();
- list.add(value);
- list.add(v);
- value = list;
- }
- }
- /**
- * Creates the configuration node defined by this builder.
- *
- * @return the newly created configuration node
- */
- public ImmutableNode createNode() {
- final ImmutableNode.Builder nodeBuilder = new ImmutableNode.Builder(childBuilders.size());
- childBuilders.forEach(child -> nodeBuilder.addChild(child.createNode()));
- return nodeBuilder.name(name).value(getNodeValue()).create();
- }
- /**
- * Gets the final value for the node to be created. This method is called when the represented configuration node is
- * actually created.
- *
- * @return the value of the resulting configuration node
- */
- protected Object getNodeValue() {
- return value;
- }
- /**
- * Sets the name of the represented node.
- *
- * @param nodeName the node name
- */
- public void setName(final String nodeName) {
- name = nodeName;
- }
- }
- /**
- * SAX Handler to build the configuration nodes while the document is being parsed.
- */
- private final class XMLPropertyListHandler extends DefaultHandler {
- /** The buffer containing the text node being read */
- private final StringBuilder buffer = new StringBuilder();
- /** The stack of configuration nodes */
- private final List<PListNodeBuilder> stack = new ArrayList<>();
- /** The builder for the resulting node. */
- private final PListNodeBuilder resultBuilder;
- public XMLPropertyListHandler() {
- resultBuilder = new PListNodeBuilder();
- push(resultBuilder);
- }
- @Override
- public void characters(final char[] ch, final int start, final int length) throws SAXException {
- buffer.append(ch, start, length);
- }
- @Override
- public void endElement(final String uri, final String localName, final String qName) throws SAXException {
- if ("key".equals(qName)) {
- // create a new node, link it to its parent and push it on the stack
- final PListNodeBuilder node = new PListNodeBuilder();
- node.setName(buffer.toString());
- peekNE().addChild(node);
- push(node);
- } else if ("dict".equals(qName)) {
- // remove the root of the XMLPropertyListConfiguration previously pushed on the stack
- final PListNodeBuilder builder = pop();
- assert builder != null : "Stack was empty!";
- if (peek() instanceof ArrayNodeBuilder) {
- // create the configuration
- final XMLPropertyListConfiguration config = new XMLPropertyListConfiguration(builder.createNode());
- // add it to the ArrayNodeBuilder
- final ArrayNodeBuilder node = (ArrayNodeBuilder) peekNE();
- node.addValue(config);
- }
- } else {
- switch (qName) {
- case "string":
- peekNE().addValue(buffer.toString());
- break;
- case "integer":
- peekNE().addIntegerValue(buffer.toString());
- break;
- case "real":
- peekNE().addRealValue(buffer.toString());
- break;
- case "true":
- peekNE().addTrueValue();
- break;
- case "false":
- peekNE().addFalseValue();
- break;
- case "data":
- peekNE().addDataValue(buffer.toString());
- break;
- case "date":
- try {
- peekNE().addDateValue(buffer.toString());
- } catch (final IllegalArgumentException iex) {
- getLogger().warn("Ignoring invalid date property " + buffer);
- }
- break;
- case "array": {
- final ArrayNodeBuilder array = (ArrayNodeBuilder) pop();
- peekNE().addList(array);
- break;
- }
- default:
- break;
- }
- // 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 ArrayNodeBuilder)) {
- pop();
- }
- }
- buffer.setLength(0);
- }
- /**
- * Gets the builder for the result node.
- *
- * @return the result node builder
- */
- public PListNodeBuilder getResultBuilder() {
- return resultBuilder;
- }
- /**
- * Return the node on the top of the stack.
- */
- private PListNodeBuilder peek() {
- if (!stack.isEmpty()) {
- return stack.get(stack.size() - 1);
- }
- return null;
- }
- /**
- * Returns the node on top of the non-empty stack. Throws an exception if the stack is empty.
- *
- * @return the top node of the stack
- * @throws ConfigurationRuntimeException if the stack is empty
- */
- private PListNodeBuilder peekNE() {
- final PListNodeBuilder result = peek();
- if (result == null) {
- throw new ConfigurationRuntimeException("Access to empty stack!");
- }
- return result;
- }
- /**
- * Remove and return the node on the top of the stack.
- */
- private PListNodeBuilder pop() {
- if (!stack.isEmpty()) {
- return stack.remove(stack.size() - 1);
- }
- return null;
- }
- /**
- * Put a node on the top of the stack.
- */
- private void push(final PListNodeBuilder node) {
- stack.add(node);
- }
- @Override
- public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
- if ("array".equals(qName)) {
- push(new ArrayNodeBuilder());
- } else if ("dict".equals(qName) && peek() instanceof ArrayNodeBuilder) {
- // push the new root builder on the stack
- push(new PListNodeBuilder());
- }
- }
- }
- /** Size of the indentation for the generated file. */
- private static final int INDENT_SIZE = 4;
- /** Constant for the encoding for binary data. */
- private static final Charset DATA_ENCODING = StandardCharsets.UTF_8;
- /**
- * 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;
- }
- /** Temporarily stores the current file location. */
- private FileLocator locator;
- /**
- * Creates an empty XMLPropertyListConfiguration object which can be used to synthesize a new plist file by adding
- * values and then saving().
- */
- public XMLPropertyListConfiguration() {
- }
- /**
- * Creates a new instance of {@code XMLPropertyListConfiguration} and copies the content of the specified configuration
- * into this object.
- *
- * @param configuration the configuration to copy
- * @since 1.4
- */
- public XMLPropertyListConfiguration(final HierarchicalConfiguration<ImmutableNode> configuration) {
- super(configuration);
- }
- /**
- * Creates a new instance of {@code XMLPropertyConfiguration} with the given root node.
- *
- * @param root the root node
- */
- XMLPropertyListConfiguration(final ImmutableNode root) {
- super(new InMemoryNodeModel(root));
- }
- @Override
- protected void addPropertyInternal(final String key, final Object value) {
- if (value instanceof byte[] || value instanceof List) {
- addPropertyDirect(key, value);
- } else if (value instanceof Object[]) {
- addPropertyDirect(key, Arrays.asList((Object[]) value));
- } else {
- super.addPropertyInternal(key, value);
- }
- }
- /**
- * Stores the current file locator. This method is called before I/O operations.
- *
- * @param locator the current {@code FileLocator}
- */
- @Override
- public void initFileLocator(final FileLocator locator) {
- this.locator = locator;
- }
- /**
- * 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 String padding = StringUtils.repeat(" ", indentLevel * INDENT_SIZE);
- if (node.getNodeName() != null) {
- out.println(padding + "<key>" + StringEscapeUtils.escapeXml10(node.getNodeName()) + "</key>");
- }
- final List<ImmutableNode> children = node.getChildren();
- if (!children.isEmpty()) {
- out.println(padding + "<dict>");
- final Iterator<ImmutableNode> it = children.iterator();
- while (it.hasNext()) {
- final ImmutableNode child = 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 {
- 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 Date) {
- synchronized (PListNodeBuilder.FORMAT) {
- out.println(padding + "<date>" + PListNodeBuilder.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>");
- ((List<?>) value).forEach(o -> printValue(out, indentLevel + 1, o));
- out.println(padding + "</array>");
- } else if (value instanceof HierarchicalConfiguration) {
- // This is safe because we have created this configuration
- @SuppressWarnings("unchecked")
- final HierarchicalConfiguration<ImmutableNode> config = (HierarchicalConfiguration<ImmutableNode>) value;
- printNode(out, indentLevel, config.getNodeModel().getNodeHandler().getRootNode());
- } else if (value instanceof ImmutableConfiguration) {
- // display a flat Configuration as a dictionary
- out.println(padding + "<dict>");
- final ImmutableConfiguration config = (ImmutableConfiguration) value;
- final Iterator<String> it = config.getKeys();
- while (it.hasNext()) {
- // create a node for each property
- final String key = it.next();
- final ImmutableNode node = new ImmutableNode.Builder().name(key).value(config.getProperty(key)).create();
- // 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
- final Map<String, Object> map = transformMap((Map<?, ?>) value);
- printValue(out, indentLevel, new MapConfiguration(map));
- } else if (value instanceof byte[]) {
- final String base64 = new String(Base64.getMimeEncoder().encode((byte[]) value), DATA_ENCODING);
- out.println(padding + "<data>" + StringEscapeUtils.escapeXml10(base64) + "</data>");
- } else if (value != null) {
- out.println(padding + "<string>" + StringEscapeUtils.escapeXml10(String.valueOf(value)) + "</string>");
- } else {
- out.println(padding + "<string/>");
- }
- }
- @Override
- public void read(final Reader in) throws ConfigurationException {
- // set up the DTD validation
- final EntityResolver resolver = (publicId, systemId) -> new InputSource(getClass().getClassLoader().getResourceAsStream("PropertyList-1.0.dtd"));
- // parse the file
- final XMLPropertyListHandler handler = new XMLPropertyListHandler();
- try {
- final SAXParserFactory factory = SAXParserFactory.newInstance();
- factory.setValidating(true);
- final SAXParser parser = factory.newSAXParser();
- parser.getXMLReader().setEntityResolver(resolver);
- parser.getXMLReader().setContentHandler(handler);
- parser.getXMLReader().parse(new InputSource(in));
- getNodeModel().mergeRoot(handler.getResultBuilder().createNode(), null, null, null, this);
- } catch (final Exception e) {
- throw new ConfigurationException("Unable to parse the configuration file", e);
- }
- }
- private void setPropertyDirect(final String key, final Object value) {
- setDetailEvents(false);
- try {
- clearProperty(key);
- addPropertyDirect(key, value);
- } finally {
- setDetailEvents(true);
- }
- }
- @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[] || value instanceof List) {
- setPropertyDirect(key, value);
- } else if (value instanceof Object[]) {
- setPropertyDirect(key, Arrays.asList((Object[]) value));
- } else {
- super.setPropertyInternal(key, value);
- }
- }
- @Override
- public void write(final Writer out) throws ConfigurationException {
- if (locator == null) {
- throw new ConfigurationException(
- "Save operation not properly " + "initialized! Do not call write(Writer) directly," + " but use a FileHandler to save a configuration.");
- }
- final PrintWriter writer = new PrintWriter(out);
- if (locator.getEncoding() != null) {
- writer.println("<?xml version=\"1.0\" encoding=\"" + locator.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, getNodeModel().getNodeHandler().getRootNode());
- writer.println("</plist>");
- writer.flush();
- }
- }