Skip to content

Commit fb52f85

Browse files
committed
Update JSONDecoder.
1 parent 950e63f commit fb52f85

6 files changed

Lines changed: 184 additions & 73 deletions

File tree

kilo-client/src/main/java/org/httprpc/kilo/io/CSVEncoder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public void write(Iterable<?> row, Writer writer) throws IOException {
6666
}
6767

6868
/**
69-
* Encodes multiple rows.
69+
* Encodes multiple rows to an output stream.
7070
*
7171
* @param rows
7272
* The rows to encode.
@@ -86,7 +86,7 @@ public void writeAll(Iterable<? extends Iterable<?>> rows, OutputStream outputSt
8686
}
8787

8888
/**
89-
* Encodes multiple rows.
89+
* Encodes multiple rows to a character stream.
9090
*
9191
* @param rows
9292
* The rows to encode.

kilo-client/src/main/java/org/httprpc/kilo/io/JSONDecoder.java

Lines changed: 108 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package org.httprpc.kilo.io;
1616

1717
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.io.InputStreamReader;
1820
import java.io.Reader;
1921
import java.util.ArrayList;
2022
import java.util.Deque;
@@ -38,7 +40,6 @@ public class JSONDecoder extends Decoder<Object> {
3840
private static final String NULL = "null";
3941

4042
@Override
41-
@SuppressWarnings("unchecked")
4243
public Object read(Reader reader) throws IOException {
4344
if (reader == null) {
4445
throw new IllegalArgumentException();
@@ -60,89 +61,135 @@ public Object read(Reader reader) throws IOException {
6061
} else if (c == ',') {
6162
c = reader.read();
6263
} else {
63-
var container = containers.peek();
64+
value = readValue(reader);
65+
}
6466

65-
// If the current container is a map, read the key
66-
String key;
67-
if (container instanceof Map) {
68-
if (c != '"') {
69-
throw new IOException("Invalid key.");
70-
}
67+
skipWhitespace(reader);
68+
}
7169

72-
key = readString(reader);
70+
if (!containers.isEmpty()) {
71+
throw new IOException("Unterminated container.");
72+
}
7373

74-
skipWhitespace(reader);
74+
return value;
75+
}
7576

76-
if (c != ':') {
77-
throw new IOException("Missing colon.");
78-
}
77+
/**
78+
* Reads multiple values from an input stream.
79+
*
80+
* @param inputStream
81+
* The input stream to read from.
82+
*
83+
* @return
84+
* The decoded values.
85+
*
86+
* @throws IOException
87+
* If an exception occurs.
88+
*/
89+
public Iterable<Object> readAll(InputStream inputStream) throws IOException {
90+
if (inputStream == null) {
91+
throw new IllegalArgumentException();
92+
}
7993

80-
c = reader.read();
94+
return readAll(new InputStreamReader(inputStream, getCharset()));
95+
}
8196

82-
skipWhitespace(reader);
83-
} else {
84-
key = null;
85-
}
97+
/**
98+
* Reads multiple values from a character stream.
99+
*
100+
* @param reader
101+
* The character stream to read from.
102+
*
103+
* @return
104+
* The decoded values.
105+
*
106+
* @throws IOException
107+
* If an exception occurs.
108+
*/
109+
@SuppressWarnings("unchecked")
110+
public Iterable<Object> readAll(Reader reader) throws IOException {
111+
// TODO
112+
return (Iterable<Object>)read(reader);
113+
}
114+
115+
private void skipWhitespace(Reader reader) throws IOException {
116+
while (c != EOF && Character.isWhitespace(c)) {
117+
c = reader.read();
118+
}
119+
}
120+
121+
@SuppressWarnings("unchecked")
122+
private Object readValue(Reader reader) throws IOException {
123+
var container = containers.peek();
124+
125+
// If the current container is a map, read the key
126+
String key;
127+
if (container instanceof Map) {
128+
if (c != '"') {
129+
throw new IOException("Invalid key.");
130+
}
131+
132+
key = readString(reader);
86133

87-
// Read the value
88-
if (c == '"') {
89-
value = readString(reader);
90-
} else if (c == '-' || Character.isDigit(c)) {
91-
value = readNumber(reader);
92-
} else if (c == TRUE.charAt(0)) {
93-
readLiteral(reader, TRUE);
134+
skipWhitespace(reader);
94135

95-
value = Boolean.TRUE;
96-
} else if (c == FALSE.charAt(0)) {
97-
readLiteral(reader, FALSE);
136+
if (c != ':') {
137+
throw new IOException("Missing colon.");
138+
}
98139

99-
value = Boolean.FALSE;
100-
} else if (c == NULL.charAt(0)) {
101-
readLiteral(reader, NULL);
140+
c = reader.read();
102141

103-
value = null;
104-
} else if (c == '[') {
105-
value = new ArrayList<>();
142+
skipWhitespace(reader);
143+
} else {
144+
key = null;
145+
}
106146

107-
containers.push(value);
147+
// Read the value
148+
Object value;
149+
if (c == '"') {
150+
value = readString(reader);
151+
} else if (c == '-' || Character.isDigit(c)) {
152+
value = readNumber(reader);
153+
} else if (c == TRUE.charAt(0)) {
154+
readLiteral(reader, TRUE);
108155

109-
c = reader.read();
110-
} else if (c == '{') {
111-
value = new LinkedHashMap<>();
156+
value = Boolean.TRUE;
157+
} else if (c == FALSE.charAt(0)) {
158+
readLiteral(reader, FALSE);
112159

113-
containers.push(value);
160+
value = Boolean.FALSE;
161+
} else if (c == NULL.charAt(0)) {
162+
readLiteral(reader, NULL);
114163

115-
c = reader.read();
116-
} else {
117-
throw new IOException(String.format("Unexpected character (0x%04X).", c));
118-
}
164+
value = null;
165+
} else if (c == '[') {
166+
value = new ArrayList<>();
119167

120-
// Add the value to the current container
121-
if (container != null) {
122-
if (key != null) {
123-
((Map<String, Object>)container).put(key, value);
124-
} else {
125-
((List<Object>)container).add(value);
126-
}
127-
}
128-
}
168+
containers.push(value);
129169

130-
skipWhitespace(reader);
170+
c = reader.read();
171+
} else if (c == '{') {
172+
value = new LinkedHashMap<>();
173+
174+
containers.push(value);
175+
176+
c = reader.read();
177+
} else {
178+
throw new IOException(String.format("Unexpected character (0x%04X).", c));
131179
}
132180

