From 96894ca2062b24096464dc77a82c09584ddd222a Mon Sep 17 00:00:00 2001 From: Francis Dong Date: Thu, 8 Apr 2021 17:47:58 +0800 Subject: [PATCH] add xml to json library --- fizz-common/pom.xml | 27 + .../src/main/java/we/xml/FileReader.java | 69 ++ .../src/main/java/we/xml/JsonToXml.java | 311 ++++++++ fizz-common/src/main/java/we/xml/Node.java | 81 +++ fizz-common/src/main/java/we/xml/Tag.java | 108 +++ .../src/main/java/we/xml/XmlToJson.java | 675 ++++++++++++++++++ .../src/test/java/we/xml/XmlTests.java | 154 ++++ pom.xml | 7 + 8 files changed, 1432 insertions(+) create mode 100644 fizz-common/src/main/java/we/xml/FileReader.java create mode 100644 fizz-common/src/main/java/we/xml/JsonToXml.java create mode 100644 fizz-common/src/main/java/we/xml/Node.java create mode 100644 fizz-common/src/main/java/we/xml/Tag.java create mode 100644 fizz-common/src/main/java/we/xml/XmlToJson.java create mode 100644 fizz-common/src/test/java/we/xml/XmlTests.java diff --git a/fizz-common/pom.xml b/fizz-common/pom.xml index 7ccbdb0..4dc41f0 100644 --- a/fizz-common/pom.xml +++ b/fizz-common/pom.xml @@ -74,6 +74,33 @@ com.alibaba fastjson + + + net.sf.kxml + kxml2 + + + + org.json + json + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + org.skyscreamer + jsonassert + + + + \ No newline at end of file diff --git a/fizz-common/src/main/java/we/xml/FileReader.java b/fizz-common/src/main/java/we/xml/FileReader.java new file mode 100644 index 0000000..fe23fc3 --- /dev/null +++ b/fizz-common/src/main/java/we/xml/FileReader.java @@ -0,0 +1,69 @@ +/* + Copyright 2016 Arnaud Guyon + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package we.xml; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Created by arnaud on 03/12/2016. + */ + +public class FileReader { + + public static String readFileFromAsset(String fileName) { + try { + InputStream inputStream = new FileInputStream(fileName); + String result = readFileFromInputStream(inputStream); + inputStream.close(); + return result; + } catch (IOException e) { + e.printStackTrace(); // TODO + } + return null; + } + + public static String readFileFromInputStream(InputStream inputStream) { + + InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + + StringBuilder result = new StringBuilder(); + String line; + try { + while ((line = bufferedReader.readLine()) != null) { + result.append(line); + } + return result.toString(); + } catch (IOException exception) { + } finally { + try { + bufferedReader.close(); + } catch (IOException e2) { + } + try { + inputStreamReader.close(); + } catch (IOException e2) { + } + } + return null; + } + + +} diff --git a/fizz-common/src/main/java/we/xml/JsonToXml.java b/fizz-common/src/main/java/we/xml/JsonToXml.java new file mode 100644 index 0000000..2fe76cb --- /dev/null +++ b/fizz-common/src/main/java/we/xml/JsonToXml.java @@ -0,0 +1,311 @@ +/* + Copyright 2016 Arnaud Guyon + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package we.xml; + +//import android.util.Xml; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.kxml2.io.KXmlSerializer; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +/** + * Converts JSON to XML
+ *
+ * Add default tag prefix(-) by Francis Dong
+ * Change default content name to #text by Francis Dong
+ */ + +public class JsonToXml { + + private static final String DEFAULT_TAG_PREFIX = "-"; + private static final int DEFAULT_INDENTATION = 3; + // TODO: Set up Locale in the builder + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + + public static class Builder { + + private JSONObject mJson; + private HashSet mForcedAttributes = new HashSet<>(); + private HashSet mForcedContent = new HashSet<>(); + + /** + * Constructor + * @param jsonObject a JSON object + */ + public Builder(JSONObject jsonObject) { + mJson = jsonObject; + } + + /** + * Constructor + * @param inputStream InputStream containing the JSON + */ + public Builder(InputStream inputStream) { + this(FileReader.readFileFromInputStream(inputStream)); + } + + /** + * Constructor + * @param jsonString String containing the JSON + */ + public Builder(String jsonString) { + try { + mJson = new JSONObject(jsonString); + } catch (JSONException exception) { + exception.printStackTrace(); + } + } + + /** + * Force a TAG to be an attribute of the parent TAG + * @param path Path for the attribute, using format like "/parentTag/childTag/childTagAttribute" + * @return the Builder + */ + public Builder forceAttribute(String path) { + mForcedAttributes.add(path); + return this; + } + + /** + * Force a TAG to be the content of its parent TAG + * @param path Path for the content, using format like "/parentTag/contentTag" + * @return the Builder + */ + public Builder forceContent(String path) { + mForcedContent.add(path); + return this; + } + + /** + * Creates the JsonToXml object + * @return a JsonToXml instance + */ + public JsonToXml build() { + return new JsonToXml(mJson, mForcedAttributes, mForcedContent); + } + } + + private JSONObject mJson; + private HashSet mForcedAttributes; + private HashSet mForcedContent; + + private JsonToXml(JSONObject jsonObject, HashSet forcedAttributes, HashSet forcedContent) { + mJson = jsonObject; + mForcedAttributes = forcedAttributes; + mForcedContent = forcedContent; + } + + /** + * + * @return the XML + */ + @Override + public String toString() { + Node rootNode = new Node(null, ""); + prepareObject(rootNode, mJson); + return nodeToXML(rootNode); + } + + /** + * + * @return the formatted XML with a default indent (3 spaces) + */ + public String toFormattedString() { + return toFormattedString(DEFAULT_INDENTATION); + } + + /** + * + * @param indent size of the indent (number of spaces) + * @return the formatted XML + */ + public String toFormattedString(int indent) { + String input = toString(); + try { + Source xmlInput = new StreamSource(new StringReader(input)); + StringWriter stringWriter = new StringWriter(); + StreamResult xmlOutput = new StreamResult(stringWriter); + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "" + indent); + transformer.transform(xmlInput, xmlOutput); + return xmlOutput.getWriter().toString(); + } catch (Exception e) { + throw new RuntimeException(e); // TODO: do my own + } + } + + private String nodeToXML(Node node) { +// XmlSerializer serializer = Xml.newSerializer(); + try { + XmlSerializer serializer = new KXmlSerializer(); + + StringWriter writer = new StringWriter(); + serializer.setOutput(writer); + serializer.startDocument("UTF-8", true); + + nodeToXml(serializer, node); + + serializer.endDocument(); + return writer.toString(); + } catch (Exception e) { + throw new RuntimeException(e); // TODO: do my own + } + } + + private void nodeToXml(XmlSerializer serializer, Node node) throws IOException { + String nodeName = node.getName(); + if (nodeName != null) { + serializer.startTag("", nodeName); + + for (Node.Attribute attribute : node.getAttributes()) { + serializer.attribute("", attribute.mKey, attribute.mValue); + } + String nodeContent = node.getContent(); + if (nodeContent != null) { + serializer.text(nodeContent); + } + } + + for (Node subNode : node.getChildren()) { + nodeToXml(serializer, subNode); + } + + if (nodeName != null) { + serializer.endTag("", nodeName); + } + } + + private void prepareObject(Node node, JSONObject json) { + Iterator keyterator = json.keys(); + while (keyterator.hasNext()) { + String key = keyterator.next(); + Object object = json.opt(key); + if (object != null) { + if (object instanceof JSONObject) { + JSONObject subObject = (JSONObject) object; + String path = node.getPath() + "/" + key; + Node subNode = new Node(key, path); + node.addChild(subNode); + prepareObject(subNode, subObject); + } else if (object instanceof JSONArray) { + JSONArray array = (JSONArray) object; + prepareArray(node, key, array); + } else { + String path = node.getPath() + "/" + key; + // JSON numbers are represented either Integer or Double (IEEE 754) + // Long may be represented in scientific notation because they are stored as Double + // This workaround attempts to represent Long and Double objects accordingly + String value; + if (object instanceof Double) { + double d = (double) object; + // If it is a Long + if (d % 1 == 0) { + value = Long.toString((long) d); + } else { + // TODO: Set up number of decimal digits per attribute in the builder + // Set only once. Represent all double numbers up to 20 decimal digits + if (DECIMAL_FORMAT.getMaximumFractionDigits() == 0) { + DECIMAL_FORMAT.setMaximumFractionDigits(20); + } + value = DECIMAL_FORMAT.format(d); + } + } else { + // Integer, Boolean and String are handled here + value = object.toString(); + } + if (isAttribute(path)) { + if(key.startsWith(DEFAULT_TAG_PREFIX)) { + key = key.substring(1, key.length()); + } + node.addAttribute(key, value); + } else if (isContent(path) ) { + node.setContent(value); + } else { + Node subNode = new Node(key, node.getPath()); + subNode.setContent(value); + node.addChild(subNode); + } + } + } + } + } + + private void prepareArray(Node node, String key, JSONArray array) { + int count = array.length(); + String path = node.getPath() + "/" + key; + for (int i = 0; i < count; ++i) { + Node subNode = new Node(key, path); + Object object = array.opt(i); + if (object != null) { + if (object instanceof JSONObject) { + JSONObject jsonObject = (JSONObject) object; + prepareObject(subNode, jsonObject); + } else if (object instanceof JSONArray) { + JSONArray subArray = (JSONArray) object; + prepareArray(subNode, key, subArray); + } else { + String value = object.toString(); + subNode.setName(key); + subNode.setContent(value); + } + } + node.addChild(subNode); + } + } + + private boolean isAttribute(String path) { + if (mForcedAttributes.contains(path)) { + return true; + } + String[] paths = path.split("/"); + if (paths[paths.length - 1].startsWith(DEFAULT_TAG_PREFIX)) { + return true; + } + return false; + } + + private boolean isContent(String path) { + if (mForcedContent.contains(path)) { + return true; + } + String[] paths = path.split("/"); + if ("#text".equals(paths[paths.length - 1])) { + return true; + } + return false; + } +} diff --git a/fizz-common/src/main/java/we/xml/Node.java b/fizz-common/src/main/java/we/xml/Node.java new file mode 100644 index 0000000..7bdb2c9 --- /dev/null +++ b/fizz-common/src/main/java/we/xml/Node.java @@ -0,0 +1,81 @@ +/* + Copyright 2016 Arnaud Guyon + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package we.xml; + +import java.util.ArrayList; + +/** + * Used to store data when converting from JSON to XML + */ + +/* package */ class Node { + + /* package */ class Attribute { + String mKey; + String mValue; + Attribute(String key, String value) { + mKey = key; + mValue = value; + } + } + + private String mName; + private String mPath; + private String mContent; + private ArrayList mAttributes = new ArrayList<>(); + private ArrayList mChildren = new ArrayList<>(); + + /* package */ Node(String name, String path) { + mName = name; + mPath = path; + } + + /* package */ void addAttribute(String key, String value) { + mAttributes.add(new Attribute(key, value)); + } + + /* package */ void setContent(String content) { + mContent = content; + } + + /* package */ void setName(String name) { + mName = name; + } + + /* package */ void addChild(Node child) { + mChildren.add(child); + } + + /* package */ ArrayList getAttributes() { + return mAttributes; + } + + /* package */ String getContent() { + return mContent; + } + + /* package */ ArrayList getChildren() { + return mChildren; + } + + /* package */ String getPath() { + return mPath; + } + + /* package */ String getName() { + return mName; + } +} diff --git a/fizz-common/src/main/java/we/xml/Tag.java b/fizz-common/src/main/java/we/xml/Tag.java new file mode 100644 index 0000000..4a5db33 --- /dev/null +++ b/fizz-common/src/main/java/we/xml/Tag.java @@ -0,0 +1,108 @@ +package we.xml; + +/* + Copyright 2016 Arnaud Guyon + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Class used to store XML hierarchy + * + */ +public class Tag { + + private String mPath; + private String mName; + private ArrayList mChildren = new ArrayList<>(); + private String mContent; + + /* package */ Tag(String path, String name) { + mPath = path; + mName = name; + } + + /* package */ void addChild(Tag tag) { + mChildren.add(tag); + } + + /* package */ 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 getChildren() { + return mChildren; + } + + /* package */ boolean hasChildren() { + return (mChildren.size() > 0); + } + + /* package */ int getChildrenCount() { + return mChildren.size(); + } + + /* package */ Tag getChild(int index) { + if ((index >= 0) && (index < mChildren.size())) { + return mChildren.get(index); + } + return null; + } + + /* package */ HashMap> getGroupedElements() { + HashMap> groups = new HashMap<>(); + for(Tag child : mChildren) { + String key = child.getName(); + ArrayList group = groups.get(key); + if (group == null) { + group = new ArrayList<>(); + groups.put(key, group); + } + group.add(child); + } + return groups; + } + + /* package */ String getPath() { + return mPath; + } + + @Override + public String toString() { + return "Tag: " + mName + ", " + mChildren.size() + " children, Content: " + mContent; + } +} diff --git a/fizz-common/src/main/java/we/xml/XmlToJson.java b/fizz-common/src/main/java/we/xml/XmlToJson.java new file mode 100644 index 0000000..b7e777d --- /dev/null +++ b/fizz-common/src/main/java/we/xml/XmlToJson.java @@ -0,0 +1,675 @@ +package we.xml; + +/* + Copyright 2016 Arnaud Guyon + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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; + +/** + * Converts XML to JSON
+ *
+ * Add default tag prefix(-) by Francis Dong
+ * Change default content name to #text by Francis Dong
+ * Add handerListItem method to handler list item by Francis Dong
+ */ + +public class XmlToJson { + + private static final Logger LOGGER = LoggerFactory.getLogger(XmlToJson.class); + + private static final String TAG = "XmlToJson"; +// private static final String DEFAULT_CONTENT_NAME = "content"; + private static final String DEFAULT_CONTENT_NAME = "#text"; + private static final String DEFAULT_TAG_PREFIX = "-"; + 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 mForceListPaths = new HashSet<>(); + private HashSet mForceListPatterns = new HashSet<>(); + private HashMap mAttributeNameReplacements = new HashMap<>(); + private HashMap mContentNameReplacements = new HashMap<>(); + private HashMap mForceClassForPath = new HashMap<>(); // Integer, Long, Double, Boolean + private HashSet mSkippedAttributes = new HashSet<>(); + private HashSet 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 mForceListPaths; + private HashSet mForceListPatterns = new HashSet<>(); + private HashMap mAttributeNameReplacements; + private HashMap mContentNameReplacements; + private HashMap mForceClassForPath; + private HashSet mSkippedAttributes = new HashSet<>(); + private HashSet 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(false); // 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 { + LOGGER.info("{} unknown xml eventType {}", TAG, 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> groups = tag.getGroupedElements(); // groups by tag names so that we can detect lists or single elements + for (ArrayList 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(handerListItem(child.getPath(), 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(handerListItem(child.getPath(), convertTagToJson(child, true))); + } + String childrenNames = group.get(0).getName(); + json.put(childrenNames, list); + } + } + return json; + + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } + + /** + * Convert to single value if JSONObject only contain content key
+ * Returns "abc" if JSONObject is {"#text": "abc"}
+ */ + @SuppressWarnings({ "rawtypes", "unused" }) + private Object handerListItem(String path, JSONObject json) { + if(json.length() == 1 && json.has(DEFAULT_CONTENT_NAME)) { + Object val = json.get(DEFAULT_CONTENT_NAME); + if(val == null) { + return val; + } + String content = String.valueOf(val); + 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 + return content; + } else { + if (forcedClass == Integer.class) { + try { + return Integer.parseInt(content); + } catch (NumberFormatException exception) { + return DEFAULT_EMPTY_INTEGER; + } + } else if (forcedClass == Long.class) { + try { + return Long.parseLong(content); + } catch (NumberFormatException exception) { + return DEFAULT_EMPTY_LONG; + } + } else if (forcedClass == Double.class) { + try { + return Double.parseDouble(content); + } catch (NumberFormatException exception) { + return DEFAULT_EMPTY_DOUBLE; + } + } else if (forcedClass == Boolean.class) { + if (content == null) { + return DEFAULT_EMPTY_BOOLEAN; + } else if (content.equalsIgnoreCase("true")) { + return true; + } else if (content.equalsIgnoreCase("false")) { + return false; + } else { + return DEFAULT_EMPTY_BOOLEAN; + } + } + } + } catch (JSONException exception) { + // keep continue in case of error + } + } + return json; + } + + 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 DEFAULT_TAG_PREFIX + defaultValue; + } + + private String getContentNameReplacement(String path, String defaultValue) { + String result = mContentNameReplacements.get(path); + if (result != null) { + return result; + } + return defaultValue; + } + + @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 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 { + builder.append(indent); + builder.append(mIndentationPattern); + 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()); + } + } + +} diff --git a/fizz-common/src/test/java/we/xml/XmlTests.java b/fizz-common/src/test/java/we/xml/XmlTests.java new file mode 100644 index 0000000..b6c0f43 --- /dev/null +++ b/fizz-common/src/test/java/we/xml/XmlTests.java @@ -0,0 +1,154 @@ +package we.xml; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +public class XmlTests { + + private String xmlStr = "\n" + "\n" + + " James Bond\n" + ""; + + private String jsonStr1 = "{\"library\":{\"book\":{\"#text\":\"James Bond\",\"-id\":\"007\"}}}"; + private String jsonStr2 = "{\n" + + " \"library\": {\n" + + " \"owner\": \"John Doe\",\n" + + " \"book\": [\n" + + " \"James Bond\",\n" + + " \"Book for the dummies\"\n" + + " ]\n" + + " }\n" + + "}"; + + private String jsonStr3 = "{\n" + + " \"library\": {\n" + + " \"book\": [\n" + + " \"James Bond\",\n" + + " \"Book for the dummies\"\n" + + " ]\n" + + " }\n" + + "}"; + + private String jsonStr4 = "{\n" + + " \"library\": {\n" + + " \"owner\": \"John Doe\",\n" + + " \"book\": [\n" + + " {\n" + + " \"-id\": \"007\",\n" + + " \"#text\": \"James Bond\"\n" + + " },\n" + + " \"Book for the dummies\"\n" + + " ]\n" + + " }\n" + + "}"; + + private String jsonStr5 = "{\n" + + " \"library\": {\n" + + " \"owner\": \"John Doe\",\n" + + " \"book\": [\n" + + " \"1\",\n" + + " \"2\"\n" + + " ]\n" + + " }\n" + + "}"; + + @Test + public void TestXmlToJson() { + XmlToJson xmlToJson = new XmlToJson.Builder(xmlStr).build(); + String jsonStr = xmlToJson.toString(); + + // System.out.println(jsonStr); + + JSONObject jsonObj = new JSONObject(jsonStr); + + assertEquals("007", jsonObj.getJSONObject("library").getJSONObject("book").getString("-id")); + assertEquals("James Bond", jsonObj.getJSONObject("library").getJSONObject("book").getString("#text")); + } + + @Test + public void TestXmlToJsonForceList() { + XmlToJson xmlToJson = new XmlToJson.Builder(xmlStr).forceList("/library/book").build(); + String jsonStr = xmlToJson.toString(); + + JSONObject jsonObj = new JSONObject(jsonStr); + + assertEquals("007", jsonObj.getJSONObject("library").getJSONArray("book").getJSONObject(0).getString("-id")); + assertEquals("James Bond", jsonObj.getJSONObject("library").getJSONArray("book").getJSONObject(0).getString("#text")); + } + + @Test + public void TestJsonToXml1() { + JsonToXml jsonToXml = new JsonToXml.Builder(jsonStr1).build(); + + XmlToJson xmlToJson = new XmlToJson.Builder(jsonToXml.toString()).forceList("/library/book").build(); + String jsonStr = xmlToJson.toString(); + + JSONObject jsonObj = new JSONObject(jsonStr); + + assertEquals("007", jsonObj.getJSONObject("library").getJSONArray("book").getJSONObject(0).getString("-id")); + assertEquals("James Bond", jsonObj.getJSONObject("library").getJSONArray("book").getJSONObject(0).getString("#text")); + } + + @Test + public void TestJsonToXml2() { + JsonToXml jsonToXml = new JsonToXml.Builder(jsonStr2).build(); + + XmlToJson xmlToJson = new XmlToJson.Builder(jsonToXml.toString()).build(); + String jsonStr = xmlToJson.toString(); + + JSONObject jsonObj = new JSONObject(jsonStr); + + // System.out.println(xmlToJson.toFormattedString()); + assertEquals("John Doe", jsonObj.getJSONObject("library").getString("owner")); + assertEquals("James Bond", jsonObj.getJSONObject("library").getJSONArray("book").get(0).toString()); + assertEquals("Book for the dummies", jsonObj.getJSONObject("library").getJSONArray("book").get(1).toString()); + } + + @Test + public void TestJsonToXml3() { + JsonToXml jsonToXml = new JsonToXml.Builder(jsonStr3).build(); + + XmlToJson xmlToJson = new XmlToJson.Builder(jsonToXml.toString()).build(); + String jsonStr = xmlToJson.toString(); + + JSONObject jsonObj = new JSONObject(jsonStr); + + assertEquals("James Bond", jsonObj.getJSONObject("library").getJSONArray("book").get(0).toString()); + assertEquals("Book for the dummies", jsonObj.getJSONObject("library").getJSONArray("book").get(1).toString()); + } + + @Test + public void TestJsonToXml4() { + JsonToXml jsonToXml = new JsonToXml.Builder(jsonStr4).build(); + + XmlToJson xmlToJson = new XmlToJson.Builder(jsonToXml.toString()).build(); + String jsonStr = xmlToJson.toString(); + + JSONObject jsonObj = new JSONObject(jsonStr); + + // System.out.println(xmlToJson.toFormattedString()); + assertEquals("007", jsonObj.getJSONObject("library").getJSONArray("book").getJSONObject(0).getString("-id")); + assertEquals("James Bond", jsonObj.getJSONObject("library").getJSONArray("book").getJSONObject(0).getString("#text")); + assertEquals("Book for the dummies", jsonObj.getJSONObject("library").getJSONArray("book").get(1).toString()); + } + + @Test + public void TestJsonToXml5() { + JsonToXml jsonToXml = new JsonToXml.Builder(jsonStr5).build(); + + XmlToJson xmlToJson = new XmlToJson.Builder(jsonToXml.toString()).forceIntegerForPath("/library/book").build(); + String jsonStr = xmlToJson.toString(); + + JSONObject jsonObj = new JSONObject(jsonStr); + + // System.out.println(xmlToJson.toFormattedString()); + assertEquals("John Doe", jsonObj.getJSONObject("library").getString("owner")); + Object val = jsonObj.getJSONObject("library").getJSONArray("book").get(0); + assertTrue(val instanceof Integer); + assertEquals(1, jsonObj.getJSONObject("library").getJSONArray("book").getInt(0)); + assertEquals(2, jsonObj.getJSONObject("library").getJSONArray("book").getInt(1)); + } + +} diff --git a/pom.xml b/pom.xml index 860c32b..55a3a52 100644 --- a/pom.xml +++ b/pom.xml @@ -351,6 +351,13 @@ bcpkix-jdk15on 1.64 + + + net.sf.kxml + kxml2 + 2.3.0 + +