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
+
+