1313 */
1414package com .facebook .presto .operator .scalar ;
1515
16+ import com .facebook .airlift .json .JsonObjectMapperProvider ;
17+ import com .facebook .presto .common .function .SqlFunctionProperties ;
1618import com .facebook .presto .spi .PrestoException ;
1719import com .fasterxml .jackson .core .JsonFactory ;
1820import com .fasterxml .jackson .core .JsonGenerator ;
1921import com .fasterxml .jackson .core .JsonParseException ;
2022import com .fasterxml .jackson .core .JsonParser ;
2123import com .fasterxml .jackson .core .JsonToken ;
2224import com .fasterxml .jackson .core .io .SerializedString ;
25+ import com .fasterxml .jackson .databind .ObjectMapper ;
2326import com .google .common .collect .ImmutableList ;
2427import io .airlift .slice .DynamicSliceOutput ;
2528import io .airlift .slice .Slice ;
2629
2730import java .io .IOException ;
2831import java .io .InputStream ;
32+ import java .io .OutputStream ;
2933import java .io .UncheckedIOException ;
3034
3135import static com .facebook .presto .spi .StandardErrorCode .INVALID_FUNCTION_ARGUMENT ;
3842import static com .fasterxml .jackson .core .JsonToken .START_ARRAY ;
3943import static com .fasterxml .jackson .core .JsonToken .START_OBJECT ;
4044import static com .fasterxml .jackson .core .JsonToken .VALUE_NULL ;
45+ import static com .fasterxml .jackson .databind .SerializationFeature .ORDER_MAP_ENTRIES_BY_KEYS ;
4146import static io .airlift .slice .Slices .utf8Slice ;
4247import static java .util .Objects .requireNonNull ;
4348
@@ -121,13 +126,15 @@ public final class JsonExtract
121126 private static final JsonFactory JSON_FACTORY = new JsonFactory ()
122127 .disable (CANONICALIZE_FIELD_NAMES );
123128
129+ private static final ObjectMapper SORTED_MAPPER = new JsonObjectMapperProvider ().get ().configure (ORDER_MAP_ENTRIES_BY_KEYS , true );
130+
124131 private JsonExtract () {}
125132
126- public static <T > T extract (Slice jsonInput , JsonExtractor <T > jsonExtractor )
133+ public static <T > T extract (Slice jsonInput , JsonExtractor <T > jsonExtractor , SqlFunctionProperties properties )
127134 {
128135 requireNonNull (jsonInput , "jsonInput is null" );
129136 try {
130- return jsonExtractor .extract (jsonInput .getInput ());
137+ return jsonExtractor .extract (jsonInput .getInput (), properties );
131138 }
132139 catch (JsonParseException e ) {
133140 // Return null if we failed to parse something
@@ -156,7 +163,7 @@ public static <T> PrestoJsonExtractor<T> generateExtractor(String path, PrestoJs
156163
157164 public interface JsonExtractor <T >
158165 {
159- T extract (InputStream inputStream )
166+ T extract (InputStream inputStream , SqlFunctionProperties properties )
160167 throws IOException ;
161168 }
162169
@@ -174,11 +181,11 @@ public abstract static class PrestoJsonExtractor<T>
174181 *
175182 * @return the value, or null if not applicable
176183 */
177- abstract T extract (JsonParser jsonParser )
184+ abstract T extract (JsonParser jsonParser , SqlFunctionProperties properties )
178185 throws IOException ;
179186
180187 @ Override
181- public T extract (InputStream inputStream )
188+ public T extract (InputStream inputStream , SqlFunctionProperties properties )
182189 throws IOException
183190 {
184191 try (JsonParser jsonParser = createJsonParser (JSON_FACTORY , inputStream )) {
@@ -187,7 +194,7 @@ public T extract(InputStream inputStream)
187194 return null ;
188195 }
189196
190- return extract (jsonParser );
197+ return extract (jsonParser , properties );
191198 }
192199 }
193200 }
@@ -214,21 +221,21 @@ public ObjectFieldJsonExtractor(String fieldName, PrestoJsonExtractor<? extends
214221 }
215222
216223 @ Override
217- public T extract (JsonParser jsonParser )
224+ public T extract (JsonParser jsonParser , SqlFunctionProperties properties )
218225 throws IOException
219226 {
220227 if (jsonParser .getCurrentToken () == START_OBJECT ) {
221- return processJsonObject (jsonParser );
228+ return processJsonObject (jsonParser , properties );
222229 }
223230
224231 if (jsonParser .getCurrentToken () == START_ARRAY ) {
225- return processJsonArray (jsonParser );
232+ return processJsonArray (jsonParser , properties );
226233 }
227234
228235 throw new JsonParseException (jsonParser , "Expected a JSON object or array" );
229236 }
230237
231- public T processJsonObject (JsonParser jsonParser )
238+ public T processJsonObject (JsonParser jsonParser , SqlFunctionProperties properties )
232239 throws IOException
233240 {
234241 while (!jsonParser .nextFieldName (fieldName )) {
@@ -244,10 +251,10 @@ public T processJsonObject(JsonParser jsonParser)
244251
245252 jsonParser .nextToken (); // Shift to first token of the value
246253
247- return delegate .extract (jsonParser );
254+ return delegate .extract (jsonParser , properties );
248255 }
249256
250- public T processJsonArray (JsonParser jsonParser )
257+ public T processJsonArray (JsonParser jsonParser , SqlFunctionProperties properties )
251258 throws IOException
252259 {
253260 int currentIndex = 0 ;
@@ -270,15 +277,15 @@ public T processJsonArray(JsonParser jsonParser)
270277 jsonParser .skipChildren (); // Skip nested structure if currently at the start of one
271278 }
272279
273- return delegate .extract (jsonParser );
280+ return delegate .extract (jsonParser , properties );
274281 }
275282 }
276283
277284 public static class ScalarValueJsonExtractor
278285 extends PrestoJsonExtractor <Slice >
279286 {
280287 @ Override
281- public Slice extract (JsonParser jsonParser )
288+ public Slice extract (JsonParser jsonParser , SqlFunctionProperties properties )
282289 throws IOException
283290 {
284291 JsonToken token = jsonParser .getCurrentToken ();
@@ -296,13 +303,31 @@ public static class JsonValueJsonExtractor
296303 extends PrestoJsonExtractor <Slice >
297304 {
298305 @ Override
299- public Slice extract (JsonParser jsonParser )
306+ public Slice extract (JsonParser jsonParser , SqlFunctionProperties properties )
300307 throws IOException
301308 {
302309 if (!jsonParser .hasCurrentToken ()) {
303310 throw new JsonParseException (jsonParser , "Unexpected end of value" );
304311 }
312+ if (!properties .isCanonicalizedJsonExtract ()) {
313+ return legacyExtract (jsonParser );
314+ }
315+ DynamicSliceOutput dynamicSliceOutput = new DynamicSliceOutput (ESTIMATED_JSON_OUTPUT_SIZE );
316+ // Write the JSON to output stream with sorted keys
317+ SORTED_MAPPER .writeValue ((OutputStream ) dynamicSliceOutput , SORTED_MAPPER .readValue (jsonParser , Object .class ));
318+ // nextToken will throw an exception if there are trailing characters.
319+ try {
320+ jsonParser .nextToken ();
321+ }
322+ catch (JsonParseException e ) {
323+ throw new PrestoException (INVALID_FUNCTION_ARGUMENT , e .getMessage ());
324+ }
325+ return dynamicSliceOutput .slice ();
326+ }
305327
328+ public Slice legacyExtract (JsonParser jsonParser )
329+ throws IOException
330+ {
306331 DynamicSliceOutput dynamicSliceOutput = new DynamicSliceOutput (ESTIMATED_JSON_OUTPUT_SIZE );
307332 try (JsonGenerator jsonGenerator = createJsonGenerator (JSON_FACTORY , dynamicSliceOutput )) {
308333 jsonGenerator .copyCurrentStructure (jsonParser );
@@ -315,7 +340,7 @@ public static class JsonSizeExtractor
315340 extends PrestoJsonExtractor <Long >
316341 {
317342 @ Override
318- public Long extract (JsonParser jsonParser )
343+ public Long extract (JsonParser jsonParser , SqlFunctionProperties properties )
319344 throws IOException
320345 {
321346 if (!jsonParser .hasCurrentToken ()) {
0 commit comments