001package gudusoft.gsqlparser.pp.score.json; 002 003import java.util.Collection; 004import java.util.Iterator; 005import java.util.Locale; 006import java.util.Map; 007 008/** 009 * Minimal, dependency-free JSON writer with deterministic output. 010 * 011 * <p>Handles {@link Map} (insertion order preserved), {@link Collection}, 012 * arrays, {@link String}, {@link Number}, {@link Boolean}, {@link Enum} (as 013 * its lower-case name) and {@code null}. Doubles are written with at most 014 * three decimals and no exponent so two runs over the same input produce 015 * byte-identical text. 016 * 017 * <p>Package-private on purpose: the JSON contract is owned by 018 * {@code ScoreReport}; nothing else should serialise ad hoc. 019 */ 020public final class JsonOut { 021 022 private JsonOut() { 023 } 024 025 public static String toJson(Object value, boolean pretty) { 026 StringBuilder sb = new StringBuilder(1024); 027 write(sb, value, pretty ? 0 : -1); 028 return sb.toString(); 029 } 030 031 /** Format a double for the wire: at most 3 decimals, no trailing zeros, no exponent. */ 032 public static String num(double d) { 033 if (Double.isNaN(d) || Double.isInfinite(d)) return "null"; 034 if (d == Math.rint(d) && Math.abs(d) < 1e15) { 035 return Long.toString((long) d); 036 } 037 String s = String.format(Locale.ROOT, "%.3f", d); 038 int end = s.length(); 039 while (end > 0 && s.charAt(end - 1) == '0') end--; 040 if (end > 0 && s.charAt(end - 1) == '.') end--; 041 return s.substring(0, end); 042 } 043 044 private static void write(StringBuilder sb, Object v, int indent) { 045 if (v == null) { 046 sb.append("null"); 047 } else if (v instanceof String) { 048 quote(sb, (String) v); 049 } else if (v instanceof Boolean) { 050 sb.append(((Boolean) v).booleanValue() ? "true" : "false"); 051 } else if (v instanceof Double || v instanceof Float) { 052 sb.append(num(((Number) v).doubleValue())); 053 } else if (v instanceof Number) { 054 sb.append(v.toString()); 055 } else if (v instanceof Enum) { 056 quote(sb, ((Enum<?>) v).name().toLowerCase(Locale.ROOT)); 057 } else if (v instanceof Map) { 058 writeMap(sb, (Map<?, ?>) v, indent); 059 } else if (v instanceof Collection) { 060 writeArray(sb, ((Collection<?>) v).iterator(), ((Collection<?>) v).size(), indent); 061 } else if (v instanceof int[]) { 062 int[] a = (int[]) v; 063 sb.append('['); 064 for (int i = 0; i < a.length; i++) { 065 if (i > 0) sb.append(','); 066 sb.append(a[i]); 067 } 068 sb.append(']'); 069 } else if (v instanceof Object[]) { 070 Object[] a = (Object[]) v; 071 writeArray(sb, java.util.Arrays.asList(a).iterator(), a.length, indent); 072 } else { 073 quote(sb, v.toString()); 074 } 075 } 076 077 private static void writeMap(StringBuilder sb, Map<?, ?> m, int indent) { 078 if (m.isEmpty()) { 079 sb.append("{}"); 080 return; 081 } 082 sb.append('{'); 083 boolean first = true; 084 for (Map.Entry<?, ?> e : m.entrySet()) { 085 if (!first) sb.append(','); 086 first = false; 087 newline(sb, indent < 0 ? -1 : indent + 1); 088 quote(sb, String.valueOf(e.getKey())); 089 sb.append(indent >= 0 ? ": " : ":"); 090 write(sb, e.getValue(), indent >= 0 ? indent + 1 : -1); 091 } 092 newline(sb, indent); 093 sb.append('}'); 094 } 095 096 private static void writeArray(StringBuilder sb, Iterator<?> it, int size, int indent) { 097 if (size == 0) { 098 sb.append("[]"); 099 return; 100 } 101 sb.append('['); 102 boolean first = true; 103 while (it.hasNext()) { 104 if (!first) sb.append(','); 105 first = false; 106 newline(sb, indent < 0 ? -1 : indent + 1); 107 write(sb, it.next(), indent >= 0 ? indent + 1 : -1); 108 } 109 newline(sb, indent); 110 sb.append(']'); 111 } 112 113 private static void newline(StringBuilder sb, int indent) { 114 if (indent < 0) return; 115 sb.append('\n'); 116 for (int i = 0; i < indent; i++) sb.append(" "); 117 } 118 119 public static void quote(StringBuilder sb, String s) { 120 sb.append('"'); 121 for (int i = 0; i < s.length(); i++) { 122 char c = s.charAt(i); 123 switch (c) { 124 case '"': sb.append("\\\""); break; 125 case '\\': sb.append("\\\\"); break; 126 case '\n': sb.append("\\n"); break; 127 case '\r': sb.append("\\r"); break; 128 case '\t': sb.append("\\t"); break; 129 case '\b': sb.append("\\b"); break; 130 case '\f': sb.append("\\f"); break; 131 default: 132 if (c < 0x20) { 133 sb.append(String.format(Locale.ROOT, "\\u%04x", (int) c)); 134 } else { 135 sb.append(c); 136 } 137 } 138 } 139 sb.append('"'); 140 } 141}