133-
if (!containers.isEmpty()) {
134-
throw new IOException("Unterminated container.");
181+
// Add the value to the current container
182+
if (container != null) {
183+
if (key != null) {
184+
((Map<String, Object>)container).put(key, value);
185+
} else {
186+
((List<Object>)container).add(value);
187+
}
135188
}
136189

137190
return value;
138191
}
139192

140-
private void skipWhitespace(Reader reader) throws IOException {
141-
while (c != EOF && Character.isWhitespace(c)) {
142-
c = reader.read();
143-
}
144-
}
145-
146193
private String readString(Reader reader) throws IOException {
147194
valueBuilder.setLength(0);
148195

kilo-client/src/test/java/org/httprpc/kilo/io/JSONDecoderTest.java

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.io.StringReader;
21-
import java.util.function.Supplier;
21+
import java.io.StringWriter;
2222

2323
import static org.httprpc.kilo.util.Collections.*;
2424
import static org.junit.jupiter.api.Assertions.*;
@@ -109,6 +109,15 @@ public void testUnterminatedObject() {
109109
assertThrows(IOException.class, () -> decode("{\"a\": 1, \"b\": 2, \"c\": 3, "));
110110
}
111111

112+
@Test
113+
public void testTrailingWhitespace() throws IOException {
114+
var expected = listOf(1, 2, 3);
115+
116+
var actual = decode("[1, 2, 3]\n");
117+
118+
assertEquals(actual, expected);
119+
}
120+
112121
@Test
113122
public void testInvalidKey() {
114123
assertThrows(IOException.class, () -> decode("{a: 1}"));
@@ -125,12 +134,39 @@ public void testInvalidCharacters() {
125134
}
126135

127136
private static Object decode(String text) throws IOException {
128-
return decode(text, JSONDecoder::new);
137+
var jsonDecoder = new JSONDecoder();
138+
139+
return jsonDecoder.read(new StringReader(text));
140+
}
141+
142+
@Test
143+
public void testReadAll() throws IOException {
144+
var expected = listOf(
145+
"abc",
146+
123,
147+
true,
148+
listOf(1, 2.0, 3.0),
149+
mapOf(entry("x", 1), entry("y", 2.0), entry("z", 3.0))
150+
);
151+
152+
var jsonEncoder = new JSONEncoder();
153+
154+
var writer = new StringWriter();
155+
156+
jsonEncoder.write(expected, writer);
157+
158+
var jsonDecoder = new JSONDecoder();
159+
160+
var actual = listOf(jsonDecoder.readAll(new StringReader(writer.toString())));
161+
162+
assertEquals(expected, actual);
129163
}
130164

131-
private static Object decode(String text, Supplier<JSONDecoder> factory) throws IOException {
132-
var jsonDecoder = factory.get();
165+
@Test
166+
public void testReadAllUnterminated() {
167+
var jsonDecoder = new JSONDecoder();
133168

134-
return jsonDecoder.read(new StringReader(text));
169+
assertThrows(IOException.class, () -> listOf(jsonDecoder.readAll(new StringReader("[1, 2, 3"))));
170+
assertThrows(IOException.class, () -> listOf(jsonDecoder.readAll(new StringReader("[1, 2, 3, "))));
135171
}
136172
}

kilo-test/src/main/java/org/httprpc/kilo/test/BulkUploadService.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,29 @@
1818
import org.httprpc.kilo.RequestMethod;
1919
import org.httprpc.kilo.ResourcePath;
2020
import org.httprpc.kilo.beans.BeanAdapter;
21+
import org.httprpc.kilo.io.JSONDecoder;
2122
import org.httprpc.kilo.sql.QueryBuilder;
2223

24+
import java.io.IOException;
2325
import java.sql.SQLException;
24-
import java.util.List;
26+
27+
import static org.httprpc.kilo.util.Iterables.*;
2528

2629
@WebServlet(urlPatterns = {"/bulk-upload/*"}, loadOnStartup = 1)
2730
public class BulkUploadService extends AbstractDatabaseService {
2831
private static final int BATCH_SIZE = 5000;
2932

3033
@RequestMethod("POST")
31-
public void upload(List<Row> rows) throws SQLException {
34+
public void upload(Void body) throws IOException, SQLException {
3235
var queryBuilder = new QueryBuilder();
3336

3437
queryBuilder.appendLine("insert into bulk_upload_test (text1, text2, number1, number2, number3)");
3538
queryBuilder.appendLine("values (:text1, :text2, :number1, :number2, :number3)");
3639

40+
var jsonDecoder = new JSONDecoder();
41+
42+
var rows = mapAll(jsonDecoder.readAll(getRequest().getReader()), BeanAdapter.toType(Row.class));
43+
3744
try (var statement = queryBuilder.prepare(getConnection())) {
3845
for (var row : rows) {
3946
queryBuilder.executeUpdate(statement, new BeanAdapter(row));
@@ -43,12 +50,16 @@ public void upload(List<Row> rows) throws SQLException {
4350

4451
@RequestMethod("POST")
4552
@ResourcePath("batch")
46-
public void uploadBatch(List<Row> rows) throws SQLException {
53+
public void uploadBatch(Void body) throws IOException, SQLException {
4754
var queryBuilder = new QueryBuilder();
4855

4956
queryBuilder.appendLine("insert into bulk_upload_test (text1, text2, number1, number2, number3)");
5057
queryBuilder.appendLine("values (:text1, :text2, :number1, :number2, :number3)");
5158

59+
var jsonDecoder = new JSONDecoder();
60+
61+
var rows = mapAll(jsonDecoder.readAll(getRequest().getReader()), BeanAdapter.toType(Row.class));
62+
5263
try (var statement = queryBuilder.prepare(getConnection())) {
5364
var i = 0;
5465

kilo-test/src/main/java/org/httprpc/kilo/test/PetService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public List<Pet> getPets(@Required String owner) throws SQLException {
4949

5050
@RequestMethod("GET")
5151
@ResourcePath("stream")
52-
public void getPetsStream(@Required String owner) throws SQLException, IOException {
52+
public void getPetsStream(@Required String owner) throws IOException, SQLException {
5353
var response = getResponse();
5454

5555
var accept = getRequest().getHeader("Accept");

kilo-test/src/test/java/org/httprpc/kilo/test/EmployeeServiceTest.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
package org.httprpc.kilo.test;
1616

1717
import org.httprpc.kilo.WebServiceProxy;
18+
import org.httprpc.kilo.beans.BeanAdapter;
19+
import org.httprpc.kilo.io.JSONDecoder;
1820
import org.junit.jupiter.api.Test;
1921

2022
import java.io.IOException;
2123
import java.net.URI;
2224

25+
import static org.httprpc.kilo.util.Iterables.*;
2326
import static org.junit.jupiter.api.Assertions.*;
2427

2528
public class EmployeeServiceTest {
@@ -43,7 +46,21 @@ public void testEmployees() throws IOException {
4346
private static void loadEmployees(String path) throws IOException {
4447
var webServiceProxy = new WebServiceProxy("GET", baseURI.resolve(path));
4548

46-
webServiceProxy.invoke();
49+
webServiceProxy.setResponseHandler((inputStream, contentType) -> {
50+
var jsonDecoder = new JSONDecoder();
51+
52+
return jsonDecoder.readAll(inputStream);
53+
});
54+
55+
var n = 0;
56+
57+
for (var employee : mapAll((Iterable<?>)webServiceProxy.invoke(), BeanAdapter.toType(Employee.class))) {
58+
assertNotNull(employee.getEmployeeNumber());
59+
60+
n++;
61+
}
62+
63+
assertEquals(300024, n);
4764
}
4865

4966
@Test

0 commit comments

Comments
 (0)