From f3b780054ad52fba97bcf37132e44f4964535ad9 Mon Sep 17 00:00:00 2001 From: Luck Date: Tue, 30 Jan 2018 21:39:44 +0000 Subject: [PATCH] Implement XML configuration loader --- .../AttributedConfigurationNode.java | 112 +++++++++++ .../SimpleAttributedConfigurationNode.java | 188 ++++++++++++++++++ configurate-xml/pom.xml | 41 ++++ .../xml/XMLConfigurationLoader.java | 181 +++++++++++++++++ .../xml/XMLConfigurationLoaderTest.java | 87 ++++++++ .../src/test/resources/example.xml | 14 ++ pom.xml | 1 + 7 files changed, 624 insertions(+) create mode 100644 configurate-core/src/main/java/ninja/leaping/configurate/attributed/AttributedConfigurationNode.java create mode 100644 configurate-core/src/main/java/ninja/leaping/configurate/attributed/SimpleAttributedConfigurationNode.java create mode 100644 configurate-xml/pom.xml create mode 100644 configurate-xml/src/main/java/ninja/leaping/configurate/xml/XMLConfigurationLoader.java create mode 100644 configurate-xml/src/test/java/ninja/leaping/configurate/xml/XMLConfigurationLoaderTest.java create mode 100644 configurate-xml/src/test/resources/example.xml diff --git a/configurate-core/src/main/java/ninja/leaping/configurate/attributed/AttributedConfigurationNode.java b/configurate-core/src/main/java/ninja/leaping/configurate/attributed/AttributedConfigurationNode.java new file mode 100644 index 000000000..4a3ea0415 --- /dev/null +++ b/configurate-core/src/main/java/ninja/leaping/configurate/attributed/AttributedConfigurationNode.java @@ -0,0 +1,112 @@ +/** + * Configurate + * Copyright (C) zml and Configurate contributors + * + * Licensed 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 ninja.leaping.configurate.attributed; + +import ninja.leaping.configurate.ConfigurationNode; + +import java.util.List; +import java.util.Map; + +/** + * A configuration node that is capable of having attributes + */ +public interface AttributedConfigurationNode extends ConfigurationNode { + + /** + * Gets the tag name of this node. + * + * @return the tag name + */ + String getTagName(); + + /** + * Sets the tag name of this node. + * + *

Will have no effect when called on nodes within a + * {@link #getChildrenMap() children map}.

+ * + * @param name the name to set, cannot be null + * @return this + */ + AttributedConfigurationNode setTagName(String name); + + /** + * Adds an attribute to this node + * + * @param name the name of the attribute + * @param value the value of the attribute + * @return this + */ + AttributedConfigurationNode addAttribute(String name, String value); + + /** + * Removes an attribute from this node + * + * @param name the name of the attribute to remove + * @return this + */ + AttributedConfigurationNode removeAttribute(String name); + + /** + * Sets the attributes of this node + * + * @param attributes the attributes to set + * @return this + */ + AttributedConfigurationNode setAttributes(Map attributes); + + /** + * Gets if this node has any attributes + * + * @return true if this node has any attributes + */ + boolean hasAttributes(); + + /** + * Gets the value of an attribute, or null if this node doesn't have the + * given attribute + * + * @param name the name of the attribute to get + * @return this + */ + String getAttribute(String name); + + /** + * Gets the attributes this node has + * + *

The returned map is immutable.

+ * + * @return this + */ + Map getAttributes(); + + // Methods from superclass overridden to have correct return types + + AttributedConfigurationNode getParent(); + @Override + List getChildrenList(); + @Override + Map getChildrenMap(); + @Override + AttributedConfigurationNode setValue(Object value); + @Override + AttributedConfigurationNode mergeValuesFrom(ConfigurationNode other); + @Override + AttributedConfigurationNode getAppendedNode(); + @Override + AttributedConfigurationNode getNode(Object... path); +} diff --git a/configurate-core/src/main/java/ninja/leaping/configurate/attributed/SimpleAttributedConfigurationNode.java b/configurate-core/src/main/java/ninja/leaping/configurate/attributed/SimpleAttributedConfigurationNode.java new file mode 100644 index 000000000..9a5dc57b7 --- /dev/null +++ b/configurate-core/src/main/java/ninja/leaping/configurate/attributed/SimpleAttributedConfigurationNode.java @@ -0,0 +1,188 @@ +/** + * Configurate + * Copyright (C) zml and Configurate contributors + * + * Licensed 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 ninja.leaping.configurate.attributed; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.ConfigurationOptions; +import ninja.leaping.configurate.SimpleConfigurationNode; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a configuration node containing comments + */ +public class SimpleAttributedConfigurationNode extends SimpleConfigurationNode implements AttributedConfigurationNode { + private String tagName; + private final Map attributes = new HashMap<>(); + + public static SimpleAttributedConfigurationNode root() { + return root("root", ConfigurationOptions.defaults()); + } + + public static SimpleAttributedConfigurationNode root(String tagName) { + return root(tagName, ConfigurationOptions.defaults()); + } + + public static SimpleAttributedConfigurationNode root(String tagName, ConfigurationOptions options) { + return new SimpleAttributedConfigurationNode(tagName, null, null, options); + } + + protected SimpleAttributedConfigurationNode(String tagName, Object path, SimpleConfigurationNode parent, ConfigurationOptions options) { + super(path, parent, options); + this.tagName = tagName; + } + + @Override + public String getTagName() { + return tagName; + } + + @Override + public SimpleAttributedConfigurationNode setTagName(String tagName) { + if (Strings.isNullOrEmpty(tagName)) { + throw new IllegalArgumentException("Tag name cannot be null/empty"); + } + + this.tagName = tagName; + return this; + } + + @Override + public SimpleAttributedConfigurationNode addAttribute(String name, String value) { + if (Strings.isNullOrEmpty(name)) { + throw new IllegalArgumentException("Attribute name cannot be null/empty"); + } + + attributes.put(name, value); + return this; + } + + @Override + public SimpleAttributedConfigurationNode removeAttribute(String name) { + attributes.remove(name); + return this; + } + + @Override + public SimpleAttributedConfigurationNode setAttributes(Map attributes) { + for (String name : attributes.keySet()) { + if (Strings.isNullOrEmpty(name)) { + throw new IllegalArgumentException("Attribute name cannot be null/empty"); + } + } + + this.attributes.clear(); + this.attributes.putAll(attributes); + return this; + } + + @Override + public boolean hasAttributes() { + return !attributes.isEmpty(); + } + + @Override + public String getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Map getAttributes() { + return ImmutableMap.copyOf(attributes); + } + + @Override + public SimpleAttributedConfigurationNode getParent() { + return (SimpleAttributedConfigurationNode) super.getParent(); + } + + @Override + protected SimpleAttributedConfigurationNode createNode(Object path) { + return new SimpleAttributedConfigurationNode("element", path, this, getOptions()); + } + + @Override + public SimpleAttributedConfigurationNode setValue(Object value) { + if (value instanceof AttributedConfigurationNode && ((AttributedConfigurationNode) value).hasAttributes()) { + setAttributes(((AttributedConfigurationNode) value).getAttributes()); + } + return (SimpleAttributedConfigurationNode)super.setValue(value); + } + + @Override + public SimpleAttributedConfigurationNode mergeValuesFrom(ConfigurationNode other) { + if (other instanceof AttributedConfigurationNode && ((AttributedConfigurationNode) other).hasAttributes()) { + Map attributes = ((AttributedConfigurationNode) other).getAttributes(); + setAttributes(attributes); + } + return (SimpleAttributedConfigurationNode) super.mergeValuesFrom(other); + } + + @Override + public SimpleAttributedConfigurationNode getNode(Object... path) { + return (SimpleAttributedConfigurationNode)super.getNode(path); + } + + @Override + @SuppressWarnings("unchecked") + public List getChildrenList() { + return (List) super.getChildrenList(); + } + + @Override + @SuppressWarnings("unchecked") + public Map getChildrenMap() { + return (Map) super.getChildrenMap(); + } + + @Override + public SimpleAttributedConfigurationNode getAppendedNode() { + return (SimpleAttributedConfigurationNode) super.getAppendedNode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SimpleAttributedConfigurationNode)) return false; + if (!super.equals(o)) return false; + + SimpleAttributedConfigurationNode that = (SimpleAttributedConfigurationNode) o; + if (!attributes.equals(that.attributes)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + attributes.hashCode(); + return result; + } + + @Override + public String toString() { + return "SimpleAttributedConfigurationNode{" + + "super=" + super.toString() + ", " + + "attributes=" + attributes + + '}'; + } +} diff --git a/configurate-xml/pom.xml b/configurate-xml/pom.xml new file mode 100644 index 000000000..a5054c9d0 --- /dev/null +++ b/configurate-xml/pom.xml @@ -0,0 +1,41 @@ + + + + + ninja.leaping.configurate + configurate-parent + 3.4-SNAPSHOT + + + 4.0.0 + jar + + configurate-xml + Configurate XML + + + + ninja.leaping.configurate + configurate-core + ${project.parent.version} + + + + diff --git a/configurate-xml/src/main/java/ninja/leaping/configurate/xml/XMLConfigurationLoader.java b/configurate-xml/src/main/java/ninja/leaping/configurate/xml/XMLConfigurationLoader.java new file mode 100644 index 000000000..3eb4c8f01 --- /dev/null +++ b/configurate-xml/src/main/java/ninja/leaping/configurate/xml/XMLConfigurationLoader.java @@ -0,0 +1,181 @@ +/** + * Configurate + * Copyright (C) zml and Configurate contributors + * + * Licensed 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 ninja.leaping.configurate.xml; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.ConfigurationOptions; +import ninja.leaping.configurate.attributed.AttributedConfigurationNode; +import ninja.leaping.configurate.attributed.SimpleAttributedConfigurationNode; +import ninja.leaping.configurate.loader.AbstractConfigurationLoader; +import ninja.leaping.configurate.loader.CommentHandler; +import ninja.leaping.configurate.loader.CommentHandlers; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.validation.Schema; + +/** + * A loader for XML (Extensible Markup Language) + */ +public class XMLConfigurationLoader extends AbstractConfigurationLoader { + private final Schema schema; + private final String defaultTagName; + + public static class Builder extends AbstractConfigurationLoader.Builder { + private Schema schema = null; + private String defaultTagName = "element"; + + protected Builder() { + } + + public Schema getSchema() { + return schema; + } + + public Builder setSchema(Schema schema) { + this.schema = schema; + return this; + } + + public String getDefaultTagName() { + return defaultTagName; + } + + public Builder setDefaultTagName(String defaultTagName) { + this.defaultTagName = defaultTagName; + return this; + } + + @Override + public XMLConfigurationLoader build() { + return new XMLConfigurationLoader(this); + } + } + + public static Builder builder() { + return new Builder(); + } + + private XMLConfigurationLoader(Builder build) { + super(build, new CommentHandler[] {CommentHandlers.HASH, CommentHandlers.DOUBLE_SLASH}); + this.schema = build.getSchema(); + this.defaultTagName = build.getDefaultTagName(); + } + + @Override + public void loadInternal(AttributedConfigurationNode node, BufferedReader reader) throws IOException { + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + if (schema != null) { + builderFactory.setSchema(schema); + } + + DocumentBuilder documentBuilder; + try { + documentBuilder = builderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + } + + Document document; + try { + document = documentBuilder.parse(new InputSource(reader)); + } catch (SAXException e) { + throw new IOException(e); + } + + Element root = document.getDocumentElement(); + readConfigValue(root, node); + } + + private void readConfigValue(Node value, AttributedConfigurationNode node) { + node.setTagName(value.getNodeName()); + if (value.hasAttributes()) { + NamedNodeMap attributes = value.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node attribute = attributes.item(i); + node.addAttribute(attribute.getNodeName(), attribute.getNodeValue()); + } + } + + if (value.hasChildNodes()) { + NodeList childNodes = value.getChildNodes(); + Multimap nested = ArrayListMultimap.create(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node child = childNodes.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + nested.put(child.getNodeName(), child); + } + } + + if (!nested.isEmpty()) { + // construct to a map only if there are no duplicate keys + // and if none of the nested elements have attributes + boolean map = nested.keys().size() == nested.keySet().size() && !anyHaveAttributes(nested.values()); + for (Map.Entry entry : nested.entries()) { + readConfigValue(entry.getValue(), map ? node.getNode(entry.getKey()) : node.getAppendedNode()); + } + return; + } + } + + // TODO handle conversion from string --> boolean, number, etc + node.setValue(value.getTextContent()); + } + + @Override + protected void saveInternal(ConfigurationNode node, Writer writer) throws IOException { + // TODO + } + + @Override + public AttributedConfigurationNode createEmptyNode(ConfigurationOptions options) { + options = options.setAcceptedTypes(ImmutableSet.of(Double.class, Long.class, + Integer.class, Boolean.class, String.class, Number.class)); + return SimpleAttributedConfigurationNode.root("root", options); + } + + private static boolean hasAttributes(Node node) { + return node.hasAttributes() && node.getAttributes().getLength() > 0; + } + + private static boolean anyHaveAttributes(Iterable nodes) { + for (Node node : nodes) { + if (hasAttributes(node)) { + return true; + } + } + return false; + } +} diff --git a/configurate-xml/src/test/java/ninja/leaping/configurate/xml/XMLConfigurationLoaderTest.java b/configurate-xml/src/test/java/ninja/leaping/configurate/xml/XMLConfigurationLoaderTest.java new file mode 100644 index 000000000..d52971819 --- /dev/null +++ b/configurate-xml/src/test/java/ninja/leaping/configurate/xml/XMLConfigurationLoaderTest.java @@ -0,0 +1,87 @@ +/** + * Configurate + * Copyright (C) zml and Configurate contributors + * + * Licensed 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 ninja.leaping.configurate.xml; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import ninja.leaping.configurate.attributed.AttributedConfigurationNode; +import ninja.leaping.configurate.loader.AtomicFiles; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Basic sanity checks for the loader + */ +public class XMLConfigurationLoaderTest { + + @Rule + public final TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void testSimpleLoading() throws IOException { + URL url = getClass().getResource("/example.xml"); + final Path saveTest = folder.newFile().toPath(); + + XMLConfigurationLoader loader = XMLConfigurationLoader.builder() + .setSource(() -> new BufferedReader(new InputStreamReader(url.openStream(), UTF_8))) + .setSink(AtomicFiles.createAtomicWriterFactory(saveTest, UTF_8)).build(); + + AttributedConfigurationNode node = loader.load(); + assertEquals("messages", node.getTagName()); + assertEquals("false", node.getAttribute("secret")); + assertTrue(node.hasListChildren()); + + List notes = node.getChildrenList(); + assertEquals(2, notes.size()); + + AttributedConfigurationNode firstNote = notes.get(0); + assertEquals("501", firstNote.getAttribute("id")); + assertTrue(firstNote.hasMapChildren()); + assertFalse(firstNote.hasListChildren()); + + Map properties = firstNote.getChildrenMap(); + assertEquals("Tove", properties.get("to").getValue()); + assertEquals("Jani", properties.get("from").getValue()); + assertEquals("Don't forget me this weekend!", properties.get("body").getValue()); + assertEquals("heading", properties.get("heading").getTagName()); + + AttributedConfigurationNode secondNode = notes.get(1); + assertEquals("502", secondNode.getAttribute("id")); + assertFalse(secondNode.hasMapChildren()); + assertTrue(secondNode.hasListChildren()); + + List subNodes = secondNode.getChildrenList(); + for (AttributedConfigurationNode subNode : subNodes) { + if (subNode.getTagName().equals("heading")) { + assertEquals("true", subNode.getAttribute("bold")); + } + } + } +} diff --git a/configurate-xml/src/test/resources/example.xml b/configurate-xml/src/test/resources/example.xml new file mode 100644 index 000000000..35f41af11 --- /dev/null +++ b/configurate-xml/src/test/resources/example.xml @@ -0,0 +1,14 @@ + + + Tove + Jani + Reminder + Don't forget me this weekend! + + + Jani + Tove + Re: Reminder + I will not + + diff --git a/pom.xml b/pom.xml index 0ddde73a1..d1c79a1c8 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,7 @@ configurate-hocon configurate-json configurate-gson + configurate-xml