Skip to content

Commit a736292

Browse files
authored
Merge pull request #2 from wsw-stack/milestone3-jiacheng2
Finish Milestone 3
2 parents f369612 + 42e7ca9 commit a736292

File tree

3 files changed

+332
-2
lines changed

3 files changed

+332
-2
lines changed

README-M3.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Milestone 3
2+
3+
For the Milestone 3 of SWE262P, the following new functions was added:
4+
5+
```java
6+
static JSONObject toJSONObject(Reader reader, Function func)
7+
```
8+
9+
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.
10+
11+
The new function is placed in the `XML.java` file.
12+
13+
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:
14+
15+
`mvn -Dtest=XMLKeyTransformerTest test`
16+
17+
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.
18+
19+
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.

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

Lines changed: 272 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import java.math.BigInteger;
1111
import java.util.*;
1212
import java.io.BufferedReader;
13-
import java.io.Reader;
13+
import java.util.function.Function;
1414
import java.util.stream.Collectors;
1515

1616
/**
@@ -482,6 +482,244 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
482482
}
483483
}
484484
}
485+
486+
/**
487+
* Compared to the original parse function, this function adds the function (String Convertor) as an input
488+
* @param x
489+
* @param context
490+
* @param name
491+
* @param config
492+
* @param currentNestingDepth
493+
* @param keyTransformer
494+
* The function which takes in a single String parameter, and returns another converted String
495+
* @return
496+
* @throws JSONException
497+
*/
498+
private static boolean parseMilestone3(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config, int currentNestingDepth,Function<String, String> keyTransformer)
499+
throws JSONException {
500+
char c;
501+
int i;
502+
JSONObject jsonObject = null;
503+
String string;
504+
String tagName;
505+
Object token;
506+
XMLXsiTypeConverter<?> xmlXsiTypeConverter;
507+
508+
// Test for and skip past these forms:
509+
// <!-- ... -->
510+
// <! ... >
511+
// <![ ... ]]>
512+
// <? ... ?>
513+
// Report errors for these forms:
514+
// <>
515+
// <=
516+
// <<
517+
token = x.nextToken();
518+
519+
// <!
520+
if (token == BANG) {
521+
c = x.next();
522+
if (c == '-') {
523+
if (x.next() == '-') {
524+
x.skipPast("-->");
525+
return false;
526+
}
527+
x.back();
528+
} else if (c == '[') {
529+
token = x.nextToken();
530+
if ("CDATA".equals(token)) {
531+
if (x.next() == '[') {
532+
string = x.nextCDATA();
533+
if (string.length() > 0) {
534+
context.accumulate(config.getcDataTagName(), string);
535+
}
536+
return false;
537+
}
538+
}
539+
throw x.syntaxError("Expected 'CDATA['");
540+
}
541+
i = 1;
542+
do {
543+
token = x.nextMeta();
544+
if (token == null) {
545+
throw x.syntaxError("Missing '>' after '<!'.");
546+
} else if (token == LT) {
547+
i += 1;
548+
} else if (token == GT) {
549+
i -= 1;
550+
}
551+
} while (i > 0);
552+
return false;
553+
} else if (token == QUEST) {
554+
555+
// <?
556+
x.skipPast("?>");
557+
return false;
558+
} else if (token == SLASH) {
559+
560+
// Close tag </
561+
562+
token = x.nextToken();
563+
if (name == null) {
564+
throw x.syntaxError("Mismatched close tag " + token);
565+
}
566+
if (!token.equals(name)) {
567+
throw x.syntaxError("Mismatched " + name + " and " + token);
568+
}
569+
if (x.nextToken() != GT) {
570+
throw x.syntaxError("Misshaped close tag");
571+
}
572+
return true;
573+
574+
} else if (token instanceof Character) {
575+
throw x.syntaxError("Misshaped tag");
576+
577+
// Open tag <
578+
579+
} else {
580+
tagName = (String) token;
581+
String transformedTagName = keyTransformer.apply(tagName);//add
582+
token = null;
583+
jsonObject = new JSONObject();
584+
boolean nilAttributeFound = false;
585+
xmlXsiTypeConverter = null;
586+
for (;;) {
587+
if (token == null) {
588+
token = x.nextToken();
589+
}
590+
// attribute = value
591+
if (token instanceof String) {
592+
string = (String) token;
593+
token = x.nextToken();
594+
if (token == EQ) {
595+
token = x.nextToken();
596+
if (!(token instanceof String)) {
597+
throw x.syntaxError("Missing value");
598+
}
599+
String transformedKey = keyTransformer.apply(string); //add new code
600+
if (config.isConvertNilAttributeToNull()
601+
&& NULL_ATTR.equals(string)
602+
&& Boolean.parseBoolean((String) token)) {
603+
nilAttributeFound = true;
604+
} else if(config.getXsiTypeMap() != null && !config.getXsiTypeMap().isEmpty()
605+
&& TYPE_ATTR.equals(string)) {
606+
xmlXsiTypeConverter = config.getXsiTypeMap().get(token);
607+
} else if (!nilAttributeFound) {
608+
Object obj = stringToValue((String) token);
609+
jsonObject.accumulate(transformedKey, obj);
610+
}
611+
token = null;
612+
} else {
613+
jsonObject.accumulate(keyTransformer.apply(string), "");
614+
}
615+
} else if (token == SLASH) {
616+
// Empty tag <.../>
617+
if (x.nextToken() != GT) {
618+
throw x.syntaxError("Misshaped tag");
619+
}
620+
if (config.getForceList().contains(tagName)) {
621+
if (nilAttributeFound) {
622+
context.append(transformedTagName, JSONObject.NULL);
623+
} else if (jsonObject.length() > 0) {
624+
context.append(transformedTagName, jsonObject);
625+
} else {
626+
context.put(transformedTagName, new JSONArray());
627+
}
628+
} else {
629+
if (nilAttributeFound) {
630+
context.accumulate(transformedTagName, JSONObject.NULL);
631+
} else if (jsonObject.length() > 0) {
632+
context.accumulate(transformedTagName, jsonObject);
633+
} else {
634+
context.accumulate(transformedTagName, "");
635+
}
636+
}
637+
return false;
638+
/*
639+
if (config.getForceList().contains(prefix + tagName)) {
640+
// Force the value to be an array
641+
if (nilAttributeFound) {
642+
context.append(prefix + tagName, JSONObject.NULL);
643+
} else if (jsonObject.length() > 0) {
644+
context.append(prefix + tagName, jsonObject);
645+
} else {
646+
context.put(prefix + tagName, new JSONArray());
647+
}
648+
} else {
649+
if (nilAttributeFound) {
650+
context.accumulate(prefix + tagName, JSONObject.NULL);
651+
} else if (jsonObject.length() > 0) {
652+
context.accumulate(prefix + tagName, jsonObject);
653+
} else {
654+
context.accumulate(prefix + tagName, "");
655+
}
656+
}
657+
return false;
658+
*/
659+
} else if (token == GT) {
660+
// Content, between <...> and </...>
661+
for (;;) {
662+
token = x.nextContent();
663+
if (token == null) {
664+
if (tagName != null) {
665+
throw x.syntaxError("Unclosed tag " + tagName);
666+
}
667+
return false;
668+
} else if (token instanceof String) {
669+
string = (String) token;
670+
if (string.length() > 0) {
671+
if(xmlXsiTypeConverter != null) {
672+
jsonObject.accumulate(config.getcDataTagName(),
673+
stringToValue(string, xmlXsiTypeConverter));
674+
} else {
675+
Object obj = stringToValue((String) token);
676+
if (obj instanceof Boolean) {
677+
jsonObject.accumulate(config.getcDataTagName(),
678+
config.isKeepBooleanAsString()
679+
? ((String) token)
680+
: obj);
681+
} else if (obj instanceof Number) {
682+
jsonObject.accumulate(config.getcDataTagName(),
683+
config.isKeepNumberAsString()
684+
? ((String) token)
685+
: obj);
686+
} else {
687+
jsonObject.accumulate(config.getcDataTagName(), stringToValue((String) token));
688+
}
689+
}
690+
}
691+
692+
} else if (token == LT) {
693+
if (parseMilestone3(x, jsonObject,tagName, config, currentNestingDepth + 1, keyTransformer)) {
694+
if (config.getForceList().contains(tagName)) {
695+
if (jsonObject.length() == 0) {
696+
context.put(transformedTagName, new JSONArray());
697+
} else if (jsonObject.length() == 1
698+
&& jsonObject.opt(config.getcDataTagName()) != null) {
699+
context.append(transformedTagName, jsonObject.opt(config.getcDataTagName()));
700+
} else {
701+
context.append(transformedTagName, jsonObject);
702+
}
703+
} else {
704+
if (jsonObject.length() == 0) {
705+
context.accumulate(transformedTagName, "");
706+
} else if (jsonObject.length() == 1
707+
&& jsonObject.opt(config.getcDataTagName()) != null) {
708+
context.accumulate(transformedTagName, jsonObject.opt(config.getcDataTagName()));
709+
} else {
710+
context.accumulate(transformedTagName, jsonObject);
711+
}
712+
}
713+
return false;
714+
}
715+
}
716+
}
717+
} else {
718+
throw x.syntaxError("Misshaped tag");
719+
}
720+
}
721+
}
722+
}
485723
/**
486724
* This method removes any JSON entry which has the key set by XMLParserConfiguration.cDataTagName
487725
* and contains whitespace as this is caused by whitespace between tags. See test XMLTest.testNestedWithWhitespaceTrimmingDisabled.
@@ -955,6 +1193,39 @@ public static JSONObject toJSONObject(Reader reader, JSONPointer path) throws JS
9551193
return result;
9561194
}
9571195

1196+
/**
1197+
* Given a customized function, convert the keys in the Json Object
1198+
* @param reader the XML input
1199+
* @param keyTransformer a function that transforms each key name
1200+
* @return JSONObject with transformed keys
1201+
* @throws JSONException if any XML parsing or transformation fails
1202+
*/
1203+
public static JSONObject toJSONObject(Reader reader, Function<String, String> keyTransformer) throws JSONException {
1204+
JSONObject result = new JSONObject();
1205+
XMLTokener x = new XMLTokener(reader);
1206+
1207+
while (x.more()) {
1208+
x.skipPast("<");
1209+
if (x.more()) {
1210+
XML.parseMilestone3(x, result, null, XMLParserConfiguration.ORIGINAL, 0, keyTransformer);
1211+
}
1212+
}
1213+
return result;
1214+
}
1215+
/*
1216+
public static JSONObject toJSONObject(Reader reader, String prefix) throws JSONException {
1217+
JSONObject jo = new JSONObject();
1218+
XMLParserConfiguration config = XMLParserConfiguration.ORIGINAL;
1219+
XMLTokener x = new XMLTokener(reader, config);
1220+
while (x.more()) {
1221+
x.skipPast("<");
1222+
if(x.more()) {
1223+
parse(x, jo, null, prefix, config, 0);
1224+
}
1225+
}
1226+
return jo;
1227+
}
1228+
*/
9581229
/**
9591230
* Helper method: skip the current element and its entire subtree without
9601231
* building any JSON output.
@@ -1617,7 +1888,6 @@ public static JSONObject toJSONObject(Reader reader, JSONPointer path, JSONObjec
16171888
}
16181889
}
16191890

1620-
16211891
/**
16221892
* Convert a well-formed (but not necessarily valid) XML string into a
16231893
* JSONObject. Some information may be lost in this transformation because
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.json.junit.milestone3.tests;
2+
3+
import org.json.JSONException;
4+
import org.json.JSONObject;
5+
import org.json.JSONPointer;
6+
import org.json.XML;
7+
import org.junit.Test;
8+
9+
import java.io.StringReader;
10+
import java.util.function.Function;
11+
12+
import static org.junit.Assert.assertEquals;
13+
14+
public class XMLKeyTransformerTest {
15+
// define some customized functions for testing
16+
@Test
17+
public void keyTransformerAddPrefixTest() {
18+
String xml = "<book><title>Title</title><author>John</author></book>";
19+
Function<String, String> prefixer = key -> "swe262_" + key;
20+
21+
JSONObject result = XML.toJSONObject(new StringReader(xml), prefixer);
22+
23+
assertEquals("Title", result.getJSONObject("swe262_book").get("swe262_title"));
24+
assertEquals("John", result.getJSONObject("swe262_book").get("swe262_author"));
25+
}
26+
27+
@Test
28+
public void keyTransformerReverseTest() {
29+
String xml = "<data><item>value</item></data>";
30+
Function<String, String> reverser = key -> new StringBuilder(key).reverse().toString();
31+
32+
JSONObject result = XML.toJSONObject(new StringReader(xml), reverser);
33+
34+
assertEquals("value", result.getJSONObject("atad").get("meti"));
35+
}
36+
37+
@Test(expected = NullPointerException.class)
38+
public void nullReaderTest() {
39+
XML.toJSONObject(null, key -> "x_" + key);
40+
}
41+
}

0 commit comments

Comments
 (0)