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
|
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.search.handler;
import ai.vespa.json.Jackson;
import com.fasterxml.jackson.core.JsonFactoryBuilder;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.processing.IllegalInputException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* Parser that consumes json and creates a single level key value map by dotting nested objects.
* This is specially tailored for the json query input coming as post.
* It does the cheapest possible json parsing delaying number parsing to where it is needed and avoids dreaded toString()
* of complicated json object trees.
*
* @author baldersheim
*/
class Json2SingleLevelMap {
private static final ObjectMapper jsonMapper = createMapper();
private final byte [] buf;
private final JsonParser parser;
Json2SingleLevelMap(InputStream data) {
try {
buf = data.readAllBytes();
parser = jsonMapper.createParser(buf);
} catch (IOException e) {
throw new RuntimeException("Problem reading POSTed data", e);
}
}
private static ObjectMapper createMapper() {
return Jackson.createMapper(new JsonFactoryBuilder()
.streamReadConstraints(StreamReadConstraints.builder().maxStringLength(Integer.MAX_VALUE).build())
.configure(JsonReadFeature.ALLOW_SINGLE_QUOTES, true));
}
Map<String, String> parse() {
try {
Map<String, String> map = new HashMap<>();
if (parser.nextToken() != JsonToken.START_OBJECT) {
throw new IllegalInputException("Expected start of object, got '" + parser.currentToken() + "'");
}
parse(map, "");
return map;
} catch (JsonParseException e) {
throw new IllegalInputException("Json parse error.", e);
} catch (IOException e) {
throw new RuntimeException("Problem reading POSTed data", e);
}
}
void parse(Map<String, String> map, String parent) throws IOException {
for (parser.nextToken(); parser.getCurrentToken() != JsonToken.END_OBJECT; parser.nextToken()) {
String fieldName = parent + parser.getCurrentName();
JsonToken token = parser.nextToken();
if ((token == JsonToken.VALUE_STRING) ||
(token == JsonToken.VALUE_NUMBER_FLOAT) ||
(token == JsonToken.VALUE_NUMBER_INT) ||
(token == JsonToken.VALUE_TRUE) ||
(token == JsonToken.VALUE_FALSE) ||
(token == JsonToken.VALUE_NULL)) {
map.put(fieldName, parser.getText());
} else if (token == JsonToken.START_ARRAY) {
map.put(fieldName, skipChildren(parser, buf));
} else if (token == JsonToken.START_OBJECT) {
if (fieldName.startsWith("input.") || fieldName.equals("select.where") || fieldName.equals("select.grouping")) {
map.put(fieldName, skipChildren(parser, buf));
} else {
parse(map, fieldName + ".");
}
} else {
throw new IllegalInputException("In field '" + fieldName + "', got unknown json token '" + token.asString() + "'");
}
}
}
private String skipChildren(JsonParser parser, byte [] input) throws IOException {
JsonLocation start = parser.getCurrentLocation();
parser.skipChildren();
JsonLocation end = parser.getCurrentLocation();
int offset = (int)start.getByteOffset() - 1;
return new String(input, offset, (int)(end.getByteOffset() - offset), StandardCharsets.UTF_8);
}
}
|