• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aspectran / aspectran / #4057

11 Feb 2025 06:00AM CUT coverage: 35.269% (+1.9%) from 33.377%
#4057

push

github

topframe
Update

13 of 42 new or added lines in 7 files covered. (30.95%)

4 existing lines in 3 files now uncovered.

14247 of 40395 relevant lines covered (35.27%)

0.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.39
/utils/src/main/java/com/aspectran/utils/json/JsonWriter.java
1
/*
2
 * Copyright (c) 2008-2025 The Aspectran Project
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package com.aspectran.utils.json;
17

18
import com.aspectran.utils.ArrayStack;
19
import com.aspectran.utils.Assert;
20
import com.aspectran.utils.BeanUtils;
21
import com.aspectran.utils.StringifyContext;
22
import com.aspectran.utils.annotation.jsr305.NonNull;
23
import com.aspectran.utils.apon.Parameter;
24
import com.aspectran.utils.apon.Parameters;
25

26
import java.io.BufferedReader;
27
import java.io.IOException;
28
import java.io.StringReader;
29
import java.io.Writer;
30
import java.lang.reflect.Array;
31
import java.lang.reflect.InvocationTargetException;
32
import java.time.LocalDate;
33
import java.time.LocalDateTime;
34
import java.time.LocalTime;
35
import java.util.Collection;
36
import java.util.Date;
37
import java.util.Enumeration;
38
import java.util.Iterator;
39
import java.util.Map;
40

41
/**
42
 * Converts an object to a JSON formatted string.
43
 * <p>If pretty-printing is enabled, the JsonWriter will add newlines and
44
 * indentation to the written data. Pretty-printing is disabled by default.</p>
45
 *
46
 * <p>Created: 2008. 06. 12 PM 8:20:54</p>
47
 *
48
 * @author Juho Jeong
49
 */
