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}