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-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 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 4bf475935..2991622d4 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 @@ -780,6 +787,837 @@ 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 **/ + 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 * JSONObject. Some information may be lost in this transformation because 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/milestone2/tests/XMLPointerReplaceTest.java b/src/test/java/org/json/junit/milestone2/tests/XMLPointerReplaceTest.java new file mode 100644 index 000000000..4a439022a --- /dev/null +++ b/src/test/java/org/json/junit/milestone2/tests/XMLPointerReplaceTest.java @@ -0,0 +1,52 @@ +package org.json.junit.milestone2.tests; + +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); + } +} + +