aboutsummaryrefslogtreecommitdiffstats
path: root/vespajlib/src/main/java/com/yahoo/text/JSONWriter.java
blob: 30746cf016c7c607ca1aefa7e751f47027085fc6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.text;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayDeque;
import java.util.Deque;

/**
 * A class which knows how to write JSON markup. All methods return this to
 * enable chaining of method calls.
 * Consider using the Jackson generator API instead, as that may be faster.
 *
 * @author bratseth
 */
public final class JSONWriter {

    /** A stack maintaining the "needs comma" state at the current level */
    private final Deque<Boolean> needsComma = new ArrayDeque<>();

    private static final char[] DIGITS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

    private final OutputStream stream;

    public JSONWriter(OutputStream stream) {
        this.stream = stream;
    }

    /** Called on the start of a field or array value */
    private void beginFieldOrArrayValue() throws IOException {
        if (needsComma.getFirst()) {
            write(",");
        }
    }

    /** Called on the end of a field or array value */
    private void endFieldOrArrayValue() {
        setNeedsComma();
    }

    /** Begins an object field */
    public JSONWriter beginField(String fieldName) throws IOException {
        beginFieldOrArrayValue();
        write("\"" + fieldName + "\":");
        return this;
    }

    /** Ends an object field */
    public JSONWriter endField() throws IOException {
        endFieldOrArrayValue();
        return this;
    }

    /** Begins an array value */
    public JSONWriter beginArrayValue() throws IOException {
        beginFieldOrArrayValue();
        return this;
    }

    /** Ends an array value */
    public JSONWriter endArrayValue() throws IOException {
        endFieldOrArrayValue();
        return this;
    }

    /** Begin an object value */
    public JSONWriter beginObject() throws IOException {
        write("{");
        needsComma.addFirst(Boolean.FALSE);
        return this;
    }

    /** End an object value */
    public JSONWriter endObject() throws IOException {
        write("}");
        needsComma.removeFirst();
        return this;
    }

    /** Begin an array value */
    public JSONWriter beginArray() throws IOException {
        write("[");
        needsComma.addFirst(Boolean.FALSE);
        return this;
    }

    /** End an array value */
    public JSONWriter endArray() throws IOException {
        write("]");
        needsComma.removeFirst();
        return this;
    }

    /** Writes a string value */
    public JSONWriter value(String value) throws IOException {
        write("\"").write(escape(value)).write("\"");
        return this;
    }

    /** Writes a numeric value */
    public JSONWriter value(Number value) throws IOException {
        write(value.toString());
        return this;
    }

    /** Writes a boolean value */
    public JSONWriter value(boolean value) throws IOException {
        write(Boolean.toString(value));
        return this;
    }

    /** Writes a null value */
    public JSONWriter value() throws IOException {
        write("null");
        return this;
    }

    private void setNeedsComma() {
        if (level() == 0) return;
        needsComma.removeFirst();
        needsComma.addFirst(Boolean.TRUE);
    }

    /** Returns the current nested level */
    private int level() { return needsComma.size(); }

    /**
     * Writes a string directly as-is to the stream of this.
     *
     * @return this for convenience
     */
    private JSONWriter write(String string) throws IOException {
        if (string.length() == 0) return this;
        stream.write(Utf8.toBytes(string));
        return this;
    }

    /**
     * Do JSON escaping of a string.
     *
     * @param in a string to escape
     * @return a String suitable for use in JSON strings
     */
    private String escape(final String in) {
        final StringBuilder quoted = new StringBuilder((int) (in.length() * 1.2));
        return escape(in, quoted).toString();
    }

    /**
     * Do JSON escaping of the incoming string to the "quoted" buffer. The
     * buffer returned is the same as the one given in the "quoted" parameter.
     *
     * @param in a string to escape
     * @param escaped the target buffer for escaped data
     * @return the same buffer as given in the "quoted" parameter
     */
    private StringBuilder escape(final String in, final StringBuilder escaped) {
        for (final char c : in.toCharArray()) {
            switch (c) {
                case ('"'):
                    escaped.append("\\\"");
                    break;
                case ('\\'):
                    escaped.append("\\\\");
                    break;
                case ('\b'):
                    escaped.append("\\b");
                    break;
                case ('\f'):
                    escaped.append("\\f");
                    break;
                case ('\n'):
                    escaped.append("\\n");
                    break;
                case ('\r'):
                    escaped.append("\\r");
                    break;
                case ('\t'):
                    escaped.append("\\t");
                    break;
                default:
                    if (c < 32) {
                        escaped.append("\\u").append(fourDigitHexString(c));
                    } else {
                        escaped.append(c);
                    }
            }
        }
        return escaped;
    }

    private static char[] fourDigitHexString(final char c) {
        final char[] hex = new char[4];
        int in = ((c) & 0xFFFF);
        for (int i = 3; i >= 0; --i) {
            hex[i] = DIGITS[in & 0xF];
            in >>>= 4;
        }
        return hex;
    }

}