From 15cf3ad6eee3b4c611e8f15bac15c750cf2cfcb3 Mon Sep 17 00:00:00 2001 From: jiachengzhuo <“richardjcheuk@gmail.com”> Date: Fri, 2 May 2025 12:03:08 -0700 Subject: [PATCH 01/12] Milestone 2 Task 2: Add XML.toJSONObject(...) with replacement logic and corresponding tests --- src/main/java/org/json/XML.java | 333 +++++++++++++++++- .../org/json/junit/XMLPointerReplaceTest.java | 52 +++ 2 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/json/junit/XMLPointerReplaceTest.java diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index 4bf475935..36bcd1f5d 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -8,7 +8,10 @@ import java.io.StringReader; import java.math.BigDecimal; import java.math.BigInteger; -import java.util.Iterator; +import java.util.*; +import java.io.BufferedReader; +import java.io.Reader; +import java.util.stream.Collectors; /** * This provides static methods to convert an XML text into a JSONObject, and to @@ -63,6 +66,10 @@ public XML() { */ public static final String TYPE_ATTR = "xsi:type"; + private static boolean replaced = false; + private static boolean skipCurrentKey = false; + + /** * Creates an iterator for navigating Code Points in a string instead of * characters. Once Java7 support is dropped, this can be replaced with @@ -779,6 +786,330 @@ public static JSONObject toJSONObject(Reader reader, XMLParserConfiguration conf } return jo; } + /** SWE262P MileStone2 project, Task2 by Jiacheng Zhuo **/ + + /** Edit the parse method, add functions for the replacement implement **/ + private static boolean parseMilestone2(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config, int currentNestingDepth, List targetPath, + int targetPathLength, + Map arrayKey, + boolean isReplace, + boolean mergeToParent, + JSONObject replacement) + throws JSONException { + char c; + int i; + JSONObject jsonObject = null; + String string; + String tagName; + Object token; + XMLXsiTypeConverter xmlXsiTypeConverter; + + + // Test for and skip past these forms: + // + // + // + // + // Report errors for these forms: + // <> + // <= + // << + + token = x.nextToken(); + + // "); + return false; + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if ("CDATA".equals(token)) { + if (x.next() == '[') { + string = x.nextCDATA(); + if (string.length() > 0) { + context.accumulate(config.getcDataTagName(), string); + } + return false; + } + } + throw x.syntaxError("Expected 'CDATA['"); + } + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + return false; + } else if (token == QUEST) { + + // "); + return false; + } else if (token == SLASH) { + + // Close tag "); + return false; + } + + replaced = true; + x.skipPast(currentTag + ">"); + return false; + } + + if (isReplace && hasIndex && !indexMatches) { + arrayKey.put(currentTag, remainingIndex - 1); + } + + if (!isReplace) { + if (hasIndex) { + skipCurrentKey = (remainingIndex != 0); + arrayKey.put(currentTag, remainingIndex - 1); + } + + if (!targetPath.get(currentNestingDepth).equals(currentTag)) { + skipCurrentKey = true; + } + + } + } //--------add replacement logic ends-----------------------------------// + tagName = (String) token; + token = null; + jsonObject = new JSONObject(); + boolean nilAttributeFound = false; + xmlXsiTypeConverter = null; + for (;;) { + if (token == null) { + token = x.nextToken(); + } + // attribute = value + if (token instanceof String) { + string = (String) token; + token = x.nextToken(); + if (token == EQ) { + token = x.nextToken(); + if (!(token instanceof String)) { + throw x.syntaxError("Missing value"); + } + + if (config.isConvertNilAttributeToNull() + && NULL_ATTR.equals(string) + && Boolean.parseBoolean((String) token)) { + nilAttributeFound = true; + } else if(config.getXsiTypeMap() != null && !config.getXsiTypeMap().isEmpty() + && TYPE_ATTR.equals(string)) { + xmlXsiTypeConverter = config.getXsiTypeMap().get(token); + } else if (!nilAttributeFound) { + Object obj = stringToValue((String) token); + if (obj instanceof Boolean) { + jsonObject.accumulate(string, + config.isKeepBooleanAsString() + ? ((String) token) + : obj); + } else if (obj instanceof Number) { + jsonObject.accumulate(string, + config.isKeepNumberAsString() + ? ((String) token) + : obj); + } else { + jsonObject.accumulate(string, stringToValue((String) token)); + } + } + token = null; + } else { + jsonObject.accumulate(string, ""); + } + + + } else if (token == SLASH) { + // Empty tag <.../> + if (x.nextToken() != GT) { + throw x.syntaxError("Misshaped tag"); + } + if (config.getForceList().contains(tagName)) { + // Force the value to be an array + if (nilAttributeFound) { + context.append(tagName, JSONObject.NULL); + } else if (jsonObject.length() > 0) { + context.append(tagName, jsonObject); + } else { + context.put(tagName, new JSONArray()); + } + } else { + if (nilAttributeFound) { + context.accumulate(tagName, JSONObject.NULL); + } else if (jsonObject.length() > 0) { + context.accumulate(tagName, jsonObject); + } else { + context.accumulate(tagName, ""); + } + } + return false; + + } else if (token == GT) { + // Content, between <...> and + for (;;) { + token = x.nextContent(); + if (token == null) { + if (tagName != null) { + throw x.syntaxError("Unclosed tag " + tagName); + } + return false; + } else if (token instanceof String) { + string = (String) token; + if (string.length() > 0) { + if(xmlXsiTypeConverter != null) { + jsonObject.accumulate(config.getcDataTagName(), + stringToValue(string, xmlXsiTypeConverter)); + } else { + Object obj = stringToValue((String) token); + if (obj instanceof Boolean) { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepBooleanAsString() + ? ((String) token) + : obj); + } else if (obj instanceof Number) { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepNumberAsString() + ? ((String) token) + : obj); + } else { + jsonObject.accumulate(config.getcDataTagName(), stringToValue((String) token)); + } + } + } + + } else if (token == LT) { + + if (parseMilestone2(x, jsonObject, tagName, config, currentNestingDepth + 1, + targetPath, targetPathLength, arrayKey, isReplace, mergeToParent, replacement)) { + if (config.getForceList().contains(tagName)) { + if (jsonObject.length() == 0) { + context.put(tagName, new JSONArray()); + } else if (jsonObject.length() == 1 + && jsonObject.opt(config.getcDataTagName()) != null) { + context.append(tagName, jsonObject.opt(config.getcDataTagName())); + } else { + context.append(tagName, jsonObject); + } + } else { + if (jsonObject.length() == 0) { + context.accumulate(tagName, ""); + } else if (jsonObject.length() == 1 + && jsonObject.opt(config.getcDataTagName()) != null) { + context.accumulate(tagName, jsonObject.opt(config.getcDataTagName())); + } else { + if (!config.shouldTrimWhiteSpace()) { + removeEmpty(jsonObject, config); + } + context.accumulate(tagName, jsonObject); + } + } + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } + /** + * Converts an XML input stream into a JSONObject, replacing a sub-object at a specified JSONPointer path. + * + *

This method is added as part of SWE262P Milestone2 Task2. It performs in-place replacement + * during parsing, avoiding the need to first build the entire JSON tree before modifying it. + * This offers performance benefits by allowing early exit from the parser once the target node is handled.

+ * + * @param reader The XML input + * @param path The JSONPointer path where replacement should occur + * @param replacement The JSONObject to insert at the given path + * @return A JSONObject with the sub-object at the given path replaced + * @throws JSONException if parsing or path manipulation fails + */ + public static JSONObject toJSONObject(Reader reader, JSONPointer path, JSONObject replacement) throws JSONException { + JSONObject jo = new JSONObject(); + XMLTokener x = new XMLTokener(reader); + + // Reset shared state + replaced = false; + skipCurrentKey = false; + + String[] segments = path.toString().split("/"); + List targetPath = Arrays.stream(segments) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + int targetPathLength = targetPath.size(); + + Map arrayKey = new HashMap<>(); + for (int i = 0; i < segments.length; i++) { + if (segments[i].matches("\\d+") && i > 0) { + arrayKey.put(segments[i - 1], Integer.parseInt(segments[i])); + } + } + + boolean mergeToParent = !path.toString().endsWith("/"); + + while (x.more()) { + x.skipPast("<"); + if (x.more()) { + parseMilestone2(x, jo, null, XMLParserConfiguration.ORIGINAL, 0, + targetPath, targetPathLength, arrayKey, true, mergeToParent, replacement); + } + } + + if (replaced) { + return jo; + } else { + throw new JSONException("Replacement failed or path not found: " + path); + } + } + /** * Convert a well-formed (but not necessarily valid) XML string into a diff --git a/src/test/java/org/json/junit/XMLPointerReplaceTest.java b/src/test/java/org/json/junit/XMLPointerReplaceTest.java new file mode 100644 index 000000000..86346be6c --- /dev/null +++ b/src/test/java/org/json/junit/XMLPointerReplaceTest.java @@ -0,0 +1,52 @@ +package org.json.junit; + +import org.json.JSONObject; +import org.json.JSONPointer; +import org.json.XML; +import org.junit.Test; +import static org.junit.Assert.*; + + +import java.io.StringReader; + +public class XMLPointerReplaceTest { + + @Test + public void testReplaceSubObject_success() { + String xml = "<content>Old Title</content>John"; + StringReader reader = new StringReader(xml); + + JSONObject replacement = new JSONObject().put("content", "New Title"); + JSONPointer pointer = new JSONPointer("/book/title"); + + JSONObject result = XML.toJSONObject(reader, pointer, replacement); + + assertEquals("New Title", result.getJSONObject("book").getJSONObject("title").get("content")); + assertEquals("John", result.getJSONObject("book").get("author")); + } + + + + + + @Test(expected = RuntimeException.class) + public void testReplaceSubObject_invalidPath() { + String xml = "Old Title"; + StringReader reader = new StringReader(xml); + + JSONObject replacement = new JSONObject().put("name", "Unknown"); + JSONPointer pointer = new JSONPointer("/abcde/fghijk"); + + XML.toJSONObject(reader, pointer, replacement); + } + + @Test(expected = RuntimeException.class) + public void testReplaceSubObject_nullReader() { + JSONObject replacement = new JSONObject().put("title", "Anything"); + JSONPointer pointer = new JSONPointer("/book/title"); + + XML.toJSONObject(null, pointer, replacement); + } +} + + From 06e8a2378413ff145f6eee5a34c1ffec53b46b5b Mon Sep 17 00:00:00 2001 From: wsw-stack <1770115879@qq.com> Date: Fri, 2 May 2025 17:18:39 -0700 Subject: [PATCH 02/12] add a function and update readme --- .gitignore | 1 + README.md | 21 +- src/main/java/org/json/XML.java | 507 ++++++++++++++++++ .../tests/XMLJsonPointerQueryingTest.java | 86 +++ .../tests}/XMLPointerReplaceTest.java | 2 +- 5 files changed, 614 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/json/junit/milestone2/tests/XMLJsonPointerQueryingTest.java rename src/test/java/org/json/junit/{ => milestone2/tests}/XMLPointerReplaceTest.java (97%) diff --git a/.gitignore b/.gitignore index b78af4db7..4af089d79 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .idea *.iml /target/ +/src/test/resources/xml_files /bin/ build diff --git a/README.md b/README.md index 206afbb21..05de306fc 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Bug fixes, code improvements, and unit test coverage changes are welcome! Becaus # Build Instructions The org.json package can be built from the command line, Maven, and Gradle. The unit tests can be executed from Maven, Gradle, or individually in an IDE e.g. Eclipse. - + **Building from the command line** *Build the class files from the package root directory src/main/java* @@ -83,7 +83,7 @@ java -cp .:json-java.jar Test (Unix Systems) {"abc":"def"} ``` - + **Tools to build the package and execute the unit tests** Execute the test suite with Maven: @@ -109,6 +109,23 @@ gradlew testWithStrictMode mvn test -P test-strict-mode ``` +# Milestone 2 + +For the Milestone 2 of SWE262P, 2 new functions were added: + +```java +static JSONObject toJSONObject(Reader reader, JSONPointer path) +static JSONObject toJSONObject(Reader reader, JSONPointer path, JSONObject replacement) +``` + +The first one takes in 2 parameters, a `Reader` object which contains some XML input and a `JSONPointer` object that includes a json path for querying, and returns a `JSONObject` object which has the corresponding path, or throw an error if that path does not exist. + +The first one takes in 3 parameters, a `Reader` object which contains some XML input and a `JSONPointer` object that includes a json path for querying, and a `JSONObject` for replacement. And returns a new `JSONObject` object which has the corresponding path replaced with the new given object, or throw an error if that path does not exist. + +Both new functions are placed in the `XML.java` file. + +The test cases of the functions are placed under the `org.json.junit.milestone2.tests` package, and to run the test case which deals with large file input, first create a folder named `xml_files` under the `/src/test/resources` path, and put the xml files for testing under this path. + # Notes For more information, please see [NOTES.md](https://github.com/stleary/JSON-java/blob/master/docs/NOTES.md) diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index 36bcd1f5d..2991622d4 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -786,6 +786,513 @@ public static JSONObject toJSONObject(Reader reader, XMLParserConfiguration conf } return jo; } + + /** + * + * @param reader, a reader with XML content inside + * @param path, a Json path for querying the inside object + * @return a Json object which matches the given JsonPointer path, or throw an error if not found + * @throws JSONException + */ + public static JSONObject toJSONObject(Reader reader, JSONPointer path) throws JSONException { + XMLTokener x = new XMLTokener(reader); + // parse the JSONPointer + String pointerExpr = path.toString(); + List tokens = new ArrayList<>(); + if (!pointerExpr.isEmpty()) { + String[] parts = pointerExpr.split("/", -1); + for (int i = (pointerExpr.startsWith("/") ? 1 : 0); i < parts.length; i++) { + String part = parts[i].replace("~1", "/").replace("~0", "~"); + if (part.isEmpty()) { + tokens.add(""); + } else if (part.matches("-?\\d+") && !(part.startsWith("0") && part.length() > 1)) { + try { + int index = Integer.parseInt(part); + if (index < 0) { + tokens.add(part); + } else { + tokens.add(index); + continue; + } + } catch (NumberFormatException e) { + } + } + tokens.add(part); + } + } + // pointer is empty, then parse the whole document + if (tokens.isEmpty()) { + return XML.toJSONObject(reader); + } + + JSONObject result = null; + Object firstToken = tokens.get(0); + if (!(firstToken instanceof String)) { + throw new JSONException("Path not found: " + path); + } + String targetRoot = (String) firstToken; + Integer targetRootIndex = null; + int nextTokenIndex = 1; + if (nextTokenIndex < tokens.size() && tokens.get(nextTokenIndex) instanceof Integer) { + targetRootIndex = (Integer) tokens.get(nextTokenIndex); + nextTokenIndex++; + } + int currentIndexCount = 0; + + while (x.more()) { + x.skipPast("<"); + if (!x.more()) break; + char c = x.next(); + if (c == '?') { + // XML announcement + x.skipPast("?>"); + continue; + } + if (c == '!') { + if (x.more()) { + char c2 = x.next(); + if (c2 == '-' && x.more() && x.next() == '-') { + x.skipPast("-->"); + } else if (c2 == '[') { + x.skipPast("]]>"); + } else { + // skip "); + } + } + continue; + } + if (c == '/') { + // unexpected closing tag + continue; + } + x.back(); + Object token = x.nextToken(); + if (!(token instanceof String)) { + throw x.syntaxError("Misshaped element"); + } + String tagName = (String) token; + // if the top level matches + if (tagName.equals(targetRoot)) { + // get the index + if (targetRootIndex != null) { + if (currentIndexCount < targetRootIndex) { + // skip the whole tree if not reached yet + skipElement(x, tagName); + currentIndexCount++; + continue; + } else if (currentIndexCount > targetRootIndex) { + break; + } + } + currentIndexCount++; + // path only contains root element itself + if (nextTokenIndex >= tokens.size()) { + // parse the whole root element as JSONObject + result = parseElement(x, tagName); + break; + } + boolean selfClosing = false; + JSONObject currentObj = new JSONObject(); + while (true) { + token = x.nextToken(); + if (token == null) { + throw x.syntaxError("Misshaped tag"); + } + if (token instanceof Character) { + char ch = (Character) token; + if (ch == '>') { + break; + } + if (ch == '/') { + // end of element + if (x.next() != '>') { + throw x.syntaxError("Misshaped tag"); + } + selfClosing = true; + break; + } + } else { + String attrName = (String) token; + Object nextTok = x.nextToken(); + if (nextTok == XML.EQ) { + Object attrValueToken = x.nextToken(); + if (!(attrValueToken instanceof String)) { + throw x.syntaxError("Missing value for attribute " + attrName); + } + String attrValue = (String) attrValueToken; + currentObj.accumulate(attrName, XML.stringToValue(attrValue)); + } else { + currentObj.accumulate(attrName, ""); + token = nextTok; + if (token instanceof Character) { + x.back(); + } + } + } + } + if (selfClosing) { + // if tag is empty and path does not end here + result = null; + } else { + result = findInElement(x, tagName, nextTokenIndex, tokens); + } + // if result already found + if (result != null) { + break; + } else { + // continue searching for the element + continue; + } + } else { + // if the top level does not match + skipElement(x, tagName); + } + } + if (result == null) { + throw new JSONException("Path not found: " + path.toString()); + } + return result; + } + + /** + * Helper method: skip the current element and its entire subtree without + * building any JSON output. + * + * Preconditions: + * The caller has already read the element name (we are positioned + * right after the `') { // normal end of start‑tag + break; + } + if (ch == '/') { // empty‑element tag `/>` + if (x.next() != '>') { + throw x.syntaxError("Misshaped tag"); + } + selfClosing = true; + break; + } + } + // Otherwise ‑– attribute name or value, ignore + } + if (!selfClosing) { + // Skip everything until we see the matching close tag + int depth = 0; + while (true) { + x.skipPast("<"); + if (!x.more()) { + throw x.syntaxError("Unclosed tag " + tagName); + } + char c = x.next(); + if (c == '/') { + // Found a closing tag + Object nameToken = x.nextToken(); + if (!(nameToken instanceof String)) { + throw x.syntaxError("Missing close name"); + } + String closeName = (String) nameToken; + if (x.next() != '>') { + throw x.syntaxError("Misshaped close tag"); + } + if (closeName.equals(tagName)) { + if (depth == 0) { + // Reached the matching close tag – done + break; + } else { + // Closing an inner tag with the same name + depth--; + continue; + } + } else { + // Closing some other tag – ignore + continue; + } + } else if (c == '!') { + // Comment / CDATA / DOCTYPE – skip + if (x.more()) { + char c2 = x.next(); + if (c2 == '-' && x.more() && x.next() == '-') { + x.skipPast("-->"); + } else if (c2 == '[') { + x.skipPast("]]>"); + } else { + x.skipPast(">"); + } + } + continue; + } else if (c == '?') { + // Processing instruction – skip + x.skipPast("?>"); + continue; + } else { + // New child element – recurse to skip it + x.back(); + Object newName = x.nextToken(); + if (!(newName instanceof String)) { + throw x.syntaxError("Misshaped tag"); + } + skipElement(x, (String) newName); + // If the child has the same tag name, track nesting depth + if (((String) newName).equals(tagName)) { + depth++; + } + } + } + } + } + + /** + * Search within the current element for the sub‑path specified by `tokens`, + * starting at `tokenIndex`. Stops as soon as the desired node is found. + * + * @param x XMLTokener – cursor is right after the parent start‑tag. + * @param parentName The name of the element we are currently inside. + * @param tokenIndex Index of the current JSONPointer token to match. + * @param tokens Full list of JSONPointer tokens. + * @return The matched JSONObject, or {@code null} if not found here. + */ + private static JSONObject findInElement(XMLTokener x, + String parentName, + int tokenIndex, + List tokens) throws JSONException { + String targetName = null; // child element name to look for + Integer targetIndex = null; // optional array index + int nextIndex = tokenIndex; + + if (tokenIndex < tokens.size()) { + Object tk = tokens.get(tokenIndex); + if (tk instanceof String) { + targetName = (String) tk; + if (tokenIndex + 1 < tokens.size() + && tokens.get(tokenIndex + 1) instanceof Integer) { + targetIndex = (Integer) tokens.get(tokenIndex + 1); + nextIndex = tokenIndex + 2; + } else { + nextIndex = tokenIndex + 1; + } + } else { // JSONPointer should not give a number at an object level + return null; + } + } + + int count = 0; // how many siblings seen + JSONObject result = null; + + while (true) { + Object contentToken = x.nextContent(); + if (contentToken == null) { + throw x.syntaxError("Unclosed tag " + parentName); + } + if (contentToken instanceof String) { + // Ignore plain text (unless pointer explicitly targets "content") + continue; + } + if (contentToken instanceof Character && (Character) contentToken == '<') { + char c = x.next(); + if (c == '/') { // end‑tag for parent + Object closeName = x.nextToken(); + if (!(closeName instanceof String) || !closeName.equals(parentName)) { + throw x.syntaxError("Mismatched close tag for " + parentName); + } + if (x.next() != '>') { + throw x.syntaxError("Misshaped close tag"); + } + break; // search in this parent finished + } + if (c == '?') { x.skipPast("?>"); continue; } + if (c == '!') { + // comment / CDATA – skip + if (x.more()) { + char c2 = x.next(); + if (c2 == '-' && x.more() && x.next() == '-') { + x.skipPast("-->"); + } else if (c2 == '[') { + x.skipPast("]]>"); + } else { + x.skipPast(">"); + } + } + continue; + } + // Child element start + x.back(); + Object childToken = x.nextToken(); + if (!(childToken instanceof String)) { + throw x.syntaxError("Bad tag syntax"); + } + String childName = (String) childToken; + + if (targetName != null && childName.equals(targetName)) { + // Found desired child name + if (targetIndex != null) { // need a specific index + if (count < targetIndex) { + skipElement(x, childName); // not yet reached – skip + count++; + continue; + } + count++; // now at the right sibling + } else { // first match is enough + count++; + if (count > 1) { // ambiguous path + skipElement(x, childName); + continue; + } + } + + // Dive into this child + if (nextIndex >= tokens.size()) { + result = parseElement(x, childName); // path ends here + } else { + result = findInElement(x, childName, nextIndex, tokens); + } + return result; // regardless of success, stop searching siblings + } else { + // Not the target child – skip whole subtree + skipElement(x, childName); + } + } + } + return null; // target not found in this element + } + + /** + * Parse the current element (including its subtree) into a JSONObject. + * + * Preconditions: + * – Caller has already consumed the element name; tokenizer cursor is + * positioned immediately after that name token. + */ + private static JSONObject parseElement(XMLTokener x, String tagName) throws JSONException { + JSONObject jo = new JSONObject(); + Object token; + boolean selfClosing = false; + + /* ---------- Parse attributes ---------- */ + while ((token = x.nextToken()) != null) { + if (token instanceof Character) { + char ch = (Character) token; + if (ch == '>') { break; } // end of start‑tag + if (ch == '/') { // empty element + if (x.next() != '>') { + throw x.syntaxError("Misshaped tag"); + } + selfClosing = true; + break; + } + } else { + String attrName = (String) token; + Object nextTok = x.nextToken(); + if (nextTok == XML.EQ) { + Object valTok = x.nextToken(); + if (!(valTok instanceof String)) { + throw x.syntaxError("Missing value for attribute " + attrName); + } + jo.accumulate(attrName, XML.stringToValue((String) valTok)); + } else { + // Attribute without value + jo.accumulate(attrName, ""); + if (nextTok instanceof Character) { + char ch2 = (Character) nextTok; + if (ch2 == '>') { break; } + if (ch2 == '/') { + if (x.next() != '>') throw x.syntaxError("Misshaped tag"); + selfClosing = true; + break; + } + } + token = nextTok; // nextTok could be another attribute name + continue; + } + } + } + if (selfClosing) { + return jo; // nothing more to parse + } + + /* ---------- Parse children / text ---------- */ + StringBuilder textBuf = null; + while (true) { + Object contentToken = x.nextContent(); + if (contentToken == null) { + throw x.syntaxError("Unclosed tag " + tagName); + } + if (contentToken instanceof String) { + String txt = (String) contentToken; + if (!txt.isEmpty()) { + if (textBuf == null) textBuf = new StringBuilder(); + textBuf.append(XML.stringToValue(txt)); + } + } else if (contentToken instanceof Character + && (Character) contentToken == '<') { + char c = x.next(); + if (c == '/') { // end‑tag + Object closeTok = x.nextToken(); + String closeName = (closeTok instanceof String) ? (String) closeTok : ""; + if (!closeName.equals(tagName)) { + throw x.syntaxError("Mismatched close tag for " + tagName); + } + if (x.next() != '>') { + throw x.syntaxError("Misshaped close tag"); + } + if (textBuf != null && textBuf.length() > 0) { + jo.accumulate("content", textBuf.toString()); + } + return jo; + } + if (c == '?') { x.skipPast("?>"); continue; } + if (c == '!') { + if (x.more()) { + char c2 = x.next(); + if (c2 == '-' && x.more() && x.next() == '-') { + x.skipPast("-->"); + } else if (c2 == '[') { + x.skipPast("]]>"); + } else { + x.skipPast(">"); + } + } + continue; + } + // Child element + x.back(); + Object childNameTok = x.nextToken(); + if (!(childNameTok instanceof String)) { + throw x.syntaxError("Bad tag syntax"); + } + String childName = (String) childNameTok; + JSONObject childObj = parseElement(x, childName); + + // Merge child into current object (array‑if‑needed semantics) + Object existing = jo.opt(childName); + if (existing == null) { + jo.accumulate(childName, childObj.length() > 0 ? childObj : ""); + } else if (existing instanceof JSONArray) { + ((JSONArray) existing).put(childObj.length() > 0 ? childObj : ""); + } else { + JSONArray arr = new JSONArray(); + arr.put(existing); + arr.put(childObj.length() > 0 ? childObj : ""); + jo.put(childName, arr); + } + + // Flush buffered text, if any + if (textBuf != null && textBuf.length() > 0) { + jo.accumulate("content", textBuf.toString()); + textBuf.setLength(0); + } + } + } + } + /** SWE262P MileStone2 project, Task2 by Jiacheng Zhuo **/ /** Edit the parse method, add functions for the replacement implement **/ diff --git a/src/test/java/org/json/junit/milestone2/tests/XMLJsonPointerQueryingTest.java b/src/test/java/org/json/junit/milestone2/tests/XMLJsonPointerQueryingTest.java new file mode 100644 index 000000000..f50ffde4b --- /dev/null +++ b/src/test/java/org/json/junit/milestone2/tests/XMLJsonPointerQueryingTest.java @@ -0,0 +1,86 @@ +package org.json.junit.milestone2.tests; + +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONPointer; +import org.json.XML; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +public class XMLJsonPointerQueryingTest { + @Test + public void testXML() { + String xmlString = "\n"+ + "\n"+ + " Crista \n"+ + " Crista Lopes\n" + + "
\n" + + " Ave of Nowhere\n" + + " 92614\n" + + "
\n" + + "
"; + + try { + // notice that there cannot be a '/' at the end + JSONObject jobj = XML.toJSONObject(new StringReader(xmlString), new JSONPointer("/contact/address/street")); + System.out.println(jobj); + } catch (JSONException e) { + System.out.println(e); + } + } + + @Test + public void testXMLWithArray() { + String xml = + "" + + "XML Developer's Guide" + + "Midnight Rain" + + ""; + + try (Reader reader = new StringReader(xml)) { + JSONPointer ptr = new JSONPointer("/catalog/book/1"); + JSONObject node = XML.toJSONObject(reader, ptr); + System.out.println(node); + } catch (JSONException | IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testPointerWithResourceFile() throws Exception { + // test performance on large xml files + // this program is expected to finish early due to early appearance of requested path + try (Reader reader = new InputStreamReader( + getClass().getResourceAsStream("/xml_files/large_file.xml"))) { + + JSONPointer ptr = new JSONPointer("/mediawiki/siteinfo/namespaces"); + JSONObject node = XML.toJSONObject(reader, ptr); + System.out.println(node); + } + } + + @Test + public void testXMLWithNonexistentPath() { + String xmlString = "\n"+ + "\n"+ + " Crista \n"+ + " Crista Lopes\n" + + "
\n" + + " Ave of Nowhere\n" + + " 92614\n" + + "
\n" + + "
"; + + try { + // zipcode is not a sub-path of /contact/address/street/, thus should throw an error + JSONObject jobj = XML.toJSONObject(new StringReader(xmlString), new JSONPointer("/contact/address/street/zipcode")); + System.out.println(jobj); + } catch (JSONException e) { + System.out.println(e); + } + } +} diff --git a/src/test/java/org/json/junit/XMLPointerReplaceTest.java b/src/test/java/org/json/junit/milestone2/tests/XMLPointerReplaceTest.java similarity index 97% rename from src/test/java/org/json/junit/XMLPointerReplaceTest.java rename to src/test/java/org/json/junit/milestone2/tests/XMLPointerReplaceTest.java index 86346be6c..4a439022a 100644 --- a/src/test/java/org/json/junit/XMLPointerReplaceTest.java +++ b/src/test/java/org/json/junit/milestone2/tests/XMLPointerReplaceTest.java @@ -1,4 +1,4 @@ -package org.json.junit; +package org.json.junit.milestone2.tests; import org.json.JSONObject; import org.json.JSONPointer; From f369612eb3c9cd19e361778797c9de86362d3c4f Mon Sep 17 00:00:00 2001 From: wsw-stack <1770115879@qq.com> Date: Fri, 2 May 2025 20:01:08 -0700 Subject: [PATCH 03/12] add a standalone readme file for milestone 2 --- README-M2.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 README-M2.md diff --git a/README-M2.md b/README-M2.md new file mode 100644 index 000000000..6a0628e06 --- /dev/null +++ b/README-M2.md @@ -0,0 +1,16 @@ +# Milestone 2 + +For the Milestone 2 of SWE262P, 2 new functions were added: + +```java +static JSONObject toJSONObject(Reader reader, JSONPointer path) +static JSONObject toJSONObject(Reader reader, JSONPointer path, JSONObject replacement) +``` + +The first one takes in 2 parameters, a `Reader` object which contains some XML input and a `JSONPointer` object that includes a json path for querying, and returns a `JSONObject` object which has the corresponding path, or throw an error if that path does not exist. + +The first one takes in 3 parameters, a `Reader` object which contains some XML input and a `JSONPointer` object that includes a json path for querying, and a `JSONObject` for replacement. And returns a new `JSONObject` object which has the corresponding path replaced with the new given object, or throw an error if that path does not exist. + +Both new functions are placed in the `XML.java` file. + +The test cases of the functions are placed under the `org.json.junit.milestone2.tests` package, and to run the test case which deals with large file input, first create a folder named `xml_files` under the `/src/test/resources` path, and put the xml files for testing under this path. \ No newline at end of file From 4f14d9bd9f48b787d85354541756a06015aa96ac Mon Sep 17 00:00:00 2001 From: hai tong yan <1770115879@qq.com> Date: Tue, 13 May 2025 10:33:37 -0700 Subject: [PATCH 04/12] add custom prefix method, waiting for further improvement --- src/main/java/org/json/XML.java | 251 ++++++++++++++++++ .../tests/XMLKeyTransformerTest.java | 47 ++++ 2 files changed, 298 insertions(+) create mode 100644 src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index 2991622d4..031ccfc81 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -482,6 +482,237 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP } } } + + // overwritten method of parse which allows to pass a prefix tag + private static boolean parse(XMLTokener x, JSONObject context, String name, String prefix, XMLParserConfiguration config, int currentNestingDepth) + throws JSONException { + char c; + int i; + JSONObject jsonObject = null; + String string; + String tagName; + Object token; + XMLXsiTypeConverter xmlXsiTypeConverter; + + // Test for and skip past these forms: + // + // + // + // + // Report errors for these forms: + // <> + // <= + // << + token = x.nextToken(); + + // "); + return false; + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if ("CDATA".equals(token)) { + if (x.next() == '[') { + string = x.nextCDATA(); + if (string.length() > 0) { + context.accumulate(config.getcDataTagName(), string); + } + return false; + } + } + throw x.syntaxError("Expected 'CDATA['"); + } + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after ' 0); + return false; + } else if (token == QUEST) { + + // "); + return false; + } else if (token == SLASH) { + + // Close tag + if (x.nextToken() != GT) { + throw x.syntaxError("Misshaped tag"); + } + if (config.getForceList().contains(prefix + tagName)) { + // Force the value to be an array + if (nilAttributeFound) { + context.append(prefix + tagName, JSONObject.NULL); + } else if (jsonObject.length() > 0) { + context.append(prefix + tagName, jsonObject); + } else { + context.put(prefix + tagName, new JSONArray()); + } + } else { + if (nilAttributeFound) { + context.accumulate(prefix + tagName, JSONObject.NULL); + } else if (jsonObject.length() > 0) { + context.accumulate(prefix + tagName, jsonObject); + } else { + context.accumulate(prefix + tagName, ""); + } + } + return false; + + } else if (token == GT) { + // Content, between <...> and + for (;;) { + token = x.nextContent(); + if (token == null) { + if (tagName != null) { + throw x.syntaxError("Unclosed tag " + tagName); + } + return false; + } else if (token instanceof String) { + string = (String) token; + if (string.length() > 0) { + if(xmlXsiTypeConverter != null) { + jsonObject.accumulate(config.getcDataTagName(), + stringToValue(string, xmlXsiTypeConverter)); + } else { + Object obj = stringToValue((String) token); + if (obj instanceof Boolean) { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepBooleanAsString() + ? ((String) token) + : obj); + } else if (obj instanceof Number) { + jsonObject.accumulate(config.getcDataTagName(), + config.isKeepNumberAsString() + ? ((String) token) + : obj); + } else { + jsonObject.accumulate(config.getcDataTagName(), stringToValue((String) token)); + } + } + } + + } else if (token == LT) { + // Nested element + if (currentNestingDepth == config.getMaxNestingDepth()) { + throw x.syntaxError("Maximum nesting depth of " + config.getMaxNestingDepth() + " reached"); + } + + if (parse(x, jsonObject, tagName, prefix, config, currentNestingDepth + 1)) { + if (config.getForceList().contains(tagName)) { + // Force the value to be an array + if (jsonObject.length() == 0) { + context.put(tagName, new JSONArray()); + } else if (jsonObject.length() == 1 + && jsonObject.opt(config.getcDataTagName()) != null) { + context.append(prefix + tagName, jsonObject.opt(config.getcDataTagName())); + } else { + context.append(prefix + tagName, jsonObject); + } + } else { + if (jsonObject.length() == 0) { + context.accumulate(prefix + tagName, ""); + } else if (jsonObject.length() == 1 + && jsonObject.opt(config.getcDataTagName()) != null) { + context.accumulate(prefix + tagName, jsonObject.opt(config.getcDataTagName())); + } else { + if (!config.shouldTrimWhiteSpace()) { + removeEmpty(jsonObject, config); + } + context.accumulate(prefix + tagName, jsonObject); + } + } + + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } /** * This method removes any JSON entry which has the key set by XMLParserConfiguration.cDataTagName * and contains whitespace as this is caused by whitespace between tags. See test XMLTest.testNestedWithWhitespaceTrimmingDisabled. @@ -955,6 +1186,26 @@ public static JSONObject toJSONObject(Reader reader, JSONPointer path) throws JS return result; } + /** + * Given a customized function, convert the keys in the Json Object + * @param reader + * @param prefix + * @return + * @throws JSONException + */ + public static JSONObject toJSONObject(Reader reader, String prefix) throws JSONException { + JSONObject jo = new JSONObject(); + XMLParserConfiguration config = XMLParserConfiguration.ORIGINAL; + XMLTokener x = new XMLTokener(reader, config); + while (x.more()) { + x.skipPast("<"); + if(x.more()) { + parse(x, jo, null, prefix, config, 0); + } + } + return jo; + } + /** * Helper method: skip the current element and its entire subtree without * building any JSON output. diff --git a/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java b/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java new file mode 100644 index 000000000..abe0d8470 --- /dev/null +++ b/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java @@ -0,0 +1,47 @@ +package org.json.junit.milestone3.tests; + +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONPointer; +import org.json.XML; +import org.junit.Test; + +import java.io.StringReader; + +import static org.junit.Assert.assertEquals; + +public class XMLKeyTransformerTest { + // define some customized functions for testing + + @Test + public void testXML01() { + String xml = "<content>Old Title</content>John"; + StringReader reader = new StringReader(xml); + + JSONObject result = XML.toJSONObject(reader, "swe_262p"); + + System.out.println(result); + } + + @Test + public void testXML02() { + String xmlString = "\n"+ + "\n"+ + " Crista \n"+ + " Crista Lopes\n" + + "
\n" + + " Ave of Nowhere\n" + + " 92614\n" + + "
\n" + + "
"; + + try { + JSONObject jobj = XML.toJSONObject(new StringReader(xmlString), "swe_262p"); + System.out.println(jobj); + } catch (JSONException e) { + System.out.println(e); + } + } +} + + From cd7491cf2d3de0ea373128cb1a646fc1a361ca34 Mon Sep 17 00:00:00 2001 From: jiachengzhuo <“richardjcheuk@gmail.com”> Date: Tue, 13 May 2025 23:29:01 -0700 Subject: [PATCH 05/12] Implement Milestone 3: add keyTransformer support --- src/main/java/org/json/XML.java | 100 ++++++++++++++++-- .../tests/XMLKeyTransformerTest.java | 30 +++++- 2 files changed, 119 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index 031ccfc81..ea98d90cc 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -10,7 +10,7 @@ import java.math.BigInteger; import java.util.*; import java.io.BufferedReader; -import java.io.Reader; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -484,7 +484,10 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP } // overwritten method of parse which allows to pass a prefix tag - private static boolean parse(XMLTokener x, JSONObject context, String name, String prefix, XMLParserConfiguration config, int currentNestingDepth) + /* Milestone3 + overwritten new method of parse + */ + private static boolean parseMilestone3(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config, int currentNestingDepth,Function keyTransformer) throws JSONException { char c; int i; @@ -567,6 +570,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, Stri } else { tagName = (String) token; + String transformedTagName = keyTransformer.apply(tagName);//add token = null; jsonObject = new JSONObject(); boolean nilAttributeFound = false; @@ -584,7 +588,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, Stri if (!(token instanceof String)) { throw x.syntaxError("Missing value"); } - + String transformedKey = keyTransformer.apply(string); //add new code if (config.isConvertNilAttributeToNull() && NULL_ATTR.equals(string) && Boolean.parseBoolean((String) token)) { @@ -593,6 +597,14 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, Stri && TYPE_ATTR.equals(string)) { xmlXsiTypeConverter = config.getXsiTypeMap().get(token); } else if (!nilAttributeFound) { + Object obj = stringToValue((String) token); + jsonObject.accumulate(transformedKey, obj); + } + token = null; + } else { + jsonObject.accumulate(keyTransformer.apply(string), ""); + } + /*} else if (!nilAttributeFound) { Object obj = stringToValue((String) token); if (obj instanceof Boolean) { jsonObject.accumulate(prefix + string, @@ -612,6 +624,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, Stri } else { jsonObject.accumulate(prefix + string, ""); } + */ } else if (token == SLASH) { @@ -619,6 +632,25 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, Stri if (x.nextToken() != GT) { throw x.syntaxError("Misshaped tag"); } + if (config.getForceList().contains(tagName)) { + if (nilAttributeFound) { + context.append(transformedTagName, JSONObject.NULL); + } else if (jsonObject.length() > 0) { + context.append(transformedTagName, jsonObject); + } else { + context.put(transformedTagName, new JSONArray()); + } + } else { + if (nilAttributeFound) { + context.accumulate(transformedTagName, JSONObject.NULL); + } else if (jsonObject.length() > 0) { + context.accumulate(transformedTagName, jsonObject); + } else { + context.accumulate(transformedTagName, ""); + } + } + return false; + /* if (config.getForceList().contains(prefix + tagName)) { // Force the value to be an array if (nilAttributeFound) { @@ -638,7 +670,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, Stri } } return false; - + */ } else if (token == GT) { // Content, between <...> and for (;;) { @@ -673,7 +705,41 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, Stri } } else if (token == LT) { + if (parseMilestone3(x, jsonObject,tagName, config, currentNestingDepth + 1, keyTransformer)) { + if (config.getForceList().contains(tagName)) { + if (jsonObject.length() == 0) { + context.put(transformedTagName, new JSONArray()); + } else if (jsonObject.length() == 1 + && jsonObject.opt(config.getcDataTagName()) != null) { + context.append(transformedTagName, jsonObject.opt(config.getcDataTagName())); + } else { + context.append(transformedTagName, jsonObject); + } + } else { + if (jsonObject.length() == 0) { + context.accumulate(transformedTagName, ""); + } else if (jsonObject.length() == 1 + && jsonObject.opt(config.getcDataTagName()) != null) { + context.accumulate(transformedTagName, jsonObject.opt(config.getcDataTagName())); + } else { + context.accumulate(transformedTagName, jsonObject); + } + } + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } + } + + // Nested element + /* if (currentNestingDepth == config.getMaxNestingDepth()) { throw x.syntaxError("Maximum nesting depth of " + config.getMaxNestingDepth() + " reached"); } @@ -713,6 +779,8 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, Stri } } } + + */ /** * This method removes any JSON entry which has the key set by XMLParserConfiguration.cDataTagName * and contains whitespace as this is caused by whitespace between tags. See test XMLTest.testNestedWithWhitespaceTrimmingDisabled. @@ -1188,11 +1256,24 @@ public static JSONObject toJSONObject(Reader reader, JSONPointer path) throws JS /** * Given a customized function, convert the keys in the Json Object - * @param reader - * @param prefix - * @return - * @throws JSONException + * @param reader the XML input + * @param keyTransformer a function that transforms each key name + * @return JSONObject with transformed keys + * @throws JSONException if any XML parsing or transformation fails */ + public static JSONObject toJSONObject(Reader reader, Function keyTransformer) throws JSONException { + JSONObject result = new JSONObject(); + XMLTokener x = new XMLTokener(reader); + + while (x.more()) { + x.skipPast("<"); + if (x.more()) { + XML.parseMilestone3(x, result, null, XMLParserConfiguration.ORIGINAL, 0, keyTransformer); + } + } + return result; + } + /* public static JSONObject toJSONObject(Reader reader, String prefix) throws JSONException { JSONObject jo = new JSONObject(); XMLParserConfiguration config = XMLParserConfiguration.ORIGINAL; @@ -1205,7 +1286,7 @@ public static JSONObject toJSONObject(Reader reader, String prefix) throws JSONE } return jo; } - +*/ /** * Helper method: skip the current element and its entire subtree without * building any JSON output. @@ -1868,7 +1949,6 @@ public static JSONObject toJSONObject(Reader reader, JSONPointer path, JSONObjec } } - /** * Convert a well-formed (but not necessarily valid) XML string into a * JSONObject. Some information may be lost in this transformation because diff --git a/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java b/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java index abe0d8470..2546cab69 100644 --- a/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java +++ b/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java @@ -7,12 +7,40 @@ import org.junit.Test; import java.io.StringReader; +import java.util.function.Function; import static org.junit.Assert.assertEquals; public class XMLKeyTransformerTest { // define some customized functions for testing + @Test + public void keyTransformerSimpleTest() { + String xml = "TitleJohn"; + Function prefixer = key -> "swe262_" + key; + + JSONObject result = XML.toJSONObject(new StringReader(xml), prefixer); + + assertEquals("Title", result.getJSONObject("swe262_book").get("swe262_title")); + assertEquals("John", result.getJSONObject("swe262_book").get("swe262_author")); + } + @Test + public void keyTransformerReverseTest() { + String xml = "value"; + Function reverser = key -> new StringBuilder(key).reverse().toString(); + + JSONObject result = XML.toJSONObject(new StringReader(xml), reverser); + + assertEquals("value", result.getJSONObject("atad").get("meti")); + } + + @Test(expected = NullPointerException.class) + public void nullReaderTest() { + XML.toJSONObject(null, key -> "x_" + key); + } +} + +/* @Test public void testXML01() { String xml = "<content>Old Title</content>John"; @@ -43,5 +71,5 @@ public void testXML02() { } } } - +*/ From 42e7ca91983018c9519a623a67a3a25c334206fa Mon Sep 17 00:00:00 2001 From: hai tong yan <1770115879@qq.com> Date: Wed, 14 May 2025 11:41:27 -0700 Subject: [PATCH 06/12] fix bugs and document readme --- README-M3.md | 19 +++++ src/main/java/org/json/XML.java | 83 +++---------------- .../tests/XMLKeyTransformerTest.java | 36 +------- 3 files changed, 31 insertions(+), 107 deletions(-) create mode 100644 README-M3.md diff --git a/README-M3.md b/README-M3.md new file mode 100644 index 000000000..ebc3876d4 --- /dev/null +++ b/README-M3.md @@ -0,0 +1,19 @@ +# Milestone 3 + +For the Milestone 3 of SWE262P, the following new functions was added: + +```java +static JSONObject toJSONObject(Reader reader, Function func) +``` + +This function takes in 3 parameters, a `Reader` object which contains some XML input and a `Function` object that includes a function for converting a `String` (expected type of input: a `String`, and is expected to return another `String`), and a `JSONObject` for replacement. And returns a new `JSONObject` object with tag names replaced, or throw an error if the `Reader` object gives an invalid XML. + +The new function is placed in the `XML.java` file. + +The test cases of the functions are placed under the `org.json.junit.milestone3.tests` package, and to run the test case, run the following command: + +`mvn -Dtest=XMLKeyTransformerTest test` + +By implementing the code in the original library code, the function is able to complete the task in one-pass, as the function is writing to a new `JSONObject` while parsing the input XML String. However, in milestone 1, the client code is only able to convert the whole XML String to a `JSONObject`, then convert the keys of this `JSONObject` object. + +Thus, implementing this function inside the library code is able to reduce the execution time in half (one-pass vs two-pass), and also resulting in optimization of memory usage. \ No newline at end of file diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index ea98d90cc..a02ccf15d 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -483,9 +483,17 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP } } - // overwritten method of parse which allows to pass a prefix tag - /* Milestone3 - overwritten new method of parse + /** + * Compared to the original parse function, this function adds the function (String Convertor) as an input + * @param x + * @param context + * @param name + * @param config + * @param currentNestingDepth + * @param keyTransformer + * The function which takes in a single String parameter, and returns another converted String + * @return + * @throws JSONException */ private static boolean parseMilestone3(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config, int currentNestingDepth,Function keyTransformer) throws JSONException { @@ -604,29 +612,6 @@ private static boolean parseMilestone3(XMLTokener x, JSONObject context, String } else { jsonObject.accumulate(keyTransformer.apply(string), ""); } - /*} else if (!nilAttributeFound) { - Object obj = stringToValue((String) token); - if (obj instanceof Boolean) { - jsonObject.accumulate(prefix + string, - config.isKeepBooleanAsString() - ? ((String) token) - : obj); - } else if (obj instanceof Number) { - jsonObject.accumulate(prefix + string, - config.isKeepNumberAsString() - ? ((String) token) - : obj); - } else { - jsonObject.accumulate(prefix + string, stringToValue((String) token)); - } - } - token = null; - } else { - jsonObject.accumulate(prefix + string, ""); - } - */ - - } else if (token == SLASH) { // Empty tag <.../> if (x.nextToken() != GT) { @@ -735,52 +720,6 @@ private static boolean parseMilestone3(XMLTokener x, JSONObject context, String } } } - } - - - // Nested element - /* - if (currentNestingDepth == config.getMaxNestingDepth()) { - throw x.syntaxError("Maximum nesting depth of " + config.getMaxNestingDepth() + " reached"); - } - - if (parse(x, jsonObject, tagName, prefix, config, currentNestingDepth + 1)) { - if (config.getForceList().contains(tagName)) { - // Force the value to be an array - if (jsonObject.length() == 0) { - context.put(tagName, new JSONArray()); - } else if (jsonObject.length() == 1 - && jsonObject.opt(config.getcDataTagName()) != null) { - context.append(prefix + tagName, jsonObject.opt(config.getcDataTagName())); - } else { - context.append(prefix + tagName, jsonObject); - } - } else { - if (jsonObject.length() == 0) { - context.accumulate(prefix + tagName, ""); - } else if (jsonObject.length() == 1 - && jsonObject.opt(config.getcDataTagName()) != null) { - context.accumulate(prefix + tagName, jsonObject.opt(config.getcDataTagName())); - } else { - if (!config.shouldTrimWhiteSpace()) { - removeEmpty(jsonObject, config); - } - context.accumulate(prefix + tagName, jsonObject); - } - } - - return false; - } - } - } - } else { - throw x.syntaxError("Misshaped tag"); - } - } - } - } - - */ /** * This method removes any JSON entry which has the key set by XMLParserConfiguration.cDataTagName * and contains whitespace as this is caused by whitespace between tags. See test XMLTest.testNestedWithWhitespaceTrimmingDisabled. diff --git a/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java b/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java index 2546cab69..d96f9b591 100644 --- a/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java +++ b/src/test/java/org/json/junit/milestone3/tests/XMLKeyTransformerTest.java @@ -14,7 +14,7 @@ public class XMLKeyTransformerTest { // define some customized functions for testing @Test - public void keyTransformerSimpleTest() { + public void keyTransformerAddPrefixTest() { String xml = "TitleJohn"; Function prefixer = key -> "swe262_" + key; @@ -39,37 +39,3 @@ public void nullReaderTest() { XML.toJSONObject(null, key -> "x_" + key); } } - -/* - @Test - public void testXML01() { - String xml = "<content>Old Title</content>John"; - StringReader reader = new StringReader(xml); - - JSONObject result = XML.toJSONObject(reader, "swe_262p"); - - System.out.println(result); - } - - @Test - public void testXML02() { - String xmlString = "\n"+ - "\n"+ - " Crista \n"+ - " Crista Lopes\n" + - "
\n" + - " Ave of Nowhere\n" + - " 92614\n" + - "
\n" + - "
"; - - try { - JSONObject jobj = XML.toJSONObject(new StringReader(xmlString), "swe_262p"); - System.out.println(jobj); - } catch (JSONException e) { - System.out.println(e); - } - } -} -*/ - From 7e5865ef293b876a0bc5508dbec546a80ee596ec Mon Sep 17 00:00:00 2001 From: jiachengzhuo <“richardjcheuk@gmail.com”> Date: Sat, 24 May 2025 20:08:04 -0700 Subject: [PATCH 07/12] Implement Milestone 4: toStream() with JSONNode support --- src/main/java/org/json/JSONObject.java | 62 ++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index a1664f708..2bce36024 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -17,6 +17,8 @@ import java.util.*; import java.util.Map.Entry; import java.util.regex.Pattern; +import java.util.stream.IntStream; +import java.util.stream.Stream; /** * A JSONObject is an unordered collection of name/value pairs. Its external @@ -3030,4 +3032,64 @@ private static String removeLeadingZerosOfNumber(String value){ if (negativeFirstChar) {return "-0";} return "0"; } + + // ---------------------------- Milestone 4 ------------------------------ + + /** + * Represents a node in the JSON object tree, with path and value. + */ + public static class JSONNode { + private final String path; + private final Object value; + + public JSONNode(String path, Object value) { + this.path = path; + this.value = value; + } + + public String getPath() { + return path; + } + + public Object getValue() { + return value; + } + + @Override + public String toString() { + return "JSONNode{path='" + path + "', value=" + value + '}'; + } + } + + /** + * Convert this JSONObject into a stream of JSONNode, allowing chained operations. + * @return Stream of JSONNode objects (each has a full path and a value) + */ + public Stream toStream() { + return toStream("", this); + } + + /** + * Recursive helper method to flatten JSONObject/JSONArray into JSONNode stream. + */ + private Stream toStream(String path, Object value) { + if (value instanceof JSONObject) { + JSONObject obj = (JSONObject) value; + return obj.keySet().stream() + .flatMap(key -> { + String newPath = path.isEmpty() ? "/" + key : path + "/" + key; + return toStream(newPath, obj.get(key)); + }); + } else if (value instanceof JSONArray) { + JSONArray array = (JSONArray) value; + return IntStream.range(0, array.length()) + .boxed() + .flatMap(i -> { + String newPath = path + "[" + i + "]"; + return toStream(newPath, array.get(i)); + }); + } else { + return Stream.of(new JSONNode(path, value)); + } + } } From b5e989b3e66b2310924bdb579c30f09963b61d9f Mon Sep 17 00:00:00 2001 From: hai tong yan <1770115879@qq.com> Date: Sun, 25 May 2025 17:21:52 -0700 Subject: [PATCH 08/12] add test cases and readme file --- README-M4.md | 18 +++++ .../tests/JSONObjectStreamTest.java | 74 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 README-M4.md create mode 100644 src/test/java/org/json/junit/milestone4/tests/JSONObjectStreamTest.java diff --git a/README-M4.md b/README-M4.md new file mode 100644 index 000000000..a5cdb8b32 --- /dev/null +++ b/README-M4.md @@ -0,0 +1,18 @@ +# Milestone 4 – Stream support for **JSONObject** + +## What is Added + +| Item | Description | +|------|-------------| +| `JSONObject.JSONNode` | Immutable leaf wrapper containing an absolute `path` and its `value`. | +| `Stream JSONObject.toStream()` | Depth-first, **lazy** flattening of any `JSONObject/JSONArray` into a `Stream`. Only leaf nodes are emitted (low memory footprint). | + +Path conventions +* Object keys: `/parent/child` +* Array items: `/array[0]/child` + +--- + +## Run the test class +```bash +mvn -Dtest=org.json.junit.milestone4.tests.JSONObjectStreamTest test \ No newline at end of file diff --git a/src/test/java/org/json/junit/milestone4/tests/JSONObjectStreamTest.java b/src/test/java/org/json/junit/milestone4/tests/JSONObjectStreamTest.java new file mode 100644 index 000000000..7957fed9f --- /dev/null +++ b/src/test/java/org/json/junit/milestone4/tests/JSONObjectStreamTest.java @@ -0,0 +1,74 @@ +package org.json.junit.milestone4.tests; + +import org.json.JSONObject; +import org.json.JSONObject.JSONNode; +import org.json.XML; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; +import java.util.Scanner; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +/** + * Unit tests for the JSONObject.toStream() extension (Milestone 4) – JUnit 4 / Java 8. + */ +public class JSONObjectStreamTest { + + private static JSONObject catalog; + + @BeforeClass + public static void loadXml() throws Exception { + try (InputStream in = JSONObjectStreamTest.class.getResourceAsStream("/books.xml")) { + assertNotNull("books.xml must be on the class-path (src/test/resources)", in); + + String xml; + try (Scanner scanner = new Scanner(in, StandardCharsets.UTF_8.name())) { + scanner.useDelimiter("\\A"); + xml = scanner.hasNext() ? scanner.next() : ""; + } + + catalog = XML.toJSONObject(xml); + } + } + + @Test + public void extractTitles() { + List titles = catalog.toStream() + .filter(n -> n.getPath().endsWith("/title")) + .map(n -> n.getValue().toString()) + .collect(Collectors.toList()); + + assertEquals(12, titles.size()); + assertTrue(titles.contains("XML Developer's Guide")); + assertTrue(titles.contains("Visual Studio 7: A Comprehensive Guide")); + } + + @Test + public void findExactPath() { + String wantedPath = "/catalog/book[0]/author"; + Optional node = catalog.toStream() + .filter(n -> n.getPath().equals(wantedPath)) + .findFirst(); + + assertTrue(node.isPresent()); + assertEquals("Gambardella, Matthew", node.get().getValue()); + } + + @Test + public void filterCheapBooks() { + List cheapPrices = catalog.toStream() + .filter(n -> n.getPath().endsWith("/price")) + .filter(n -> Double.parseDouble(n.getValue().toString()) < 10.0) + .collect(Collectors.toList()); + + assertEquals(8, cheapPrices.size()); + assertTrue(cheapPrices.stream() + .allMatch(n -> Double.parseDouble(n.getValue().toString()) < 10.0)); + } +} From a0310057058911e55de68e47fb0b971132f2a492 Mon Sep 17 00:00:00 2001 From: hai tong yan <1770115879@qq.com> Date: Sun, 25 May 2025 17:36:13 -0700 Subject: [PATCH 09/12] add the sample xml file to pass CICD tests --- src/test/resources/books.xml | 120 +++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/test/resources/books.xml diff --git a/src/test/resources/books.xml b/src/test/resources/books.xml new file mode 100644 index 000000000..06889bd6c --- /dev/null +++ b/src/test/resources/books.xml @@ -0,0 +1,120 @@ + + + + Gambardella, Matthew + XML Developer's Guide + Computer + 44.95 + 2000-10-01 + An in-depth look at creating applications + with XML. + + + Ralls, Kim + Midnight Rain + Fantasy + 5.95 + 2000-12-16 + A former architect battles corporate zombies, + an evil sorceress, and her own childhood to become queen + of the world. + + + Corets, Eva + Maeve Ascendant + Fantasy + 5.95 + 2000-11-17 + After the collapse of a nanotechnology + society in England, the young survivors lay the + foundation for a new society. + + + Corets, Eva + Oberon's Legacy + Fantasy + 5.95 + 2001-03-10 + In post-apocalypse England, the mysterious + agent known only as Oberon helps to create a new life + for the inhabitants of London. Sequel to Maeve + Ascendant. + + + Corets, Eva + The Sundered Grail + Fantasy + 5.95 + 2001-09-10 + The two daughters of Maeve, half-sisters, + battle one another for control of England. Sequel to + Oberon's Legacy. + + + Randall, Cynthia + Lover Birds + Romance + 4.95 + 2000-09-02 + When Carla meets Paul at an ornithology + conference, tempers fly as feathers get ruffled. + + + Thurman, Paula + Splish Splash + Romance + 4.95 + 2000-11-02 + A deep sea diver finds true love twenty + thousand leagues beneath the sea. + + + Knorr, Stefan + Creepy Crawlies + Horror + 4.95 + 2000-12-06 + An anthology of horror stories about roaches, + centipedes, scorpions and other insects. + + + Kress, Peter + Paradox Lost + Science Fiction + 6.95 + 2000-11-02 + After an inadvertant trip through a Heisenberg + Uncertainty Device, James Salway discovers the problems + of being quantum. + + + O'Brien, Tim + Microsoft .NET: The Programming Bible + Computer + 36.95 + 2000-12-09 + Microsoft's .NET initiative is explored in + detail in this deep programmer's reference. + + + O'Brien, Tim + MSXML3: A Comprehensive Guide + Computer + 36.95 + 2000-12-01 + The Microsoft MSXML3 parser is covered in + detail, with attention to XML DOM interfaces, XSLT processing, + SAX and more. + + + Galos, Mike + Visual Studio 7: A Comprehensive Guide + Computer + 49.95 + 2001-04-16 + Microsoft Visual Studio 7 is explored in depth, + looking at how Visual Basic, Visual C++, C#, and ASP+ are + integrated into a comprehensive development + environment. + + \ No newline at end of file From 81f159d9045effe1f23de689538702fcfdbba120 Mon Sep 17 00:00:00 2001 From: wsw-stack <67959337+wsw-stack@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:25:58 -0700 Subject: [PATCH 10/12] add milestone5 class --- src/main/java/org/json/XML.java | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index a02ccf15d..6e100b8f6 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -10,6 +10,8 @@ import java.math.BigInteger; import java.util.*; import java.io.BufferedReader; +import java.util.concurrent.*; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -2237,4 +2239,158 @@ private static final String indent(int indent) { } return sb.toString(); } + +// public static class asyncRunner { +// private List> tasks; +// private boolean running; +// +// public asyncRunner() { +// this.tasks = new ArrayList<>(); +// running = false; +// } +// +// public void add(Future task) { +// this.tasks.add(task); +// } +// +// +// public void run() { +// running = true; +// while(running) { +// List> nextTasks = new ArrayList<>(); +// for(Future task: tasks) { +// if(!task.isDone()) { +// nextTasks.add(task); +// } +// } +// running = !nextTasks.isEmpty(); +// tasks = nextTasks; +// } +// } +// } +// +// public static Future toJSONObject(Reader reader, Consumer after, Consumer error){ +// +// ExecutorService executor = Executors.newSingleThreadExecutor(); +// futureTask task = new futureTask(reader, after, error); +// Future future = executor.submit(task); +// +// return future; +// } +// +// private static class futureTask implements Callable { +// +// Reader reader; +// Consumer after; +// Consumer error; +// public futureTask(Reader reader, Consumer after, Consumer error) { +// this.reader = reader; +// this.after = after; +// this.error = error; +// } +// +// @Override +// public JSONObject call() throws Exception { +// JSONObject jo = new JSONObject(); +// try { +// XMLTokener x = new XMLTokener(reader); +// while (x.more()) { +// x.skipPast("<"); +// if (x.more()) { +// parse(x, jo, null, XMLParserConfiguration.ORIGINAL, 0); +// } +// } +// after.accept(jo); +// } catch (Exception e) { +// error.accept(e); +// } +// return jo; +// } +// } +private static final ExecutorService executor = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors() +); + + public static Future toJSONObject( + Reader reader, + Consumer after, + Consumer error + ) { + FutureTask task = new FutureTask<>(new FutureTaskCallable(reader, after, error)); + executor.execute(task); + return task; + } + + public static void shutdownExecutor() { + executor.shutdown(); + } + + private static class FutureTaskCallable implements Callable { + private final Reader reader; + private final Consumer after; + private final Consumer error; + + public FutureTaskCallable( + Reader reader, + Consumer after, + Consumer error + ) { + this.reader = reader; + this.after = after; + this.error = error; + } + + @Override + public JSONObject call() throws Exception { + JSONObject jo = new JSONObject(); + try { + XMLTokener x = new XMLTokener(reader); + while (x.more()) { + x.skipPast("<"); + if (x.more()) { + parse(x, jo, null, XMLParserConfiguration.ORIGINAL, 0); + } + } + after.accept(jo); + return jo; + } catch (Exception e) { + error.accept(e); + throw e; + } + } + } + + public static class AsyncRunner { + private List> tasks = new ArrayList<>(); + + public AsyncRunner() { + } + + public void add(Future task) { + this.tasks.add(task); + } + + public void run() { + boolean running = true; + while (running) { + List> nextTasks = new ArrayList<>(); + for (Future task : tasks) { + if (!task.isDone()) { + nextTasks.add(task); + } + } + if (nextTasks.isEmpty()) { + running = false; + } else { + tasks = nextTasks; + try { + Thread.sleep(50); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + running = false; + } + } + } + } + } } From 08cf24389da2e55d58d747a43f2d06545732b34e Mon Sep 17 00:00:00 2001 From: jiachengzhuo <“richardjcheuk@gmail.com”> Date: Tue, 3 Jun 2025 00:44:55 -0700 Subject: [PATCH 11/12] Add async XML parsing tests for Milestone 5 --- .../junit/milestone5/JSONObjectAsyncTest.java | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/test/java/org/json/junit/milestone5/JSONObjectAsyncTest.java diff --git a/src/test/java/org/json/junit/milestone5/JSONObjectAsyncTest.java b/src/test/java/org/json/junit/milestone5/JSONObjectAsyncTest.java new file mode 100644 index 000000000..89df578f8 --- /dev/null +++ b/src/test/java/org/json/junit/milestone5/JSONObjectAsyncTest.java @@ -0,0 +1,157 @@ +package org.json.junit.milestone5; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.XML; +import org.junit.Test; + +import java.io.Reader; +import java.io.StringReader; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +public class JSONObjectAsyncTest { + + private final XML.AsyncRunner runner = new XML.AsyncRunner(); + + public JSONObjectAsyncTest() { + // Default constructor + } + + private final Reader medReader = new StringReader( + "" + + " " + + " Gambardella, Matthew" + + " XML Developer's Guide" + + " Computer" + + " 44.95" + + " 2000-10-01" + + " An in-depth look at creating applications with XML." + + " " + + " " + + " Ralls, Kim" + + " Midnight Rain" + + " Fantasy" + + " 5.95" + + " 2000-12-16" + + " A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world." + + " " + + "" + ); + + private final Reader smallReader = new StringReader( + "" + + " Hello" + + "" + ); + + @Test + public void testAsyncParsing() throws Exception { + CountDownLatch latch = new CountDownLatch(2); + JSONObject[] results = new JSONObject[2]; + Exception[] errors = new Exception[2]; + long[] timeElapsed = new long[2]; + + final long startTime = System.nanoTime(); + + Future task1 = XML.toJSONObject( + medReader, + jo -> { + results[0] = jo; + long endNano = System.nanoTime(); + timeElapsed[0] = TimeUnit.NANOSECONDS.toMillis(endNano - startTime); + latch.countDown(); + }, + e -> { + errors[0] = e; + latch.countDown(); + } + ); + + Future task2 = XML.toJSONObject( + smallReader, + jo -> { + results[1] = jo; + long endNano = System.nanoTime(); + timeElapsed[1] = TimeUnit.NANOSECONDS.toMillis(endNano - startTime); + latch.countDown(); + }, + e -> { + errors[1] = e; + latch.countDown(); + } + ); + + boolean completed = latch.await(10, TimeUnit.SECONDS); + assertTrue("Both callbacks should complete within 10 seconds", completed); + + assertNull("No error expected for medReader", errors[0]); + assertNull("No error expected for smallReader", errors[1]); + + JSONObject expectedMed = new JSONObject() + .put("catalog", new JSONObject() + .put("book", new JSONArray() + .put(new JSONObject() + .put("author", "Gambardella, Matthew") + .put("title", "XML Developer's Guide") + .put("genre", "Computer") + .put("price", 44.95) + .put("publish_date", "2000-10-01") + .put("description", "An in-depth look at creating applications with XML.") + .put("id", "bk101") + ) + .put(new JSONObject() + .put("author", "Ralls, Kim") + .put("title", "Midnight Rain") + .put("genre", "Fantasy") + .put("price", 5.95) + .put("publish_date", "2000-12-16") + .put("description", "A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world.") + .put("id", "bk102") + ) + ) + ); + + JSONObject expectedSmall = new JSONObject() + .put("root", new JSONObject() + .put("message", "Hello") + ); + + assertTrue("medReader JSON should match expected", expectedMed.similar(results[0])); + assertTrue("smallReader JSON should match expected", expectedSmall.similar(results[1])); + + assertTrue("smallReader must be faster than medReader", timeElapsed[1] < timeElapsed[0]); + + // Optional debug output + System.out.println("medReader elapsed: " + timeElapsed[0] + " ms"); + System.out.println("smallReader elapsed: " + timeElapsed[1] + " ms"); + } + + @Test + public void testAsyncParsingWithInvalidXML() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + Exception[] errors = new Exception[1]; + + Reader invalidReader = new StringReader( + "Unclosed tag" + ); + + Future task = org.json.XML.toJSONObject( + invalidReader, + jo -> fail("Should not succeed with invalid XML"), + e -> { + errors[0] = e; + latch.countDown(); + } + ); + + boolean completed = latch.await(5, TimeUnit.SECONDS); + assertTrue("Callback should complete within 5 seconds", completed); + assertNotNull("Error should be captured for invalid XML", errors[0]); + assertTrue("Error should be a JSONException", errors[0] instanceof org.json.JSONException); + } + +} From 6d6f21a2184640d886affa389a490df877831c7a Mon Sep 17 00:00:00 2001 From: hai tong yan <1770115879@qq.com> Date: Tue, 3 Jun 2025 21:49:35 -0700 Subject: [PATCH 12/12] finalize milestone5 edit and readme --- README-M5.md | 17 ++++++ src/main/java/org/json/XML.java | 100 ++------------------------------ 2 files changed, 21 insertions(+), 96 deletions(-) create mode 100644 README-M5.md diff --git a/README-M5.md b/README-M5.md new file mode 100644 index 000000000..c069036f0 --- /dev/null +++ b/README-M5.md @@ -0,0 +1,17 @@ +# Milestone 5 – Async support for **JSONObject** + +## What is Added + +| Item | Description | +|------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Future XMLUtils.toJSONObject(Reader reader, Consumer after, Consumer error)` | **Non-blocking** conversion. Parses the XML read from `reader` into a `JSONObject` on a background thread. When parsing finishes it invokes `after.accept(result)`; when failure it calls `error.accept(ex)` and throws the exception into the returned `Future`. | +| `AsyncRunner` | Tiny task aggregator. Call `add(Future task)` to collect jobs, then wait for them all (e.g. `forEach(Future::get)`). | +| `ExecutorService` | The default thread pool (size = available CPU cores). If you prefer a custom pool you can swap it out before calling the API (e.g. add `XMLUtils.setExecutor(...)`). | + +Input: XML of different sizes (where they will be parsed concurrently) + +--- + +## Run the test class +```bash +mvn -Dtest=org.json.junit.milestone5.tests.JSONObjectAsyncTest test \ No newline at end of file diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index 6e100b8f6..53ab77a62 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -2240,76 +2240,10 @@ private static final String indent(int indent) { return sb.toString(); } -// public static class asyncRunner { -// private List> tasks; -// private boolean running; -// -// public asyncRunner() { -// this.tasks = new ArrayList<>(); -// running = false; -// } -// -// public void add(Future task) { -// this.tasks.add(task); -// } -// -// -// public void run() { -// running = true; -// while(running) { -// List> nextTasks = new ArrayList<>(); -// for(Future task: tasks) { -// if(!task.isDone()) { -// nextTasks.add(task); -// } -// } -// running = !nextTasks.isEmpty(); -// tasks = nextTasks; -// } -// } -// } -// -// public static Future toJSONObject(Reader reader, Consumer after, Consumer error){ -// -// ExecutorService executor = Executors.newSingleThreadExecutor(); -// futureTask task = new futureTask(reader, after, error); -// Future future = executor.submit(task); -// -// return future; -// } -// -// private static class futureTask implements Callable { -// -// Reader reader; -// Consumer after; -// Consumer error; -// public futureTask(Reader reader, Consumer after, Consumer error) { -// this.reader = reader; -// this.after = after; -// this.error = error; -// } -// -// @Override -// public JSONObject call() throws Exception { -// JSONObject jo = new JSONObject(); -// try { -// XMLTokener x = new XMLTokener(reader); -// while (x.more()) { -// x.skipPast("<"); -// if (x.more()) { -// parse(x, jo, null, XMLParserConfiguration.ORIGINAL, 0); -// } -// } -// after.accept(jo); -// } catch (Exception e) { -// error.accept(e); -// } -// return jo; -// } -// } -private static final ExecutorService executor = Executors.newFixedThreadPool( - Runtime.getRuntime().availableProcessors() -); + // milestone 5 + private static final ExecutorService executor = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors() + ); public static Future toJSONObject( Reader reader, @@ -2321,10 +2255,6 @@ public static Future toJSONObject( return task; } - public static void shutdownExecutor() { - executor.shutdown(); - } - private static class FutureTaskCallable implements Callable { private final Reader reader; private final Consumer after; @@ -2370,27 +2300,5 @@ public void add(Future task) { this.tasks.add(task); } - public void run() { - boolean running = true; - while (running) { - List> nextTasks = new ArrayList<>(); - for (Future task : tasks) { - if (!task.isDone()) { - nextTasks.add(task); - } - } - if (nextTasks.isEmpty()) { - running = false; - } else { - tasks = nextTasks; - try { - Thread.sleep(50); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - running = false; - } - } - } - } } }