diff options
| author | Michael Williamson <mike@zwobble.org> | 2015-09-26 17:33:13 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2015-09-26 17:33:13 +0100 |
| commit | be6fbc6f4e2f80552d3a7bab8677066d1fc858fc (patch) | |
| tree | 365f33cb3c09e838ff7bfab33609c919b73508ac | |
| parent | 0cdeb56333f61fc5769749106496106cfd8ffbfe (diff) | |
Add simple XML parsing
10 files changed, 258 insertions, 0 deletions
@@ -28,5 +28,11 @@ <version>1.16.6</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <version>18.0</version> + <scope>runtime</scope> + </dependency> </dependencies> </project>
\ No newline at end of file diff --git a/src/main/java/org/zwobble/mammoth/xml/ElementName.java b/src/main/java/org/zwobble/mammoth/xml/ElementName.java new file mode 100644 index 0000000..1d70c36 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/xml/ElementName.java @@ -0,0 +1,8 @@ +package org.zwobble.mammoth.xml; + +import lombok.Value; + +@Value +public class ElementName { + private String localName; +} diff --git a/src/main/java/org/zwobble/mammoth/xml/SimpleSax.java b/src/main/java/org/zwobble/mammoth/xml/SimpleSax.java new file mode 100644 index 0000000..8165552 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/xml/SimpleSax.java @@ -0,0 +1,54 @@ +package org.zwobble.mammoth.xml; + +import java.io.IOException; +import java.io.StringReader; +import java.util.stream.IntStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import static java.util.stream.Collectors.toMap; + +import lombok.val; + +public class SimpleSax { + public static void parseString(String value, SimpleSaxHandler handler) { + val parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + try { + val saxParser = parserFactory.newSAXParser(); + val xmlReader = saxParser.getXMLReader(); + xmlReader.setContentHandler(new DefaultHandler() { + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + final ElementName name = new ElementName(localName); + val attributesMap = IntStream.range(0, attributes.getLength()) + .boxed() + .collect(toMap( + index -> new ElementName(attributes.getLocalName(index)), + attributes::getValue + )); + handler.startElement(name, attributesMap); + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + handler.endElement(); + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + handler.characters(new String(ch, start, length)); + } + }); + xmlReader.parse(new InputSource(new StringReader(value))); + } catch (ParserConfigurationException | SAXException | IOException exception) { + throw new RuntimeException(exception); + } + } +} diff --git a/src/main/java/org/zwobble/mammoth/xml/SimpleSaxHandler.java b/src/main/java/org/zwobble/mammoth/xml/SimpleSaxHandler.java new file mode 100644 index 0000000..f56a940 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/xml/SimpleSaxHandler.java @@ -0,0 +1,9 @@ +package org.zwobble.mammoth.xml; + +import java.util.Map; + +public interface SimpleSaxHandler { + void startElement(ElementName name, Map<ElementName, String> attributes); + void endElement(); + void characters(String string); +} diff --git a/src/main/java/org/zwobble/mammoth/xml/XmlElement.java b/src/main/java/org/zwobble/mammoth/xml/XmlElement.java new file mode 100644 index 0000000..a49f5f8 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/xml/XmlElement.java @@ -0,0 +1,30 @@ +package org.zwobble.mammoth.xml; + +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@RequiredArgsConstructor +@EqualsAndHashCode +@ToString +public class XmlElement implements XmlNode { + public XmlElement(String name) { + this(name, ImmutableMap.of()); + } + + public XmlElement(String name, Map<String, String> attributes) { + this(name, attributes, ImmutableList.of()); + } + + @Getter + private final String name; + private final Map<String, String> attributes; + private final List<XmlNode> children; +} diff --git a/src/main/java/org/zwobble/mammoth/xml/XmlElementBuilder.java b/src/main/java/org/zwobble/mammoth/xml/XmlElementBuilder.java new file mode 100644 index 0000000..16b4865 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/xml/XmlElementBuilder.java @@ -0,0 +1,25 @@ +package org.zwobble.mammoth.xml; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +class XmlElementBuilder { + private final String name; + private final Map<String, String> attributes; + private final List<XmlNode> children; + + XmlElementBuilder(String name, Map<String, String> attributes) { + this.name = name; + this.attributes = attributes; + this.children = new ArrayList<>(); + } + + XmlElement build() { + return new XmlElement(name, attributes, children); + } + + void addChild(XmlNode node) { + children.add(node); + } +} diff --git a/src/main/java/org/zwobble/mammoth/xml/XmlNode.java b/src/main/java/org/zwobble/mammoth/xml/XmlNode.java new file mode 100644 index 0000000..c91e100 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/xml/XmlNode.java @@ -0,0 +1,5 @@ +package org.zwobble.mammoth.xml; + +public interface XmlNode { + +} diff --git a/src/main/java/org/zwobble/mammoth/xml/XmlParser.java b/src/main/java/org/zwobble/mammoth/xml/XmlParser.java new file mode 100644 index 0000000..360c174 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/xml/XmlParser.java @@ -0,0 +1,54 @@ +package org.zwobble.mammoth.xml; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; + +import static java.util.stream.Collectors.toMap; + +import lombok.val; + +public class XmlParser { + public static XmlElement parseString(String value) { + val nodeGenerator = new NodeGenerator(); + SimpleSax.parseString(value, nodeGenerator); + return nodeGenerator.getRoot(); + } + + private static class NodeGenerator implements SimpleSaxHandler { + private final Deque<XmlElementBuilder> elementStack; + + public NodeGenerator() { + elementStack = new ArrayDeque<>(); + } + + public XmlElement getRoot() { + return elementStack.getFirst().build(); + } + + @Override + public void startElement(ElementName name, Map<ElementName, String> attributes) { + val simpleAttributes = attributes + .entrySet() + .stream() + .collect(toMap( + entry -> entry.getKey().getLocalName(), + entry -> entry.getValue())); + val element = new XmlElementBuilder(name.getLocalName(), simpleAttributes); + elementStack.add(element); + } + + @Override + public void endElement() { + if (elementStack.size() > 1) { + val element = elementStack.removeLast(); + elementStack.getLast().addChild(element.build()); + } + } + + @Override + public void characters(String string) { + elementStack.getLast().addChild(new XmlTextNode(string)); + } + } +} diff --git a/src/main/java/org/zwobble/mammoth/xml/XmlTextNode.java b/src/main/java/org/zwobble/mammoth/xml/XmlTextNode.java new file mode 100644 index 0000000..0d63273 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/xml/XmlTextNode.java @@ -0,0 +1,8 @@ +package org.zwobble.mammoth.xml; + +import lombok.Value; + +@Value +public class XmlTextNode implements XmlNode { + private String value; +} diff --git a/src/test/java/org/zwobble/mammoth/tests/xml/XmlParserTests.java b/src/test/java/org/zwobble/mammoth/tests/xml/XmlParserTests.java new file mode 100644 index 0000000..1ce5d7d --- /dev/null +++ b/src/test/java/org/zwobble/mammoth/tests/xml/XmlParserTests.java @@ -0,0 +1,59 @@ +package org.zwobble.mammoth.tests.xml; + +import org.junit.Test; +import org.zwobble.mammoth.xml.XmlElement; +import org.zwobble.mammoth.xml.XmlParser; +import org.zwobble.mammoth.xml.XmlTextNode; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +public class XmlParserTests { + @Test + public void canParseSelfClosingElement() { + assertThat( + XmlParser.parseString("<body/>"), + is(new XmlElement("body"))); + assertThat( + XmlParser.parseString("<values/>"), + is(new XmlElement("values"))); + } + + @Test + public void canParseEmptyElementWithSeparateClosingTag() { + assertThat( + XmlParser.parseString("<body></body>"), + is(new XmlElement("body"))); + } + + @Test + public void canParseAttributesOfTag() { + assertThat( + XmlParser.parseString("<body name='bob'></body>"), + is(new XmlElement("body", ImmutableMap.of("name", "bob")))); + } + + @Test + public void canParseElementWithDescendantElements() { + assertThat( + XmlParser.parseString("<body><a><b/></a><a/></body>"), + is(new XmlElement("body", ImmutableMap.of(), ImmutableList.of( + new XmlElement("a", ImmutableMap.of(), ImmutableList.of( + new XmlElement("b") + )), + new XmlElement("a") + )))); + } + + @Test + public void canParseTextNodes() { + assertThat( + XmlParser.parseString("<body>Hello!</body>"), + is(new XmlElement("body", ImmutableMap.of(), ImmutableList.of( + new XmlTextNode("Hello!") + )))); + } +} |
