Skip to content

Commit 15cf3ad

Browse files
author
jiachengzhuo
committed
Milestone 2 Task 2: Add XML.toJSONObject(...) with replacement logic and corresponding tests
1 parent 82a02d8 commit 15cf3ad

File tree

2 files changed

+384
-1
lines changed

2 files changed

+384
-1
lines changed

src/main/java/org/json/XML.java

Lines changed: 332 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
import java.io.StringReader;
99
import java.math.BigDecimal;
1010
import java.math.BigInteger;
11-
import java.util.Iterator;
11+
import java.util.*;
12+
import java.io.BufferedReader;
13+
import java.io.Reader;
14+
import java.util.stream.Collectors;
1215

1316
/**
1417
* This provides static methods to convert an XML text into a JSONObject, and to
@@ -63,6 +66,10 @@ public XML() {
6366
*/
6467
public static final String TYPE_ATTR = "xsi:type";
6568

69+
private static boolean replaced = false;
70+
private static boolean skipCurrentKey = false;
71+
72+
6673
/**
6774
* Creates an iterator for navigating Code Points in a string instead of
6875
* characters. Once Java7 support is dropped, this can be replaced with
@@ -779,6 +786,330 @@ public static JSONObject toJSONObject(Reader reader, XMLParserConfiguration conf
779786
}
780787
return jo;
781788
}
789+
/** SWE262P MileStone2 project, Task2 by Jiacheng Zhuo **/
790+
791+
/** Edit the parse method, add functions for the replacement implement **/
792+
private static boolean parseMilestone2(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config, int currentNestingDepth, List<String> targetPath,
793+
int targetPathLength,
794+
Map<String, Integer> arrayKey,
795+
boolean isReplace,
796+
boolean mergeToParent,
797+
JSONObject replacement)
798+
throws JSONException {
799+
char c;
800+
int i;
801+
JSONObject jsonObject = null;
802+
String string;
803+
String tagName;
804+
Object token;
805+
XMLXsiTypeConverter<?> xmlXsiTypeConverter;
806+
807+
808+
// Test for and skip past these forms:
809+
// <!-- ... -->
810+
// <! ... >
811+
// <![ ... ]]>
812+
// <? ... ?>
813+
// Report errors for these forms:
814+
// <>
815+
// <=
816+
// <<
817+
818+
token = x.nextToken();
819+
820+
// <!
821+
822+
if (token == BANG) {
823+
c = x.next();
824+
if (c == '-') {
825+
if (x.next() == '-') {
826+
x.skipPast("-->");
827+
return false;
828+
}
829+
x.back();
830+
} else if (c == '[') {
831+
token = x.nextToken();
832+
if ("CDATA".equals(token)) {
833+
if (x.next() == '[') {
834+
string = x.nextCDATA();
835+
if (string.length() > 0) {
836+
context.accumulate(config.getcDataTagName(), string);
837+
}
838+
return false;
839+
}
840+
}
841+
throw x.syntaxError("Expected 'CDATA['");
842+
}
843+
i = 1;
844+
do {
845+
token = x.nextMeta();
846+
if (token == null) {
847+
throw x.syntaxError("Missing '>' after '<!'.");
848+
} else if (token == LT) {
849+
i += 1;
850+
} else if (token == GT) {
851+
i -= 1;
852+
}
853+
} while (i > 0);
854+
return false;
855+
} else if (token == QUEST) {
856+
857+
// <?
858+
x.skipPast("?>");
859+
return false;
860+
} else if (token == SLASH) {
861+
862+
// Close tag </
863+
864+
token = x.nextToken();
865+
if (name == null) {
866+
throw x.syntaxError("Mismatched close tag " + token);
867+
}
868+
if (!token.equals(name)) {
869+
throw x.syntaxError("Mismatched " + name + " and " + token);
870+
}
871+
if (x.nextToken() != GT) {
872+
throw x.syntaxError("Misshaped close tag");
873+
}
874+
return true;
875+
876+
} else if (token instanceof Character) {
877+
throw x.syntaxError("Misshaped tag");
878+
879+
// Open tag <
880+
881+
} else {
882+
//--------add the replacement logic for new parse function by Jiacheng Zhuo----------------//
883+
String currentTag = token.toString();
884+
if (currentNestingDepth < targetPathLength) {
885+
boolean isTargetMatch = (currentNestingDepth == targetPathLength - 1) &&
886+
targetPath.get(targetPathLength - 1).equals(currentTag);
887+
boolean hasIndex = arrayKey.containsKey(currentTag);//
888+
int remainingIndex = hasIndex ? arrayKey.get(currentTag) : 0;//
889+
boolean indexMatches = !hasIndex || remainingIndex == 0; //
890+
891+
if (isReplace && !replaced && isTargetMatch && indexMatches) {
892+
if (isReplace && !replaced && isTargetMatch && indexMatches) {
893+
context.put(currentTag, replacement);
894+
replaced = true;
895+
x.skipPast(currentTag + ">");
896+
return false;
897+
}
898+
899+
replaced = true;
900+
x.skipPast(currentTag + ">");
901+
return false;
902+
}
903+
904+
if (isReplace && hasIndex && !indexMatches) {
905+
arrayKey.put(currentTag, remainingIndex - 1);
906+
}
907+
908+
if (!isReplace) {
909+
if (hasIndex) {
910+
skipCurrentKey = (remainingIndex != 0);
911+
arrayKey.put(currentTag, remainingIndex - 1);
912+
}
913+
914+
if (!targetPath.get(currentNestingDepth).equals(currentTag)) {
915+
skipCurrentKey = true;
916+
}
917+
918+
}
919+
} //--------add replacement logic ends-----------------------------------//
920+
tagName = (String) token;
921+
token = null;
922+
jsonObject = new JSONObject();
923+
boolean nilAttributeFound = false;
924+
xmlXsiTypeConverter = null;
925+
for (;;) {
926+
if (token == null) {
927+
token = x.nextToken();
928+
}
929+
// attribute = value
930+
if (token instanceof String) {
931+
string = (String) token;
932+
token = x.nextToken();
933+
if (token == EQ) {
934+
token = x.nextToken();
935+
if (!(token instanceof String)) {
936+
throw x.syntaxError("Missing value");
937+
}
938+
939+
if (config.isConvertNilAttributeToNull()
940+
&& NULL_ATTR.equals(string)
941+
&& Boolean.parseBoolean((String) token)) {
942+
nilAttributeFound = true;
943+
} else if(config.getXsiTypeMap() != null && !config.getXsiTypeMap().isEmpty()
944+
&& TYPE_ATTR.equals(string)) {
945+
xmlXsiTypeConverter = config.getXsiTypeMap().get(token);
946+
} else if (!nilAttributeFound) {
947+
Object obj = stringToValue((String) token);
948+
if (obj instanceof Boolean) {
949+
jsonObject.accumulate(string,
950+
config.isKeepBooleanAsString()
951+
? ((String) token)
952+
: obj);
953+
} else if (obj instanceof Number) {
954+
jsonObject.accumulate(string,
955+
config.isKeepNumberAsString()
956+
? ((String) token)
957+
: obj);
958+
} else {
959+
jsonObject.accumulate(string, stringToValue((String) token));
960+
}
961+
}
962+
token = null;
963+
} else {
964+
jsonObject.accumulate(string, "");
965+
}
966+
967+
968+
} else if (token == SLASH) {
969+
// Empty tag <.../>
970+
if (x.nextToken() != GT) {
971+
throw x.syntaxError("Misshaped tag");
972+
}
973+
if (config.getForceList().contains(tagName)) {
974+
// Force the value to be an array
975+
if (nilAttributeFound) {
976+
context.append(tagName, JSONObject.NULL);
977+
} else if (jsonObject.length() > 0) {
978+
context.append(tagName, jsonObject);
979+
} else {
980+
context.put(tagName, new JSONArray());
981+
}
982+
} else {
983+
if (nilAttributeFound) {
984+
context.accumulate(tagName, JSONObject.NULL);
985+
} else if (jsonObject.length() > 0) {
986+
context.accumulate(tagName, jsonObject);
987+
} else {
988+
context.accumulate(tagName, "");
989+
}
990+
}
991+
return false;
992+
993+
} else if (token == GT) {
994+
// Content, between <...> and </...>
995+
for (;;) {
996+
token = x.nextContent();
997+
if (token == null) {
998+
if (tagName != null) {
999+
throw x.syntaxError("Unclosed tag " + tagName);
1000+
}
1001+
return false;
1002+
} else if (token instanceof String) {
1003+
string = (String) token;
1004+
if (string.length() > 0) {
1005+
if(xmlXsiTypeConverter != null) {
1006+
jsonObject.accumulate(config.getcDataTagName(),
1007+
stringToValue(string, xmlXsiTypeConverter));
1008+
} else {
1009+
Object obj = stringToValue((String) token);
1010+
if (obj instanceof Boolean) {
1011+
jsonObject.accumulate(config.getcDataTagName(),
1012+
config.isKeepBooleanAsString()
1013+
? ((String) token)
1014+
: obj);
1015+
} else if (obj instanceof Number) {
1016+
jsonObject.accumulate(config.getcDataTagName(),
1017+
config.isKeepNumberAsString()
1018+
? ((String) token)
1019+
: obj);
1020+
} else {
1021+
jsonObject.accumulate(config.getcDataTagName(), stringToValue((String) token));
1022+
}
1023+
}
1024+
}
1025+
1026+
} else if (token == LT) {
1027+
1028+
if (parseMilestone2(x, jsonObject, tagName, config, currentNestingDepth + 1,
1029+
targetPath, targetPathLength, arrayKey, isReplace, mergeToParent, replacement)) {
1030+
if (config.getForceList().contains(tagName)) {
1031+
if (jsonObject.length() == 0) {
1032+
context.put(tagName, new JSONArray());
1033+
} else if (jsonObject.length() == 1
1034+
&& jsonObject.opt(config.getcDataTagName()) != null) {
1035+
context.append(tagName, jsonObject.opt(config.getcDataTagName()));
1036+
} else {
1037+
context.append(tagName, jsonObject);
1038+
}
1039+
} else {
1040+
if (jsonObject.length() == 0) {
1041+
context.accumulate(tagName, "");
1042+
} else if (jsonObject.length() == 1
1043+
&& jsonObject.opt(config.getcDataTagName()) != null) {
1044+
context.accumulate(tagName, jsonObject.opt(config.getcDataTagName()));
1045+
} else {
1046+
if (!config.shouldTrimWhiteSpace()) {
1047+
removeEmpty(jsonObject, config);
1048+
}
1049+
context.accumulate(tagName, jsonObject);
1050+
}
1051+
}
1052+
return false;
1053+
}
1054+
}
1055+
}
1056+
} else {
1057+
throw x.syntaxError("Misshaped tag");
1058+
}
1059+
}
1060+
}
1061+
}
1062+
/**
1063+
* Converts an XML input stream into a JSONObject, replacing a sub-object at a specified JSONPointer path.
1064+
*
1065+
* <p>This method is added as part of SWE262P Milestone2 Task2. It performs in-place replacement
1066+
* during parsing, avoiding the need to first build the entire JSON tree before modifying it.
1067+
* This offers performance benefits by allowing early exit from the parser once the target node is handled.</p>
1068+
*
1069+
* @param reader The XML input
1070+
* @param path The JSONPointer path where replacement should occur
1071+
* @param replacement The JSONObject to insert at the given path
1072+
* @return A JSONObject with the sub-object at the given path replaced
1073+
* @throws JSONException if parsing or path manipulation fails
1074+
*/
1075+
public static JSONObject toJSONObject(Reader reader, JSONPointer path, JSONObject replacement) throws JSONException {
1076+
JSONObject jo = new JSONObject();
1077+
XMLTokener x = new XMLTokener(reader);
1078+
1079+
// Reset shared state
1080+
replaced = false;
1081+
skipCurrentKey = false;
1082+
1083+
String[] segments = path.toString().split("/");
1084+
List<String> targetPath = Arrays.stream(segments)
1085+
.filter(s -> !s.isEmpty())
1086+
.collect(Collectors.toList());
1087+
int targetPathLength = targetPath.size();
1088+
1089+
Map<String, Integer> arrayKey = new HashMap<>();
1090+
for (int i = 0; i < segments.length; i++) {
1091+
if (segments[i].matches("\\d+") && i > 0) {
1092+
arrayKey.put(segments[i - 1], Integer.parseInt(segments[i]));
1093+
}
1094+
}
1095+
1096+
boolean mergeToParent = !path.toString().endsWith("/");
1097+
1098+
while (x.more()) {
1099+
x.skipPast("<");
1100+
if (x.more()) {
1101+
parseMilestone2(x, jo, null, XMLParserConfiguration.ORIGINAL, 0,
1102+
targetPath, targetPathLength, arrayKey, true, mergeToParent, replacement);
1103+
}
1104+
}
1105+
1106+
if (replaced) {
1107+
return jo;
1108+
} else {
1109+
throw new JSONException("Replacement failed or path not found: " + path);
1110+
}
1111+
}
1112+
7821113

7831114
/**
7841115
* Convert a well-formed (but not necessarily valid) XML string into a

0 commit comments

Comments
 (0)