├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── RELEASENOTES.md ├── header.txt ├── pom.xml └── src ├── main └── java │ └── net │ └── javacrumbs │ └── json2xml │ ├── ElementNameConverter.java │ ├── JsonSaxAdapter.java │ ├── JsonXmlHelper.java │ └── JsonXmlReader.java └── test └── java └── net └── javacrumbs └── json2xml ├── JsonSaxAdapterTest.java └── JsonTest.java /.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 | -------------------------------------------------------------------------------- /.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@v4 13 | - name: Set up JDK ${{ matrix.java }} 14 | uses: actions/setup-java@v4 15 | with: 16 | java-version: ${{ matrix.java }} 17 | distribution: 'zulu' 18 | - name: Cache Maven packages 19 | uses: actions/cache@v4.2.3 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.project 3 | /target 4 | *.iml 5 | /.idea 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/main/java/net/javacrumbs/json2xml/JsonXmlReader.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 com.fasterxml.jackson.core.JsonFactory; 19 | import com.fasterxml.jackson.core.JsonParser; 20 | import org.xml.sax.ContentHandler; 21 | import org.xml.sax.DTDHandler; 22 | import org.xml.sax.EntityResolver; 23 | import org.xml.sax.ErrorHandler; 24 | import org.xml.sax.InputSource; 25 | import org.xml.sax.SAXException; 26 | import org.xml.sax.SAXNotRecognizedException; 27 | import org.xml.sax.SAXNotSupportedException; 28 | import org.xml.sax.XMLReader; 29 | 30 | import java.io.IOException; 31 | 32 | /** 33 | * Helper class that can be used for JSON -> XML transformation. 34 | *
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/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 | "