001package gudusoft.gsqlparser.util.json; 002 003import gudusoft.gsqlparser.dlineage.util.BeanUtil; 004 005import java.io.IOException; 006import java.io.Serializable; 007import java.io.Writer; 008import java.lang.reflect.Array; 009import java.math.BigDecimal; 010import java.math.BigInteger; 011import java.util.Collection; 012import java.util.LinkedHashMap; 013import java.util.List; 014import java.util.Map; 015import java.util.regex.Pattern; 016 017/** 018 * Writes a map or a list as JSON, as a string or into a {@link Writer}. Both entry points share one 019 * writer, so they produce the same text (Mantis 4799: two hand-copied paths had drifted apart). 020 * 021 * <p>A value is written by its type, wherever it sits (map value, list element, array element): 022 * a {@code String} or {@code Character} as a string; a {@code Number} as a number, except that a 023 * number with no JSON form, such as NaN or an infinity, becomes {@code null}; a 024 * {@code Boolean} as {@code true} or {@code false}; a {@code Collection} or a Java array as an array; 025 * anything else, a map or a bean, as an object through {@link BeanUtil#bean2Map(Object)}. 026 * A {@code null} map value or element is left out, and with {@code ignoreId} every member named 027 * {@code id} is left out. 028 */ 029@SuppressWarnings("rawtypes") 030public class JSONSerializer implements Serializable { 031 032 private static final long serialVersionUID = -947149613761603468L; 033 034 private static final Pattern JSON_NUMBER = Pattern.compile("-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?"); 035 036 private boolean ignoreId = false; 037 038 public JSONSerializer(boolean ignoreId) { 039 this.ignoreId = ignoreId; 040 } 041 042 public JSONSerializer() { 043 } 044 045 private void check(Object obj) { 046 if (obj == null || !(obj instanceof LinkedHashMap || obj instanceof List)) { 047 throw new IllegalArgumentException("Invalid JSON structure"); 048 } 049 } 050 051 public String serialize(List list) { 052 check(list); 053 StringBuilder sb = new StringBuilder(); 054 try { 055 writeArray(list, sb); 056 } catch (IOException e) { 057 throw new JSONException(e); // a StringBuilder does not throw 058 } 059 return sb.toString(); 060 } 061 062 public String serialize(Map map) { 063 check(map); 064 StringBuilder sb = new StringBuilder(); 065 try { 066 writeObject(map, sb); 067 } catch (IOException e) { 068 throw new JSONException(e); // a StringBuilder does not throw 069 } 070 return sb.toString(); 071 } 072 073 public void serialize(Map map, Writer writer) throws IOException { 074 check(map); 075 writeObject(map, writer); 076 } 077 078 public void serialize(List list, Writer writer) throws IOException { 079 check(list); 080 writeArray(list, writer); 081 } 082 083 private void writeObject(Map map, Appendable out) throws IOException { 084 out.append('{'); 085 boolean first = true; 086 for (Object k : map.keySet()) { 087 String key = (String) k; 088 if (ignoreId && ("id".equals(key))) { 089 continue; 090 } 091 Object val = map.get(key); 092 if (val == null) { 093 continue; // Skip null values 094 } 095 if (!first) { 096 out.append(','); 097 } 098 first = false; 099 // A key is escaped exactly like a string value: keys can be user data, such as the 100 // lineage graph's tooltip map, keyed by node labels that keep their quotes (Mantis 4798). 101 writeString(String.valueOf(key), out); 102 out.append(':'); 103 writeValue(val, out); 104 } 105 out.append('}'); 106 } 107 108 // The separator goes BEFORE each written element: a skipped null must not leave "a", in front 109 // of the closing bracket (Mantis 4798). 110 private void writeArray(Collection collection, Appendable out) throws IOException { 111 out.append('['); 112 boolean first = true; 113 for (Object element : collection) { 114 if (element == null) { 115 continue; // Skip null values 116 } 117 if (!first) { 118 out.append(','); 119 } 120 first = false; 121 writeValue(element, out); 122 } 123 out.append(']'); 124 } 125 126 private void writeJavaArray(Object array, Appendable out) throws IOException { 127 out.append('['); 128 boolean first = true; 129 int n = Array.getLength(array); 130 for (int i = 0; i < n; i++) { 131 Object element = Array.get(array, i); 132 if (element == null) { 133 continue; // Skip null values 134 } 135 if (!first) { 136 out.append(','); 137 } 138 first = false; 139 writeValue(element, out); 140 } 141 out.append(']'); 142 } 143 144 private void writeValue(Object val, Appendable out) throws IOException { 145 if (val instanceof String || val instanceof Character) { 146 writeString(val.toString(), out); 147 } else if (val instanceof Number) { 148 out.append(numberText((Number) val)); 149 } else if (val instanceof Boolean) { 150 out.append(val.toString()); 151 } else if (val instanceof Collection) { 152 writeArray((Collection) val, out); 153 } else if (val.getClass().isArray()) { 154 writeJavaArray(val, out); 155 } else { 156 writeObject(BeanUtil.bean2Map(val), out); 157 } 158 } 159 160 /** 161 * The JSON text of a number. NaN and the infinities have no JSON form; JSON.stringify and 162 * fastjson write them as null, and so does any other number whose text is not a JSON number. 163 */ 164 private static String numberText(Number number) { 165 if (number instanceof Double || number instanceof Float) { 166 double value = number.doubleValue(); 167 return Double.isNaN(value) || Double.isInfinite(value) ? "null" : number.toString(); 168 } 169 String text = number.toString(); 170 if (number instanceof Integer || number instanceof Long || number instanceof Short 171 || number instanceof Byte || number instanceof BigInteger || number instanceof BigDecimal) { 172 return text; 173 } 174 // a DoubleAdder, a DoubleAccumulator or any other Number can hold NaN too 175 return JSON_NUMBER.matcher(text).matches() ? text : "null"; 176 } 177 178 private static void writeString(String str, Appendable out) throws IOException { 179 out.append('"'); 180 int unwritten = 0; 181 // An index loop, not a StringCharacterIterator: its DONE sentinel is U+FFFF, so a real 182 // U+FFFF in the text cut the string short, and two keys differing after it collided. 183 for (int i = 0; i < str.length(); i++) { 184 String escaped = escapeOf(str.charAt(i)); 185 if (escaped != null) { 186 out.append(str, unwritten, i).append(escaped); 187 unwritten = i + 1; 188 } 189 } 190 out.append(str, unwritten, str.length()).append('"'); 191 } 192 193 /** The escape that stands for {@code ch} inside a JSON string, or null to write it as it is. */ 194 private static String escapeOf(char ch) { 195 switch (ch) { 196 case '"': 197 return "\\\""; 198 case '\\': 199 return "\\\\"; 200 case '/': 201 return "\\/"; 202 case '\b': 203 return "\\b"; 204 case '\f': 205 return "\\f"; 206 case '\n': 207 return "\\n"; 208 case '\r': 209 return "\\r"; 210 case '\t': 211 return "\\t"; 212 default: 213 // JSON allows no raw control character inside a string (RFC 8259, section 7) 214 return ch < 0x20 ? String.format("\\u%04x", (int) ch) : null; 215 } 216 } 217}