在某些业务场景下,你可能需要把一个XML 转换成一个 JSON,其实这个转换并不难,网上有很多现成的工具类。但这里都有一个问题:比如这个节点设计者想表达的是一个是数组 ,但由于XML语法在设计上的缺陷,当只有一个子节点 或者 没有子节点 的情况下,如果你尝试把它转成JSON时,你会发现他默认转成了一个JSON对象了,而不是数组。
当前XML中books节点下有多个book,设计者想表达的是books下有多个元素(多本书),这种场景下转换是没有问题的。
1、转换前的XML数据
<?xml version="1.0" encoding="utf-8"?>
<library>
<books>
<book>
<name>Java</name>
</book>
<book>
<name>C语言</name>
</book>
</books>
</library>
2、转换后的JSON数据
{
"library": {
"books": {
"book": [
{
"name": "Java"
},
{
"name": "C语言"
}
]
}
}
}
结合上面XML中books节点,我们删除其中一个子节点,其实设计者依然想表达的是books下有多个元素(多本书),当由于XML不能像JSON对象一样,能清晰的表达对象和数组,这种场景下转换成JSON对象就有问题了。
1、转换前的XML数据
<?xml version="1.0" encoding="utf-8"?>
<library>
<books>
<book>
<name>Java</name>
</book>
</books>
</library>
2、转换后的JSON数据
{
"library": {
"books": {
"book": {
"name": "Java"
}
}
}
}
我们发现经过工具类转换后,book 被转换成一个JSON对象 {} 了,而不是一个JSON数组 [],因为此处XML的语义无法清晰的表达是对象还是数组,就导致转换的结果是一个对象。 其实你期待输出一个标准固定的格式给用户,不管几个节点,都是输出为JSON数组,而不是因为节点变化,导致数据格式发生变化,导致客户端无法用统一的格式解析数据。
网上有很多办法,提供了很多思路,比如(Convert XML to JSON and force array),它是一个.net语言的框架,但作为Java开发者真正解决这个问题的代码并不多。你需要自己写代码来解决这个问题。我在遇到这个问题后,也参考了部分开源代码的实现思路,你通过下面的代码可以轻松的解决这个问题。
package com.demo;
import java.util.ArrayList;
import java.util.HashMap;
/**
* @author youyun.xu
* @Description: Tag
* @date 2022/1/17 14:15
*/
public class Tag {
private String mPath;
private String mName;
private ArrayList<Tag> mChildren = new ArrayList<>();
private String mContent;
Tag(String path, String name) {
mPath = path;
mName = name;
}
void addChild(Tag tag) {
mChildren.add(tag);
}
void setContent(String content) {
// checks that there is a relevant content (not only spaces or \n)
boolean hasContent = false;
if (content != null) {
for(int i=0; i<content.length(); ++i) {
char c = content.charAt(i);
if ((c != ' ') && (c != '\n')) {
hasContent = true;
break;
}
}
}
if (hasContent) {
mContent = content;
}
}
String getName() {
return mName;
}
String getContent() {
return mContent;
}
ArrayList<Tag> getChildren() {
return mChildren;
}
boolean hasChildren() {
return (mChildren.size() > 0);
}
int getChildrenCount() {
return mChildren.size();
}
Tag getChild(int index) {
if ((index >= 0) && (index < mChildren.size())) {
return mChildren.get(index);
}
return null;
}
HashMap<String, ArrayList<Tag>> getGroupedElements() {
HashMap<String, ArrayList<Tag>> groups = new HashMap<>();
for(Tag child : mChildren) {
String key = child.getName();
ArrayList<Tag> group = groups.get(key);
if (group == null) {
group = new ArrayList<>();
groups.put(key, group);
}
group.add(child);
}
return groups;
}
String getPath() {
return mPath;
}
@Override
public String toString() {
return "Tag: " + mName + ", " + mChildren.size() + " children, Content: " + mContent;
}
}
package com.demo;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.regex.Pattern.DOTALL;
/**
* @author youyun.xu
* @Description: Xml转Json支持单个转数组(建造者模式)
* @date 2022/1/17 14:15
*/
public class XmlToJson {
private static final String TAG = "XmlToJson";
private static final String DEFAULT_CONTENT_NAME = "content";
private static final String DEFAULT_ENCODING = "utf-8";
private static final String DEFAULT_INDENTATION = " ";
private String mIndentationPattern = DEFAULT_INDENTATION;
// default values when a Tag is empty
private static final String DEFAULT_EMPTY_STRING = "";
private static final int DEFAULT_EMPTY_INTEGER = 0;
private static final long DEFAULT_EMPTY_LONG = 0;
private static final double DEFAULT_EMPTY_DOUBLE = 0;
private static final boolean DEFAULT_EMPTY_BOOLEAN = false;
/**
* Builder class to create a XmlToJson object
*/
public static class Builder {
private StringReader mStringSource;
private InputStream mInputStreamSource;
private String mInputEncoding = DEFAULT_ENCODING;
private HashSet<String> mForceListPaths = new HashSet<>();
private HashSet<Pattern> mForceListPatterns = new HashSet<>();
private HashMap<String, String> mAttributeNameReplacements = new HashMap<>();
private HashMap<String, String> mContentNameReplacements = new HashMap<>();
private HashMap<String, Class> mForceClassForPath = new HashMap<>(); // Integer, Long, Double, Boolean
private HashSet<String> mSkippedAttributes = new HashSet<>();
private HashSet<String> mSkippedTags = new HashSet<>();
/**
* Constructor
*
* @param xmlSource XML source
*/
public Builder( String xmlSource) {
mStringSource = new StringReader(xmlSource);
}
/**
* Constructor
*
* @param inputStreamSource XML source
* @param inputEncoding XML encoding format, can be null (uses UTF-8 if null).
*/
public Builder( InputStream inputStreamSource, String inputEncoding) {
mInputStreamSource = inputStreamSource;
mInputEncoding = (inputEncoding != null) ? inputEncoding : DEFAULT_ENCODING;
}
/**
* Force a XML Tag to be interpreted as a list
*
* @param path Path for the tag, with format like "/parentTag/childTag/tagAsAList"
* @return the Builder
*/
public Builder forceList( String path) {
mForceListPaths.add(path);
return this;
}
/**
* Force a XML Tag to be interpreted as a list, using a RegEx pattern for the path
*
* @param pattern Path for the tag using RegEx, like "*childTag/tagAsAList"
* @return the Builder
*/
public Builder forceListPattern( String pattern) {
Pattern pat = Pattern.compile(pattern, DOTALL);
mForceListPatterns.add(pat);
return this;
}
/**
* Change the name of an attribute
*
* @param attributePath Path for the attribute, using format like "/parentTag/childTag/childTagAttribute"
* @param replacementName Name used for replacement (childTagAttribute becomes replacementName)
* @return the Builder
*/
public Builder setAttributeName( String attributePath, String replacementName) {
mAttributeNameReplacements.put(attributePath, replacementName);
return this;
}
/**
* Change the name of the key for a XML content
* In XML there is no extra key name for a tag content. So a default name "content" is used.
* This "content" name can be replaced with a custom name.
*
* @param contentPath Path for the Tag that holds the content, using format like "/parentTag/childTag"
* @param replacementName Name used in place of the default "content" key
* @return the Builder
*/
public Builder setContentName( String contentPath, String replacementName) {
mContentNameReplacements.put(contentPath, replacementName);
return this;
}
/**
* Force an attribute or content value to be a INTEGER. A default value is used if the content is missing.
* @param path Path for the Tag content or Attribute, using format like "/parentTag/childTag"
* @return the Builder
*/
public Builder forceIntegerForPath( String path) {
mForceClassForPath.put(path, Integer.class);
return this;
}
/**
* Force an attribute or content value to be a LONG. A default value is used if the content is missing.
* @param path Path for the Tag content or Attribute, using format like "/parentTag/childTag"
* @return the Builder
*/
public Builder forceLongForPath( String path) {
mForceClassForPath.put(path, Long.class);
return this;
}
/**
* Force an attribute or content value to be a DOUBLE. A default value is used if the content is missing.
* @param path Path for the Tag content or Attribute, using format like "/parentTag/childTag"
* @return the Builder
*/
public Builder forceDoubleForPath( String path) {
mForceClassForPath.put(path, Double.class);
return this;
}
/**
* Force an attribute or content value to be a BOOLEAN. A default value is used if the content is missing.
* @param path Path for the Tag content or Attribute, using format like "/parentTag/childTag"
* @return the Builder
*/
public Builder forceBooleanForPath( String path) {
mForceClassForPath.put(path, Boolean.class);
return this;
}
/**
* Skips a Tag (will not be present in the JSON)
*
* @param path Path for the Tag, using format like "/parentTag/childTag"
* @return the Builder
*/
public Builder skipTag( String path) {
mSkippedTags.add(path);
return this;
}
/**
* Skips an attribute (will not be present in the JSON)
*
* @param path Path for the Attribute, using format like "/parentTag/childTag/ChildTagAttribute"
* @return the Builder
*/
public Builder skipAttribute( String path) {
mSkippedAttributes.add(path);
return this;
}
/**
* Creates the XmlToJson object
*
* @return a XmlToJson instance
*/
public XmlToJson build() {
return new XmlToJson(this);
}
}
private StringReader mStringSource;
private InputStream mInputStreamSource;
private String mInputEncoding;
private HashSet<String> mForceListPaths;
private HashSet<Pattern> mForceListPatterns = new HashSet<>();
private HashMap<String, String> mAttributeNameReplacements;
private HashMap<String, String> mContentNameReplacements;
private HashMap<String, Class> mForceClassForPath;
private HashSet<String> mSkippedAttributes = new HashSet<>();
private HashSet<String> mSkippedTags = new HashSet<>();
private JSONObject mJsonObject; // Used for caching the result
private XmlToJson(Builder builder) {
mStringSource = builder.mStringSource;
mInputStreamSource = builder.mInputStreamSource;
mInputEncoding = builder.mInputEncoding;
mForceListPaths = builder.mForceListPaths;
mForceListPatterns = builder.mForceListPatterns;
mAttributeNameReplacements = builder.mAttributeNameReplacements;
mContentNameReplacements = builder.mContentNameReplacements;
mForceClassForPath = builder.mForceClassForPath;
mSkippedAttributes = builder.mSkippedAttributes;
mSkippedTags = builder.mSkippedTags;
mJsonObject = convertToJSONObject(); // Build now so that the InputStream can be closed just after
}
/**
* @return the JSONObject built from the XML
*/
public
JSONObject toJson() {
return mJsonObject;
}
private
JSONObject convertToJSONObject() {
try {
Tag parentTag = new Tag("", "xml");
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true); // tags with namespace are taken as-is ("namespace:tagname")
XmlPullParser xpp = factory.newPullParser();
setInput(xpp);
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.START_DOCUMENT) {
eventType = xpp.next();
}
readTags(parentTag, xpp);
unsetInput();
return convertTagToJson(parentTag, false);
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
return null;
}
}
private void setInput(XmlPullParser xpp) {
if (mStringSource != null) {
try {
xpp.setInput(mStringSource);
} catch (XmlPullParserException e) {
e.printStackTrace();
}
} else {
try {
xpp.setInput(mInputStreamSource, mInputEncoding);
} catch (XmlPullParserException e) {
e.printStackTrace();
}
}
}
private void unsetInput() {
if (mStringSource != null) {
mStringSource.close();
}
// else the InputStream has been given by the user, it is not our role to close it
}
private void readTags(Tag parent, XmlPullParser xpp) {
try {
int eventType;
do {
eventType = xpp.next();
if (eventType == XmlPullParser.START_TAG) {
String tagName = xpp.getName();
String path = parent.getPath() + "/" + tagName;
boolean skipTag = mSkippedTags.contains(path);
Tag child = new Tag(path, tagName);
if (!skipTag) {
parent.addChild(child);
}
// Attributes are taken into account as key/values in the child
int attrCount = xpp.getAttributeCount();
for (int i = 0; i < attrCount; ++i) {
String attrName = xpp.getAttributeName(i);
String attrValue = xpp.getAttributeValue(i);
String attrPath = parent.getPath() + "/" + child.getName() + "/" + attrName;
// Skip Attributes
if (mSkippedAttributes.contains(attrPath)) {
continue;
}
attrName = getAttributeNameReplacement(attrPath, attrName);
Tag attribute = new Tag(attrPath, attrName);
attribute.setContent(attrValue);
child.addChild(attribute);
}
readTags(child, xpp);
} else if (eventType == XmlPullParser.TEXT) {
String text = xpp.getText();
parent.setContent(text);
} else if (eventType == XmlPullParser.END_TAG) {
return;
} else if (eventType == XmlPullParser.END_DOCUMENT) {
return;
} else {
//Log.i(TAG, "unknown xml eventType " + eventType);
}
} while (eventType != XmlPullParser.END_DOCUMENT);
} catch (XmlPullParserException | IOException | NullPointerException e) {
e.printStackTrace();
}
}
private JSONObject convertTagToJson(Tag tag, boolean isListElement) {
JSONObject json = new JSONObject();
// Content is injected as a key/value
if (tag.getContent() != null) {
String path = tag.getPath();
String name = getContentNameReplacement(path, DEFAULT_CONTENT_NAME);
putContent(path, json, name, tag.getContent());
}
try {
HashMap<String, ArrayList<Tag>> groups = tag.getGroupedElements(); // groups by tag names so that we can detect lists or single elements
for (ArrayList<Tag> group : groups.values()) {
if (group.size() == 1) { // element, or list of 1
Tag child = group.get(0);
if (isForcedList(child)) { // list of 1
JSONArray list = new JSONArray();
list.put(convertTagToJson(child, true));
String childrenNames = child.getName();
json.put(childrenNames, list);
} else { // stand alone element
if (child.hasChildren()) {
JSONObject jsonChild = convertTagToJson(child, false);
json.put(child.getName(), jsonChild);
} else {
String path = child.getPath();
putContent(path, json, child.getName(), child.getContent());
}
}
} else { // list
JSONArray list = new JSONArray();
for (Tag child : group) {
list.put(convertTagToJson(child, true));
}
String childrenNames = group.get(0).getName();
json.put(childrenNames, list);
}
}
return json;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
private void putContent(String path, JSONObject json, String tag, String content) {
try {
// checks if the user wants to force a class (Int, Double... for a given path)
Class forcedClass = mForceClassForPath.get(path);
if (forcedClass == null) { // default behaviour, put it as a String
if (content == null) {
content = DEFAULT_EMPTY_STRING;
}
json.put(tag, content);
} else {
if (forcedClass == Integer.class) {
try {
Integer number = Integer.parseInt(content);
json.put(tag, number);
} catch (NumberFormatException exception) {
json.put(tag, DEFAULT_EMPTY_INTEGER);
}
} else if (forcedClass == Long.class) {
try {
Long number = Long.parseLong(content);
json.put(tag, number);
} catch (NumberFormatException exception) {
json.put(tag, DEFAULT_EMPTY_LONG);
}
} else if (forcedClass == Double.class) {
try {
Double number = Double.parseDouble(content);
json.put(tag, number);
} catch (NumberFormatException exception) {
json.put(tag, DEFAULT_EMPTY_DOUBLE);
}
} else if (forcedClass == Boolean.class) {
if (content == null) {
json.put(tag, DEFAULT_EMPTY_BOOLEAN);
} else if (content.equalsIgnoreCase("true")) {
json.put(tag, true);
} else if (content.equalsIgnoreCase("false")) {
json.put(tag, false);
} else {
json.put(tag, DEFAULT_EMPTY_BOOLEAN);
}
}
}
} catch (JSONException exception) {
// keep continue in case of error
}
}
private boolean isForcedList(Tag tag) {
String path = tag.getPath();
if (mForceListPaths.contains(path)) {
return true;
}
for(Pattern pattern : mForceListPatterns) {
Matcher matcher = pattern.matcher(path);
if (matcher.find()) {
return true;
}
}
return false;
}
private String getAttributeNameReplacement(String path, String defaultValue) {
String result = mAttributeNameReplacements.get(path);
if (result != null) {
return result;
}
return defaultValue;
}
private String getContentNameReplacement(String path, String defaultValue) {
String result = mContentNameReplacements.get(path);
/* if (result != null) {
return result;
}*/
return result;
}
@Override
public String toString() {
if (mJsonObject != null) {
return mJsonObject.toString();
}
return null;
}
/**
* Format the Json with indentation and line breaks
*
* @param indentationPattern indentation to use, for example " " or "\t".
* if null, use the default 3 spaces indentation
* @return the formatted Json
*/
public String toFormattedString( String indentationPattern) {
if (indentationPattern == null) {
mIndentationPattern = DEFAULT_INDENTATION;
} else {
mIndentationPattern = indentationPattern;
}
return toFormattedString();
}
/**
* Format the Json with indentation and line breaks.
* Uses the last intendation pattern used, or the default one (3 spaces)
*
* @return the Builder
*/
public String toFormattedString() {
if (mJsonObject != null) {
String indent = "";
StringBuilder builder = new StringBuilder();
builder.append("{\n");
format(mJsonObject, builder, indent);
builder.append("}\n");
return builder.toString();
}
return null;
}
private void format(JSONObject jsonObject, StringBuilder builder, String indent) {
Iterator<String> keys = jsonObject.keys();
while (keys.hasNext()) {
String key = keys.next();
builder.append(indent);
builder.append(mIndentationPattern);
builder.append("\"");
builder.append(key);
builder.append("\": ");
Object value = jsonObject.opt(key);
if (value instanceof JSONObject) {
JSONObject child = (JSONObject) value;
builder.append(indent);
builder.append("{\n");
format(child, builder, indent + mIndentationPattern);
builder.append(indent);
builder.append(mIndentationPattern);
builder.append("}");
} else if (value instanceof JSONArray) {
JSONArray array = (JSONArray) value;
formatArray(array, builder, indent + mIndentationPattern);
} else {
formatValue(value, builder);
}
if (keys.hasNext()) {
builder.append(",\n");
} else {
builder.append("\n");
}
}
}
private void formatArray(JSONArray array, StringBuilder builder, String indent) {
builder.append("[\n");
for (int i = 0; i < array.length(); ++i) {
Object element = array.opt(i);
if (element instanceof JSONObject) {
JSONObject child = (JSONObject) element;
builder.append(indent);
builder.append(mIndentationPattern);
builder.append("{\n");
format(child, builder, indent + mIndentationPattern);
builder.append(indent);
builder.append(mIndentationPattern);
builder.append("}");
} else if (element instanceof JSONArray) {
JSONArray child = (JSONArray) element;
formatArray(child, builder, indent + mIndentationPattern);
} else {
formatValue(element, builder);
}
if (i < array.length() - 1) {
builder.append(",");
}
builder.append("\n");
}
builder.append(indent);
builder.append("]");
}
private void formatValue(Object value, StringBuilder builder) {
if (value instanceof String) {
String string = (String) value;
// Escape special characters
string = string.replaceAll("\\\\", "\\\\\\\\"); // escape backslash
string = string.replaceAll("\"", Matcher.quoteReplacement("\\\"")); // escape double quotes
string = string.replaceAll("/", "\\\\/"); // escape slash
string = string.replaceAll("\n", "\\\\n").replaceAll("\t", "\\\\t"); // escape \n and \t
string = string.replaceAll("\r", "\\\\r"); // escape \r
builder.append("\"");
builder.append(string);
builder.append("\"");
} else if (value instanceof Long) {
Long longValue = (Long) value;
builder.append(longValue);
} else if (value instanceof Integer) {
Integer intValue = (Integer) value;
builder.append(intValue);
} else if (value instanceof Boolean) {
Boolean bool = (Boolean) value;
builder.append(bool);
} else if (value instanceof Double) {
Double db = (Double) value;
builder.append(db);
} else {
builder.append(value.toString());
}
}
}
实现逻辑都写好了,集成到自己的项目中还是非常容易的,你只需要在你的项目中引入对应的Maven依赖
<dependencies>
<dependency>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>xmlpull</groupId>
<artifactId>xmlpull</artifactId>
<version>1.1.3.1</version>
</dependency>
<dependency>
<groupId>kxml2</groupId>
<artifactId>kxml2</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
通过下面的工具类你就可以把 Single element XML to JSON Array。其中forceList()函数就是你需要设置强转成数组的节点绝对路径(xPath)
public class Test {
public static void main(String [] args){
String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<library>" +
" <books>" +
"<book>" +
"<name>Java</name>" +
"</book>" +
"</books>" +
"</library>";
XmlToJson xmlToJson2 = new XmlToJson.Builder(xml)
.forceList("/library/books/book")
.build();
System.out.println(xmlToJson2);
}
}
{
"library": {
"books": {
"book": [
{
"name": "Java"
}
]
}
}
}