|
8 | 8 | import java.io.StringReader; |
9 | 9 | import java.math.BigDecimal; |
10 | 10 | 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; |
12 | 15 |
|
13 | 16 | /** |
14 | 17 | * This provides static methods to convert an XML text into a JSONObject, and to |
@@ -63,6 +66,10 @@ public XML() { |
63 | 66 | */ |
64 | 67 | public static final String TYPE_ATTR = "xsi:type"; |
65 | 68 |
|
| 69 | + private static boolean replaced = false; |
| 70 | + private static boolean skipCurrentKey = false; |
| 71 | + |
| 72 | + |
66 | 73 | /** |
67 | 74 | * Creates an iterator for navigating Code Points in a string instead of |
68 | 75 | * characters. Once Java7 support is dropped, this can be replaced with |
@@ -779,6 +786,330 @@ public static JSONObject toJSONObject(Reader reader, XMLParserConfiguration conf |
779 | 786 | } |
780 | 787 | return jo; |
781 | 788 | } |
| 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 | + |
782 | 1113 |
|
783 | 1114 | /** |
784 | 1115 | * Convert a well-formed (but not necessarily valid) XML string into a |
|
0 commit comments