├── .gitignore
├── RELEASENOTES.md
├── .github
├── dependabot.yml
└── workflows
│ └── build.yml
├── header.txt
├── src
├── main
│ └── java
│ │ └── net
│ │ └── javacrumbs
│ │ └── json2xml
│ │ ├── ElementNameConverter.java
│ │ ├── JsonXmlReader.java
│ │ ├── JsonXmlHelper.java
│ │ └── JsonSaxAdapter.java
└── test
│ └── java
│ └── net
│ └── javacrumbs
│ └── json2xml
│ ├── JsonTest.java
│ └── JsonSaxAdapterTest.java
├── pom.xml
├── README.md
└── LICENSE.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | /.settings
2 | /.project
3 | /target
4 | *.iml
5 | /.idea
6 |
--------------------------------------------------------------------------------
/RELEASENOTES.md:
--------------------------------------------------------------------------------
1 | ## 4.1
2 | * Fixed array handling
3 |
4 | ## 4.0 DO NOT USE
5 | * Changed transformation of arrays
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "maven"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | - package-ecosystem: "github-actions"
8 | directory: "/"
9 | schedule:
10 | interval: "daily"
11 |
--------------------------------------------------------------------------------
/header.txt:
--------------------------------------------------------------------------------
1 | Copyright 2009-2019 the original author or authors.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | java: [ '17', '21' ]
11 | steps:
12 | - uses: actions/checkout@v5
13 | - name: Set up JDK ${{ matrix.java }}
14 | uses: actions/setup-java@v5
15 | with:
16 | java-version: ${{ matrix.java }}
17 | distribution: 'zulu'
18 | - name: Cache Maven packages
19 | uses: actions/cache@v4.3.0
20 | with:
21 | path: ~/.m2
22 | key: m2-${{ hashFiles('**/pom.xml') }}
23 | restore-keys: m2
24 | - name: Build with Maven
25 | run: mvn test javadoc:javadoc
26 |
--------------------------------------------------------------------------------
/src/main/java/net/javacrumbs/json2xml/ElementNameConverter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2009-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package net.javacrumbs.json2xml;
17 |
18 | /**
19 | * Callback for name conversion. Can be used to convert names, that would be invalid as XML element values.
20 | */
21 | public interface ElementNameConverter {
22 |
23 | /**
24 | * Converts JSON object name to XML element name.
25 | * @param name
26 | * @return
27 | */
28 | public String convertName(String name);
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/java/net/javacrumbs/json2xml/JsonTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2009-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package net.javacrumbs.json2xml;
17 |
18 | import org.junit.Test;
19 | import org.xml.sax.InputSource;
20 |
21 | import javax.xml.transform.Transformer;
22 | import javax.xml.transform.TransformerException;
23 | import javax.xml.transform.TransformerFactory;
24 | import javax.xml.transform.dom.DOMResult;
25 | import javax.xml.transform.sax.SAXSource;
26 | import java.io.StringReader;
27 |
28 | public class JsonTest {
29 |
30 | @Test
31 | public void testMe() throws TransformerException {
32 | Transformer transformer = TransformerFactory.newInstance().newTransformer();
33 |
34 | String json = "[{\"studentName\" : \"Foo\", \"subjects2marks\":\"50\", \"subjects1name\":\"English\", \"subjects1marks\":\"40\", \"subjects2name\":\"History\", \"Age\":\"12\"},{\"studentName\":\"Bar\", \"subjects2marks\":\"50\", \"subjects1name\":\"English\", \"subjects1marks\":\"40\", \"subjects3marks\":\"40\", \"subjects2name\":\"History\", \"Age\":\"12\", \"subjects3name\":\"Science\"}, {\"studentName\":\"Baz\", \"Age\":\"12\"}]";
35 | InputSource source = new InputSource(new StringReader(json));
36 |
37 | DOMResult result = new DOMResult();
38 |
39 | transformer.transform(new SAXSource(new JsonXmlReader("http://www.w3.org/1999/xhtml", true, "details"), source), result);
40 |
41 | System.out.println(result.getNode().getChildNodes().item(0).getChildNodes().item(0).getChildNodes().item(0).getChildNodes().item(0));
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
35 | * Transformer transformer = TransformerFactory.newInstance().newTransformer(); 36 | * InputSource source = new InputSource(...); 37 | * Result result = ...; 38 | * transformer.transform(new SAXSource(new JsonXmlReader(namespace),source), result); 39 | *40 | */ 41 | public class JsonXmlReader implements XMLReader { 42 | 43 | private ContentHandler contentHandler; 44 | private final String namespaceUri; 45 | private final boolean addTypeAttributes; 46 | private final String artificialRootName; 47 | private final ElementNameConverter elementNameConverter; 48 | 49 | 50 | /** 51 | * Creates JsonXmlReader 52 | */ 53 | public JsonXmlReader() { 54 | this(""); 55 | } 56 | 57 | /** 58 | * Creates JsonXmlReader 59 | * @param namespaceUri namespace uri of the resulting XML. 60 | */ 61 | public JsonXmlReader(String namespaceUri) { 62 | this(namespaceUri, false); 63 | } 64 | 65 | /** 66 | * Creates JsonXmlReader 67 | * @param namespaceUri namespace uri of the resulting XML. 68 | * @param addTypeAttributes if true adds attributes with type info 69 | */ 70 | public JsonXmlReader(String namespaceUri, boolean addTypeAttributes) { 71 | this(namespaceUri, addTypeAttributes, null); 72 | } 73 | 74 | /** 75 | * Creates JsonXmlReader 76 | * @param namespaceUri namespace uri of the resulting XML. 77 | * @param addTypeAttributes if true adds attributes with type info 78 | * @param artificialRootName if set, an artificial root is generated so JSON documents with more roots can be handled. 79 | */ 80 | public JsonXmlReader(String namespaceUri, boolean addTypeAttributes, String artificialRootName) { 81 | this(namespaceUri, addTypeAttributes, artificialRootName, null); 82 | } 83 | 84 | /** 85 | * Creates JsonXmlReader 86 | * @param namespaceUri namespace uri of the resulting XML. 87 | * @param addTypeAttributes if true adds attributes with type info 88 | * @param artificialRootName if set, an artificial root is generated so JSON documents with more roots can be handled. 89 | * @param elementNameConverter converter to convert JSON object names to valid XML element names 90 | */ 91 | public JsonXmlReader(String namespaceUri, boolean addTypeAttributes, String artificialRootName, ElementNameConverter elementNameConverter) { 92 | this.namespaceUri = namespaceUri; 93 | this.addTypeAttributes = addTypeAttributes; 94 | this.artificialRootName = artificialRootName; 95 | this.elementNameConverter = elementNameConverter; 96 | } 97 | 98 | 99 | public boolean getFeature(String name) throws SAXNotRecognizedException, SAXNotSupportedException { 100 | throw new UnsupportedOperationException(); 101 | } 102 | 103 | public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { 104 | 105 | } 106 | 107 | public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException { 108 | throw new UnsupportedOperationException(); 109 | } 110 | 111 | 112 | public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException { 113 | //ignore 114 | } 115 | 116 | public void setEntityResolver(EntityResolver resolver) { 117 | //ignore 118 | } 119 | 120 | public EntityResolver getEntityResolver() { 121 | throw new UnsupportedOperationException(); 122 | } 123 | 124 | public void setDTDHandler(DTDHandler handler) { 125 | //ignore 126 | } 127 | 128 | public DTDHandler getDTDHandler() { 129 | throw new UnsupportedOperationException(); 130 | } 131 | 132 | public void setContentHandler(ContentHandler handler) { 133 | this.contentHandler = handler; 134 | } 135 | 136 | public ContentHandler getContentHandler() { 137 | return contentHandler; 138 | } 139 | 140 | public void setErrorHandler(ErrorHandler handler) { 141 | //ignore 142 | 143 | } 144 | 145 | public ErrorHandler getErrorHandler() { 146 | throw new UnsupportedOperationException(); 147 | } 148 | 149 | 150 | public void parse(InputSource input) throws IOException, SAXException { 151 | JsonParser jsonParser = new JsonFactory().createParser(input.getCharacterStream()); 152 | new JsonSaxAdapter(jsonParser, contentHandler, namespaceUri, addTypeAttributes, artificialRootName, elementNameConverter).parse(); 153 | } 154 | 155 | public void parse(String systemId) throws IOException, SAXException { 156 | throw new UnsupportedOperationException(); 157 | } 158 | 159 | public String getNamespaceUri() { 160 | return namespaceUri; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/net/javacrumbs/json2xml/JsonXmlHelper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2009-2019 Gwenhaël Pasquiers 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.javacrumbs.json2xml; 17 | 18 | import com.fasterxml.jackson.core.JsonFactory; 19 | import com.fasterxml.jackson.core.JsonGenerator; 20 | import java.io.IOException; 21 | import java.io.StringReader; 22 | import java.io.StringWriter; 23 | import java.math.BigDecimal; 24 | import javax.xml.transform.Transformer; 25 | import javax.xml.transform.TransformerConfigurationException; 26 | import javax.xml.transform.TransformerException; 27 | import javax.xml.transform.TransformerFactory; 28 | import javax.xml.transform.dom.DOMResult; 29 | import javax.xml.transform.sax.SAXSource; 30 | import org.w3c.dom.Document; 31 | import org.w3c.dom.Element; 32 | import org.w3c.dom.Node; 33 | 34 | import org.w3c.dom.NodeList; 35 | import org.xml.sax.InputSource; 36 | 37 | /** 38 | * Helper class that can be used for JSON -> XML and XML -> JSON transformation. 39 | * 40 | * It is mostly useful to go back to JSON after using JsonXmlReader and 41 | * eventually modifying the DOM using XPath. This class needs the type attribute 42 | * so it can only convert back XML created from JSON with addTypeAttributes 43 | * true. 44 | * 45 | *
46 | * Node node = convertToDom(myJSONString, "", true, "root"); 47 | * // ... 48 | * String json = convertToJson(node); 49 | *50 | */ 51 | public class JsonXmlHelper { 52 | 53 | static public enum TYPE { 54 | STRING, INT, FLOAT, BOOLEAN, NULL, ARRAY, OBJECT 55 | } 56 | 57 | /** 58 | * Helper method to convert JSON string to XML DOM 59 | * 60 | * @param json String containing the json document 61 | * @param namespace Namespace that will contain the generated dom nodes 62 | * @param addTypeAttributes Set to true to generate type attributes 63 | * @param artificialRootName Name of the artificial root element node 64 | * @return Document DOM node. 65 | * @throws javax.xml.transform.TransformerConfigurationException 66 | */ 67 | public static Node convertToDom(final String json, final String namespace, final boolean addTypeAttributes, final String artificialRootName) throws TransformerConfigurationException, TransformerException { 68 | Transformer transformer = TransformerFactory.newInstance().newTransformer(); 69 | InputSource source = new InputSource(new StringReader(json)); 70 | DOMResult result = new DOMResult(); 71 | transformer.transform(new SAXSource(new JsonXmlReader(namespace, addTypeAttributes, artificialRootName), source), result); 72 | return result.getNode(); 73 | } 74 | 75 | /** 76 | * Simpler helper method to convert DOM node back to JSON. The node MUST 77 | * have the "type" attributes (generated with addTypeAttributes flag set as 78 | * true). 79 | * 80 | * @param node The DOM Node 81 | * @return The JSON string 82 | * @throws IOException 83 | */ 84 | public static String convertToJson(Node node) throws IOException { 85 | try (StringWriter writer = new StringWriter(); JsonGenerator generator = new JsonFactory().createGenerator(writer)) { 86 | convertToJson(node, generator, name -> name); 87 | return writer.toString(); 88 | } 89 | } 90 | 91 | /** 92 | * More complete helper method to convert DOM node back to JSON.The node 93 | * MUST have the "type" attributes (generated with addTypeAttributes flag 94 | * set as true).This method allows to customize the JsonGenerator. 95 | * 96 | * @param node The DOM Node 97 | * @param generator A configured JsonGenerator 98 | * @param converter Converter to convert elements names from XML to JSON 99 | * @throws IOException 100 | */ 101 | public static void convertToJson(Node node, JsonGenerator generator, ElementNameConverter converter) throws IOException { 102 | Element element; 103 | if (node instanceof Document) { 104 | element = ((Document) node).getDocumentElement(); 105 | } else if (node instanceof Element) { 106 | element = (Element) node; 107 | } else { 108 | throw new IllegalArgumentException("Node must be either a Document or an Element"); 109 | } 110 | 111 | TYPE type = toTYPE(element.getAttribute("type")); 112 | switch (type) { 113 | case OBJECT: 114 | case ARRAY: 115 | convertElement(generator, element, true, converter); 116 | break; 117 | default: 118 | throw new RuntimeException("invalid root type [" + type + "]"); 119 | } 120 | generator.close(); 121 | } 122 | 123 | /** 124 | * Convert a DOM element to Json, with special handling for arrays since arrays don't exist in XML. 125 | * @param generator 126 | * @param element 127 | * @param isArrayItem 128 | * @throws IOException 129 | */ 130 | private static void convertElement(JsonGenerator generator, Element element, boolean isArrayItem, ElementNameConverter converter) throws IOException { 131 | TYPE type = toTYPE(element.getAttribute("type")); 132 | String name = element.getTagName(); 133 | 134 | if (!isArrayItem) { 135 | generator.writeFieldName(converter.convertName(name)); 136 | } 137 | 138 | switch (type) { 139 | case OBJECT: 140 | generator.writeStartObject(); 141 | convertChildren(generator, element, false, converter); 142 | generator.writeEndObject(); 143 | break; 144 | case ARRAY: 145 | generator.writeStartArray(); 146 | convertChildren(generator, element, true, converter); 147 | generator.writeEndArray(); 148 | break; 149 | case STRING: 150 | generator.writeString(element.getTextContent()); 151 | break; 152 | case INT: 153 | case FLOAT: 154 | generator.writeNumber(new BigDecimal(element.getTextContent())); 155 | break; 156 | case BOOLEAN: 157 | generator.writeBoolean(Boolean.parseBoolean(element.getTextContent())); 158 | break; 159 | case NULL: 160 | generator.writeNull(); 161 | break; 162 | } 163 | } 164 | 165 | /** 166 | * Method to recurse within children elements and convert them to JSON too. 167 | * @param generator 168 | * @param element 169 | * @param isArray 170 | * @throws IOException 171 | */ 172 | private static void convertChildren(JsonGenerator generator, Element element, boolean isArray, ElementNameConverter converter) throws IOException { 173 | NodeList list = element.getChildNodes(); 174 | int len = list.getLength(); 175 | for (int i = 0; i < len; i++) { 176 | Node node = list.item(i); 177 | if (node.getNodeType() == Node.ELEMENT_NODE) { 178 | convertElement(generator, (Element) node, isArray, converter); 179 | } 180 | } 181 | } 182 | 183 | /** 184 | * Convert type attribute value to TYPE enum (no attribute = OBJECT) 185 | * 186 | * @param type The type as a string 187 | * @return 188 | */ 189 | private static TYPE toTYPE(String type) { 190 | if (null == type || type.trim().isEmpty()) { 191 | return TYPE.OBJECT; 192 | } else { 193 | return TYPE.valueOf(type.toUpperCase()); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Java Json to XML conversion [](https://www.apache.org/licenses/LICENSE-2.0.txt) [](https://github.com/lukas-krecan/json2xml/actions/workflows/build.yml) [](https://maven-badges.herokuapp.com/maven-central/net.javacrumbs/json-xml) 2 | ============================ 3 | 4 | Json2Xml project is a simple implementation of JSON to XML conversion. Under the hood it uses [Jackson](http://jackson.codehaus.org/) [pull parser](http://wiki.fasterxml.com/JacksonInFiveMinutes#Streaming_API_Example) and generates 5 | XML SAX events. This way the conversion has low memory consumption and is pretty fast. 6 | 7 | There is already [Jettison](http://jettison.codehaus.org/) project that has similar objective, unfortunately it can not handle JSON arrays properly. 8 | 9 | Json2Xml converts the following JSON 10 | 11 |
14 |
15 | {
16 | "document":{
17 | "a":1,
18 | "b":2,
19 | "c":{
20 | "d":"text"
21 | },
22 | "e":[1, 2, 3],
23 | "f":[[1, 2, 3], [4, 5,6]],
24 | "g":null,
25 | "h":[
26 | {
27 | "i":true,
28 | "j":false
29 | }
30 | ],
31 | "k":[
32 | [
33 | {"l":1, "m":2}
34 | ],
35 | [
36 | {"n":3, "o":4},
37 | {"p":5, "q":6}
38 | ]
39 | ]
40 | }
41 | }
42 |
43 | |
44 |
45 | 46 | <document> 47 | <a>1</a> 48 | <b>2</b> 49 | <c> 50 | <d>text</d> 51 | </c> 52 | <e> 53 | <e>1</e> 54 | <e>2</e> 55 | <e>3</e> 56 | </e> 57 | <f> 58 | <f> 59 | <f>1</f> 60 | <f>2</f> 61 | <f>3</f> 62 | </f> 63 | <f> 64 | <f>4</f> 65 | <f>5</f> 66 | <f>6</f> 67 | </f> 68 | </f> 69 | <g /> 70 | <h> 71 | <h> 72 | <i>true</i> 73 | <j>false</j> 74 | </h> 75 | </h> 76 | <k> 77 | <k> 78 | <k> 79 | <l>1</l> 80 | <m>2</m> 81 | </k> 82 | </k> 83 | <k> 84 | <k> 85 | <n>3</n> 86 | <o>4</o> 87 | </k> 88 | <k> 89 | <p>5</p> 90 | <q>6</q> 91 | </k> 92 | </k> 93 | </k> 94 | </document> 95 |96 | |
97 |
5
176 |6177 |
40 | *
41 | * ContentHandler ch = ...;
42 | * JsonSaxAdapter adapter = new JsonSaxAdapter(JsonSaxAdapterTest.JSON, ch);
43 | * adapter.parse();
44 | *
45 | *
46 | *
47 | * or using {@link JsonXmlReader}
48 | *
49 | *
50 | * Transformer transformer = TransformerFactory.newInstance().newTransformer();
51 | * InputSource source = new InputSource(...);
52 | * Result result = ...;
53 | * transformer.transform(new SAXSource(new JsonXmlReader(),source), result);
54 | *
55 | *
56 | */
57 | public class JsonSaxAdapter {
58 |
59 | private static final AttributesImpl EMPTY_ATTRIBUTES = new AttributesImpl();
60 |
61 | private final JsonParser jsonParser;
62 |
63 | private final ContentHandler contentHandler;
64 |
65 | private final String namespaceUri;
66 |
67 | private final boolean addTypeAttributes;
68 |
69 | private final String artificialRootName;
70 |
71 | private final ElementNameConverter nameConverter;
72 |
73 | private static final JsonFactory JSON_FACTORY = new JsonFactory();
74 |
75 | /**
76 | * Creates JsonSaxAdapter that coverts JSON to SAX events.
77 | * @param json JSON to parse
78 | * @param contentHandler target of SAX events
79 | */
80 | public JsonSaxAdapter(final String json, final ContentHandler contentHandler) {
81 | this(parseJson(json), contentHandler);
82 | }
83 |
84 |
85 | /**
86 | * Creates JsonSaxAdapter that coverts JSON to SAX events.
87 | * @param jsonParser parsed JSON
88 | * @param contentHandler target of SAX events
89 | */
90 | public JsonSaxAdapter(final JsonParser jsonParser, final ContentHandler contentHandler) {
91 | this(jsonParser, contentHandler, "");
92 | }
93 |
94 | /**
95 | * Creates JsonSaxAdapter that coverts JSON to SAX events.
96 | * @param jsonParser parsed JSON
97 | * @param contentHandler target of SAX events
98 | * @param namespaceUri namespace of the generated XML
99 | */
100 | public JsonSaxAdapter(final JsonParser jsonParser, final ContentHandler contentHandler, final String namespaceUri) {
101 | this(jsonParser, contentHandler, namespaceUri, false);
102 | }
103 |
104 | /**
105 | * Creates JsonSaxAdapter that coverts JSON to SAX events.
106 | * @param jsonParser parsed JSON
107 | * @param contentHandler target of SAX events
108 | * @param namespaceUri namespace of the generated XML
109 | * @param addTypeAttributes adds type information as attributes
110 | */
111 | public JsonSaxAdapter(final JsonParser jsonParser, final ContentHandler contentHandler, final String namespaceUri, final boolean addTypeAttributes) {
112 | this(jsonParser, contentHandler, namespaceUri, addTypeAttributes, null);
113 | }
114 |
115 | /**
116 | * Creates JsonSaxAdapter that coverts JSON to SAX events.
117 | * @param jsonParser parsed JSON
118 | * @param contentHandler target of SAX events
119 | * @param namespaceUri namespace of the generated XML
120 | * @param addTypeAttributes adds type information as attributes
121 | * @param artificialRootName if set, an artificial root is generated so JSON documents with more roots can be handled.
122 | */
123 | public JsonSaxAdapter(final JsonParser jsonParser, final ContentHandler contentHandler, final String namespaceUri,
124 | final boolean addTypeAttributes, final String artificialRootName) {
125 | this(jsonParser, contentHandler, namespaceUri, addTypeAttributes, artificialRootName, null);
126 | }
127 |
128 | /**
129 | * Creates JsonSaxAdapter that coverts JSON to SAX events.
130 | * @param jsonParser parsed JSON
131 | * @param contentHandler target of SAX events
132 | * @param namespaceUri namespace of the generated XML
133 | * @param addTypeAttributes adds type information as attributes
134 | * @param artificialRootName if set, an artificial root is generated so JSON documents with more roots can be handled.
135 | */
136 | public JsonSaxAdapter(final JsonParser jsonParser, final ContentHandler contentHandler, final String namespaceUri,
137 | final boolean addTypeAttributes, final String artificialRootName, final ElementNameConverter nameConverter) {
138 | this.jsonParser = jsonParser;
139 | this.contentHandler = contentHandler;
140 | this.namespaceUri = namespaceUri;
141 | this.addTypeAttributes = addTypeAttributes;
142 | this.artificialRootName = artificialRootName;
143 | this.nameConverter = nameConverter;
144 | contentHandler.setDocumentLocator(new DocumentLocator());
145 | }
146 |
147 |
148 | private static JsonParser parseJson(final String json) {
149 | try {
150 | return JSON_FACTORY.createParser(json);
151 | } catch (Exception e) {
152 | throw new ParserException("Parsing error", e);
153 | }
154 | }
155 |
156 | /**
157 | * Method parses JSON and emits SAX events.
158 | */
159 | public void parse() throws ParserException {
160 | try {
161 | jsonParser.nextToken();
162 | contentHandler.startDocument();
163 | if (shouldAddArtificialRoot()) {
164 | startElement(artificialRootName);
165 | parseElement(artificialRootName, false);
166 | endElement(artificialRootName);
167 | } else if (START_OBJECT.equals(jsonParser.getCurrentToken())) {
168 | int elementsWritten = parseObject();
169 | if (elementsWritten>1) {
170 | throw new ParserException("More than one root element. Can not generate legal XML. You can set artificialRootName to generate an artificial root.");
171 | }
172 | } else {
173 | throw new ParserException("Unsupported root element. Can not generate legal XML. You can set artificialRootName to generate an artificial root.");
174 | }
175 | contentHandler.endDocument();
176 | } catch (Exception e) {
177 | throw new ParserException("Parsing error: " + e.getMessage(), e);
178 | }
179 | }
180 |
181 | private boolean shouldAddArtificialRoot() {
182 | return artificialRootName != null && artificialRootName.length() > 0;
183 | }
184 |
185 | /**
186 | * Parses generic object.
187 | *
188 | * @return number of elements written
189 | * @throws IOException
190 | * @throws JsonParseException
191 | * @throws Exception
192 | */
193 | private int parseObject() throws Exception {
194 | int elementsWritten = 0;
195 | while (jsonParser.nextToken() != null && jsonParser.getCurrentToken() != END_OBJECT) {
196 | if (FIELD_NAME.equals(jsonParser.getCurrentToken())) {
197 | String elementName = convertName(jsonParser.getCurrentName());
198 | //jump to element value
199 | jsonParser.nextToken();
200 | startElement(elementName);
201 | parseElement(elementName, false);
202 | endElement(elementName);
203 | elementsWritten++;
204 | } else {
205 | throw new ParserException("Error when parsing. Expected field name got " + jsonParser.getCurrentToken());
206 | }
207 | }
208 | return elementsWritten;
209 | }
210 |
211 | private String convertName(String name) {
212 | if (nameConverter != null) {
213 | return nameConverter.convertName(name);
214 | } else {
215 | return name;
216 | }
217 | }
218 |
219 | /**
220 | * Pares JSON element.
221 | * @param elementName
222 | * @param inArray if the element is in an array
223 | * @throws Exception
224 | */
225 | private void parseElement(final String elementName, final boolean inArray) throws Exception {
226 | JsonToken currentToken = jsonParser.getCurrentToken();
227 | if (inArray) {
228 | startElement(elementName);
229 | }
230 | if (START_OBJECT.equals(currentToken)) {
231 | parseObject();
232 | } else if (START_ARRAY.equals(currentToken)) {
233 | parseArray(elementName);
234 | } else if (currentToken.isScalarValue()) {
235 | parseValue();
236 | }
237 | if (inArray) {
238 | endElement(elementName);
239 | }
240 | }
241 |
242 | private void parseArray(final String elementName) throws Exception {
243 | while (jsonParser.nextToken() != END_ARRAY && jsonParser.getCurrentToken() != null) {
244 | parseElement(elementName, true);
245 | }
246 | }
247 |
248 | private void parseValue() throws Exception {
249 | if (VALUE_NULL != jsonParser.getCurrentToken()) {
250 | String text = jsonParser.getText();
251 | contentHandler.characters(text.toCharArray(), 0, text.length());
252 | }
253 | }
254 |
255 |
256 | private void startElement(final String elementName) throws SAXException {
257 | contentHandler.startElement(namespaceUri, elementName, elementName, getTypeAttributes());
258 | }
259 |
260 |
261 | protected Attributes getTypeAttributes() {
262 | if (addTypeAttributes) {
263 | String currentTokenType = getCurrentTokenType();
264 | if (currentTokenType != null) {
265 | AttributesImpl attributes = new AttributesImpl();
266 | attributes.addAttribute("", "type", "type", "string", currentTokenType);
267 | return attributes;
268 | } else {
269 | return EMPTY_ATTRIBUTES;
270 | }
271 | } else {
272 | return EMPTY_ATTRIBUTES;
273 | }
274 | }
275 |
276 |
277 | protected String getCurrentTokenType() {
278 | switch (jsonParser.getCurrentToken()) {
279 | case VALUE_NUMBER_INT:
280 | return "int";
281 | case VALUE_NUMBER_FLOAT:
282 | return "float";
283 | case VALUE_FALSE:
284 | return "boolean";
285 | case VALUE_TRUE:
286 | return "boolean";
287 | case VALUE_STRING:
288 | return "string";
289 | case VALUE_NULL:
290 | return "null";
291 | case START_ARRAY:
292 | return "array";
293 | default:
294 | return null;
295 | }
296 | }
297 |
298 |
299 | private void endElement(final String elementName) throws SAXException {
300 | contentHandler.endElement(namespaceUri, elementName, elementName);
301 | }
302 |
303 | public static class ParserException extends RuntimeException {
304 | private static final long serialVersionUID = 2194022343599245018L;
305 |
306 | public ParserException(final String message, final Throwable cause) {
307 | super(message, cause);
308 | }
309 |
310 | public ParserException(final String message) {
311 | super(message);
312 | }
313 |
314 | public ParserException(final Throwable cause) {
315 | super(cause);
316 | }
317 |
318 | }
319 |
320 | private class DocumentLocator implements Locator {
321 |
322 | public String getPublicId() {
323 | Object sourceRef = jsonParser.getCurrentLocation().getSourceRef();
324 | if (sourceRef != null) {
325 | return sourceRef.toString();
326 | } else {
327 | return "";
328 | }
329 | }
330 |
331 | public String getSystemId() {
332 | return getPublicId();
333 | }
334 |
335 | public int getLineNumber() {
336 | return jsonParser.getCurrentLocation() != null ? jsonParser.getCurrentLocation().getLineNr() : -1;
337 | }
338 |
339 | public int getColumnNumber() {
340 | return jsonParser.getCurrentLocation() != null ? jsonParser.getCurrentLocation().getColumnNr() : -1;
341 | }
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/src/test/java/net/javacrumbs/json2xml/JsonSaxAdapterTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2009-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package net.javacrumbs.json2xml;
17 |
18 | import net.javacrumbs.json2xml.JsonSaxAdapter.ParserException;
19 | import org.custommonkey.xmlunit.Diff;
20 | import org.custommonkey.xmlunit.XMLUnit;
21 | import org.junit.Before;
22 | import org.junit.Test;
23 | import org.w3c.dom.Node;
24 | import org.xml.sax.ContentHandler;
25 | import org.xml.sax.InputSource;
26 |
27 | import javax.xml.transform.Result;
28 | import javax.xml.transform.Transformer;
29 | import javax.xml.transform.TransformerFactory;
30 | import javax.xml.transform.dom.DOMResult;
31 | import javax.xml.transform.sax.SAXSource;
32 | import javax.xml.transform.stream.StreamResult;
33 | import java.io.ByteArrayOutputStream;
34 | import java.io.StringReader;
35 | import static net.javacrumbs.json2xml.JsonXmlHelper.convertToJson;
36 | import static net.javacrumbs.jsonunit.JsonAssert.assertJsonEquals;
37 |
38 | import static org.junit.Assert.assertTrue;
39 | import static org.mockito.Mockito.mock;
40 |
41 |
42 | public class JsonSaxAdapterTest {
43 |
44 | public static final String JSON = "{\"document\":{\"a\":1,\"b\":2,\"c\":{\"d\":\"text\"},\"e\":[1,2,3],\"f\":[[1,2,3],[4,5,6]], \"g\":null, " +
45 | "\"h\":[{\"i\":true,\"j\":false}],\"k\":[[{\"l\":1,\"m\":2}],[{\"n\":3,\"o\":4},{\"p\":5,\"q\":6}]]}}";
46 |
47 | private static final String XML = "\n" +
48 | "5
\n" + 92 | "6\n" + 93 | "
5
\n" + 144 | "6\n" + 145 | "