50
public class JsonWriter {
51

52
    private static final String DEFAULT_INDENT_STRING = "  ";
53

54
    private static final String NULL_STRING = "null";
55

56
    private final ArrayStack<Boolean> writtenFlags = new ArrayStack<>();
1✔
57

58
    private final Writer writer;
59

60
    private StringifyContext stringifyContext;
61

62
    private boolean prettyPrint = true;
1✔
63

64
    private String indentString = DEFAULT_INDENT_STRING;
1✔
65

66
    private boolean nullWritable = true;
1✔
67

68
    private int indentDepth;
69

70
    private String pendedName;
71

72
    private Object upperObject;
73

74
    /**
75
     * Instantiates a new JsonWriter.
76
     * Pretty printing is enabled by default, and the indent string is
77
     * set to "  " (two spaces).
78
     * @param writer the character-output stream
79
     */
80
    public JsonWriter(Writer writer) {
1✔
81
        Assert.notNull(writer, "out must not be null");
1✔
82
        this.writer = writer;
1✔
83
        writtenFlags.push(false);
1✔
84
    }
1✔
85

86
    public void setStringifyContext(StringifyContext stringifyContext) {
87
        this.stringifyContext = stringifyContext;
1✔
88
        if (stringifyContext != null) {
1✔
89
            if (stringifyContext.hasPrettyPrint()) {
1✔
NEW
90
                setPrettyPrint(stringifyContext.isPrettyPrint());
×
91
            }
92
            if (stringifyContext.hasIndentSize()) {
1✔
NEW
93
                setIndentString(stringifyContext.getIndentString());
×
94
            }
95
            if (stringifyContext.hasNullWritable()) {
1✔
NEW
96
                setNullWritable(stringifyContext.isNullWritable());
×
97
            }
98
        }
99
    }
1✔
100

101
    @SuppressWarnings("unchecked")
102
    public <T extends JsonWriter> T apply(StringifyContext stringifyContext) {
103
        setStringifyContext(stringifyContext);
1✔
104
        return (T)this;
1✔
105
    }
106

107
    public void setPrettyPrint(boolean prettyPrint) {
UNCOV
108
        this.prettyPrint = prettyPrint;
×
109
        if (prettyPrint) {
×
NEW
110
            if (indentString == null) {
×
NEW
111
                indentString = DEFAULT_INDENT_STRING;
×
112
            }
113
        } else {
NEW
114
            indentString = null;
×
115
        }
116
    }
×
117

118
    @SuppressWarnings("unchecked")
119
    public <T extends JsonWriter> T prettyPrint(boolean prettyPrint) {
NEW
120
        setPrettyPrint(prettyPrint);
×
NEW
121
        return (T)this;
×
122
    }
123

124
    public void setIndentString(String indentString) {
125
        this.indentString = indentString;
×
NEW
126
    }
×
127

128
    @SuppressWarnings("unchecked")
129
    public <T extends JsonWriter> T indentString(String indentString) {
NEW
130
        setIndentString(indentString);
×
UNCOV
131
        return (T)this;
×
132
    }
133

134
    public void setNullWritable(boolean nullWritable) {
135
        this.nullWritable = nullWritable;
1✔
136
    }
1✔
137

138
    @SuppressWarnings("unchecked")
139
    public <T extends JsonWriter> T nullWritable(boolean nullWritable) {
140
        setNullWritable(nullWritable);
1✔
141
        return (T)this;
1✔
142
    }
143

144
    /**
145
     * Begins encoding a new object.
146
     * @throws IOException if an I/O error has occurred
147
     */
148
    @SuppressWarnings("unchecked")
149
    public <T extends JsonWriter> T beginObject() throws IOException {
150
        writePendedName();
1✔
151
        writer.write("{");
1✔
152
        nextLine();
1✔
153
        indentDepth++;
1✔
154
        writtenFlags.push(false);
1✔
155
        return (T)this;
1✔
156
    }
157

158
    /**
159
     * Ends encoding the current object.
160
     * @throws IOException if an I/O error has occurred
161
     */
162
    @SuppressWarnings("unchecked")
163
    public <T extends JsonWriter> T endObject() throws IOException {
164
        indentDepth--;
1✔
165
        if (writtenFlags.pop()) {
1✔
166
            nextLine();
1✔
167
        }
168
        indent();
1✔
169
        writer.write("}");
1✔
170
        writtenFlags.update(true);
1✔
171
        return (T)this;
1✔
172
    }
173

174
    /**
175
     * Begins encoding a new array.
176
     * @throws IOException if an I/O error has occurred
177
     */
178
    @SuppressWarnings("unchecked")
179
    public <T extends JsonWriter> T beginArray() throws IOException {
180
        writePendedName();
1✔
181
        writer.write("[");
1✔
182
        nextLine();
1✔
183
        indentDepth++;
1✔
184
        writtenFlags.push(false);
1✔
185
        return (T)this;
1✔
186
    }
187

188
    /**
189
     * Ends encoding the current array.
190
     * @throws IOException if an I/O error has occurred
191
     */
192
    @SuppressWarnings("unchecked")
193
    public <T extends JsonWriter> T endArray() throws IOException {
194
        indentDepth--;
1✔
195
        if (writtenFlags.pop()) {
1✔
196
            nextLine();
1✔
197
        }
198
        indent();
1✔
199
        writer.write("]");
1✔
200
        writtenFlags.update(true);
1✔
201
        return (T)this;
1✔
202
    }
203

204
    @SuppressWarnings("unchecked")
205
    public <T extends JsonWriter> T name(String name) throws IOException {
206
        writeName(name);
×
207
        return (T)this;
×
208
    }
209

210
    @SuppressWarnings("unchecked")
211
    public <T extends JsonWriter> T value(Object value) throws IOException {
212
        writeValue(value);
1✔
213
        return (T)this;
1✔
214
    }
215

216
    /**
217
     * Writes a key name to the writer.
218
     * @param name the string to write to the writer
219
     */
220
    public void writeName(String name) {
221
        pendedName = name;
1✔
222
    }
1✔
223

224
    private void writePendedName() throws IOException {
225
        if (writtenFlags.peek()) {
1✔
226
            writeComma();
1✔
227
        }
228
        if (pendedName != null) {
1✔
229
            indent();
1✔
230
            writer.write(escape(pendedName));
1✔
231
            writer.write(":");
1✔
232
            if (prettyPrint) {
1✔
233
                writer.write(" ");
1✔
234
            }
235
            pendedName = null;
1✔
236
        } else {
237
            indent();
1✔
238
        }
239
    }
1✔
240

241
    private void clearPendedName() {
242
        pendedName = null;
1✔
243
    }
1✔
244

245
    /**
246
     * Writes an object to the writer.
247
     * @param object the object to write to the writer.
248
     * @throws IOException if an I/O error has occurred.
249
     */
250
    public void writeValue(Object object) throws IOException {
251
        if (object == null) {
1✔
252
            writeNull();
1✔
253
        } else if (object instanceof String string) {
1✔
254
            writeString(string);
1✔
255
        } else if (object instanceof JsonString) {
1✔
256
            writeJson(object.toString());
×
257
        } else if (object instanceof Character) {
1✔
258
            writeString(String.valueOf(object));
×
259
        } else if (object instanceof Boolean bool) {
1✔
260
            writeBool(bool);
1✔
261
        } else if (object instanceof Number number) {
1✔
262
            writeNumber(number);
1✔
263
        } else if (object instanceof Parameters parameters) {
1✔
264
            beginObject();
1✔
265
            for (Parameter p : parameters.getParameterValues()) {
1✔
266
                String name = p.getName();
1✔
267
                Object value = p.getValue();
1✔
268
                writeName(name);
1✔
269
                writeValue(value, object);
1✔
270
            }
1✔
271
            endObject();
1✔
272
        } else if (object instanceof Map<?, ?> map) {
1✔
273
            beginObject();
1✔
274
            for (Map.Entry<?, ?> entry : map.entrySet()) {
1✔
275
                String name = entry.getKey().toString();
1✔
276
                Object value = entry.getValue();
1✔
277
                writeName(name);
1✔
278
                writeValue(value, object);
1✔
279
            }
1✔
280
            endObject();
1✔
281
        } else if (object instanceof Collection<?> collection) {
1✔
282
            beginArray();
1✔
283
            for (Object value : collection) {
1✔
284
                if (value != null) {
1✔
285
                    writeValue(value, object);
1✔
286
                } else {
287
                    writeNull(true);
×
288
                }
289
            }
1✔
290
            endArray();
1✔
291
        } else if (object instanceof Iterator<?> iterator) {
1✔
292
            beginArray();
×
293
            while (iterator.hasNext()) {
×
294
                Object value = iterator.next();
×
295
                if (value != null) {
×
296
                    writeValue(value, object);
×
297
                } else {
298
                    writeNull(true);
×
299
                }
300
            }
×
301
            endArray();
×
302
        } else if (object instanceof Enumeration<?> enumeration) {
1✔
303
            beginArray();
×
304
            while (enumeration.hasMoreElements()) {
×
305
                Object value = enumeration.nextElement();
×
306
                if (value != null) {
×
307
                    writeValue(value, object);
×
308
                } else {
309
                    writeNull(true);
×
310
                }
311
            }
×
312
            endArray();
×
313
        } else if (object.getClass().isArray()) {
1✔
314
            beginArray();
1✔
315
            int len = Array.getLength(object);
1✔
316
            for (int i = 0; i < len; i++) {
1✔
317
                Object value = Array.get(object, i);
1✔
318
                if (value != null) {
1✔
319
                    writeValue(value, object);
×
320
                } else {
321
                    writeNull(true);
1✔
322
                }
323
            }
324
            endArray();
1✔
325
        } else if (object instanceof LocalDateTime localDateTime) {
1✔
326
            if (stringifyContext != null) {
×
327
                writeString(stringifyContext.toString(localDateTime));
×
328
            } else {
329
                writeString(localDateTime.toString());
×
330
            }
331
        } else if (object instanceof LocalDate localDate) {
1✔
332
            if (stringifyContext != null) {
1✔
333
                writeString(stringifyContext.toString(localDate));
×
334
            } else {
335
                writeString(localDate.toString());
1✔
336
            }
337
        } else if (object instanceof LocalTime localTime) {
×
338
            if (stringifyContext != null) {
×
339
                writeString(stringifyContext.toString(localTime));
×
340
            } else {
341
                writeString(localTime.toString());
×
342
            }
343
        } else if (object instanceof Date date) {
×
344
            if (stringifyContext != null) {
×
345
                writeString(stringifyContext.toString(date));
×
346
            } else {
347
                writeString(date.toString());
×
348
            }
349
        } else {
350
            String[] readablePropertyNames = BeanUtils.getReadablePropertyNamesWithoutNonSerializable(object);
×
351
            if (readablePropertyNames != null && readablePropertyNames.length > 0) {
×
352
                beginObject();
×
353
                for (String propertyName : readablePropertyNames) {
×
354
                    Object value;
355
                    try {
356
                        value = BeanUtils.getProperty(object, propertyName);
×
357
                    } catch (InvocationTargetException e) {
×
358
                        throw new IOException(e);
×
359
                    }
×
360
                    writeName(propertyName);
×
361
                    writeValue(value, object);
×
362
                }
363
                endObject();
×
364
            } else {
365
                writeString(object.toString());
×
366
            }
367
        }
368
    }
1✔
369

370
    private void writeValue(Object object, Object container) throws IOException {
371
        checkCircularReference(container, object);
1✔
372
        this.upperObject = container;
1✔
373
        writeValue(object);
1✔
374
        this.upperObject = null;
1✔
375
    }
1✔
376

377
    /**
378
     * Writes a "null" string to the writer.
379
     * @throws IOException if an I/O error has occurred
380
     */
381
    public void writeNull() throws IOException {
382
        writeNull(false);
1✔
383
    }
1✔
384

385
    /**
386
     * Writes a "null" string to the writer.
387
     * @param force true if forces should be written null value
388
     * @throws IOException if an I/O error has occurred
389
     */
390
    public void writeNull(boolean force) throws IOException {
391
        if (nullWritable || force) {
1✔
392
            writePendedName();
1✔
393
            writer.write(NULL_STRING);
1✔
394
            writtenFlags.update(true);
1✔
395
        } else {
396
            clearPendedName();
1✔
397
        }
398
    }
1✔
399

400
    /**
401
     * Writes a string directly to the writer stream without
402
     * quoting or escaping.
403
     * @param json the string to write to the writer
404
     * @throws IOException if an I/O error has occurred
405
     */
406
    public void writeJson(String json) throws IOException {
407
        if (nullWritable || json != null) {
1✔
408
            writePendedName();
1✔
409
            if (json != null) {
1✔
410
                BufferedReader reader = new BufferedReader(new StringReader(json));
1✔
411
                boolean first = true;
1✔
412
                String line;
413
                while ((line = reader.readLine()) != null) {
1✔
414
                    if (!first) {
1✔
415
                        nextLine();
1✔
416
                        indent();
1✔
417
                    }
418
                    writer.write(line);
1✔
419
                    first = false;
1✔
420
                }
421
            } else {
1✔
422
                writer.write(NULL_STRING);
×
423
            }
424
            writtenFlags.update(true);
1✔
425
        } else {
426
            clearPendedName();
×
427
        }
428
    }
1✔
429

430
    /**
431
     * Writes a string to the writer.
432
     * If {@code value} is null, write a null string ("").
433
     * @param value the string to write to the writer
434
     * @throws IOException if an I/O error has occurred
435
     */
436
    private void writeString(String value) throws IOException {
437
        if (nullWritable || value != null) {
1✔
438
            writePendedName();
1✔
439
            writer.write(escape(value));
1✔
440
            writtenFlags.update(true);
1✔
441
        } else {
442
            clearPendedName();
×
443
        }
444
    }
1✔
445

446
    /**
447
     * Writes a {@code Boolean} object to the writer.
448
     * @param value a {@code Boolean} object to write to the writer
449
     * @throws IOException if an I/O error has occurred
450
     */
451
    private void writeBool(Boolean value) throws IOException {
452
        if (nullWritable || value != null) {
1✔
453
            writePendedName();
1✔
454
            writer.write(value.toString());
1✔
455
            writtenFlags.update(true);
1✔
456
        } else {
457
            clearPendedName();
×
458
        }
459
    }
1✔
460

461
    /**
462
     * Writes a {@code Number} object to the writer.
463
     * @param value a {@code Number} object to write to the writer
464
     * @throws IOException if an I/O error has occurred
465
     */
466
    private void writeNumber(Number value) throws IOException {
467
        if (nullWritable || value != null) {
1✔
468
            writePendedName();
1✔
469
            writer.write(value.toString());
1✔
470
            writtenFlags.update(true);
1✔
471
        } else {
472
            clearPendedName();
×
473
        }
474
    }
1✔
475

476
    /**
477
     * Writes a comma character to the writer.
478
     * @throws IOException if an I/O error has occurred
479
     */
480
    private void writeComma() throws IOException {
481
        writer.write(",");
1✔
482
        nextLine();
1✔
483
    }
1✔
484

485
    /**
486
     * Writes a tab character to the writer.
487
     * @throws IOException if an I/O error has occurred
488
     */
489
    private void indent() throws IOException {
490
        if (prettyPrint && indentString != null && !indentString.isEmpty()) {
1✔
491
            for (int i = 0; i < indentDepth; i++) {
1✔
492
                writer.write(indentString);
1✔
493
            }
494
        }
495
    }
1✔
496

497
    /**
498
     * Writes a new line character to the writer.
499
     * @throws IOException if an I/O error has occurred
500
     */
501
    private void nextLine() throws IOException {
502
        if (prettyPrint) {
1✔
503
            writer.write("\n");
1✔
504
        }
505
    }
1✔
506

507
    /**
508
     * Ensures all buffered data is written to the underlying
509
     * {@link Writer} and flushes that writer.
510
     * @throws IOException if an I/O error has occurred
511
     */
512
    public void flush() throws IOException {
513
        writer.flush();
×
514
    }
×
515

516
    public void close() throws IOException {
517
        writer.close();
×
518
    }
×
519

520
    @Override
521
    public String toString() {
522
        return writer.toString();
1✔
523
    }
524

525
    private void checkCircularReference(Object object, Object member) throws IOException {
526
        if (object == member || (upperObject != null && upperObject == member)) {
1✔
527
            String what;
528
            if (pendedName != null) {
1✔
529
                what = "member '" + pendedName + "'";
1✔
530
            } else {
531
                what = "a member";
1✔
532
            }
533
            throw new IOException("JSON Serialization Failure: " +
1✔
534
                    "A circular reference was detected while converting " + what);
535
        }
536
    }
1✔
537

538
    /**
539
     * Produce a string in double quotes with backslash sequences in all the
540
     * right places. A backslash will be inserted within &lt;/, allowing JSON
541
     * text to be delivered in HTML. In JSON text, a string cannot contain a
542
     * control character or an unescaped quote or backslash.
543
     * @param string the input String, may be null
544
     * @return a String correctly formatted for insertion in a JSON text
545
     */
546
    @NonNull
547
    private static String escape(String string) {
548
        if (string == null || string.isEmpty()) {
1✔
549
            return "\"\"";
×
550
        }
551

552
        int len = string.length();
1✔
553
        char b;
554
        char c = 0;
1✔
555
        String t;
556

557
        StringBuilder sb = new StringBuilder(len + 4);
1✔
558
        sb.append('"');
1✔
559
        for (int i = 0; i < len; i++) {
1✔
560
            b = c;
1✔
561
            c = string.charAt(i);
1✔
562

563
            switch (c) {
1✔
564
                case '\\':
565
                case '"':
566
                    sb.append('\\');
×
567
                    sb.append(c);
×
568
                    break;
×
569
                case '/':
570
                    if (b == '<') {
×
571
                        sb.append('\\');
×
572
                    }
573
                    sb.append(c);
×
574
                    break;
×
575
                case '\b':
576
                    sb.append("\\b");
×
577
                    break;
×
578
                case '\t':
579
                    sb.append("\\t");
×
580
                    break;
×
581
                case '\n':
582
                    sb.append("\\n");
×
583
                    break;
×
584
                case '\f':
585
                    sb.append("\\f");
×
586
                    break;
×
587
                case '\r':
588
                    sb.append("\\r");
×
589
                    break;
×
590
                default:
591
                    if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) {
1✔
592
                        t = "000" + Integer.toHexString(c);
×
593
                        sb.append("\\u").append(t.substring(t.length() - 4));
×
594
                    } else {
595
                        sb.append(c);
1✔
596
                    }
597
            }
598
        }
599
        sb.append('"');
1✔
600
        return sb.toString();
1✔
601
    }
602

603
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc