forked from elastic/elasticsearch-java
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJsonpUtils.java
More file actions
542 lines (469 loc) · 20.1 KB
/
Copy pathJsonpUtils.java
File metadata and controls
542 lines (469 loc) · 20.1 KB
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package co.elastic.clients.json;
import co.elastic.clients.json.jackson.JacksonJsonProvider;
import co.elastic.clients.util.AllowForbiddenApis;
import co.elastic.clients.util.ApiTypeHelper;
import jakarta.json.JsonException;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import jakarta.json.spi.JsonProvider;
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonLocation;
import jakarta.json.stream.JsonParser;
import jakarta.json.stream.JsonParser.Event;
import jakarta.json.stream.JsonParsingException;
import javax.annotation.Nullable;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.AbstractMap;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
public class JsonpUtils {
private static JsonProvider systemJsonProvider = null;
private static JsonProvider defaultJsonProvider = null;
/**
* Get a <code>JsonProvider</code> instance. This method first calls the standard `JsonProvider.provider()` that is based on
* the current thread's context classloader, and in case of failure tries to find a provider in other classloaders. The
* value is cached for subsequent calls.
*/
public static JsonProvider provider() {
JsonProvider result = defaultJsonProvider;
if (result == null) {
result = findProvider();
defaultJsonProvider = result;
}
return result;
}
/**
* Sets the <code>JsonProvider</code> that will be returned by {@link JsonProvider}.
*/
public static void setProvider(JsonProvider provider) {
defaultJsonProvider = provider;
}
static JsonProvider findProvider() {
try {
// Default to Jackson
Class.forName("com.fasterxml.jackson.databind.ObjectMapper");
return new JacksonJsonProvider();
} catch (ClassNotFoundException e) {
return findSystemProvider();
}
}
/**
* Get the system's <code>JsonProvider</code> instance return by {@code ServiceLoader}. First calls the standard
* `JsonProvider.provider()` that is based on the current thread's context classloader, and in case of failure tries to
* find a provider in other classloaders. The value is cached for subsequent calls.
*/
public static JsonProvider systemProvider() {
JsonProvider result = systemJsonProvider;
if (result == null) {
result = findSystemProvider();
systemJsonProvider = result;
}
return result;
}
@AllowForbiddenApis("Implementation of the JsonProvider lookup")
static JsonProvider findSystemProvider() {
RuntimeException exception;
try {
return JsonProvider.provider();
} catch(RuntimeException re) {
exception = re;
}
// Not found from the thread's context classloader. Try from our own classloader which should be a descendant of an app-server
// classloader if any, and if it still fails try from the SPI class which hopefully will be close to the implementation.
try {
return ServiceLoader.load(JsonProvider.class, JsonpUtils.class.getClassLoader()).iterator().next();
} catch(Exception e) {
// ignore
}
try {
return ServiceLoader.load(JsonProvider.class, JsonProvider.class.getClassLoader()).iterator().next();
} catch(Exception e) {
// ignore
}
throw new JsonException("Unable to get a JsonProvider. Check your classpath or thread context classloader.", exception);
}
/**
* Advances the parser to the next event and checks that this even is the expected one.
*
* @return the expected event
*
* @throws jakarta.json.JsonException if an i/o error occurs (IOException would be cause of JsonException)
* @throws JsonParsingException if the event is not the expected one, or if the parser encounters invalid
* JSON when advancing to next state.
* @throws java.util.NoSuchElementException if there are no more parsing states.
*/
public static JsonParser.Event expectNextEvent(JsonParser parser, JsonParser.Event expected) {
JsonParser.Event event = parser.next();
expectEvent(parser, expected, event);
return event;
}
public static void expectEvent(JsonParser parser, JsonParser.Event expected, JsonParser.Event event) {
if (event != expected) {
throw new UnexpectedJsonEventException(parser, event, expected);
}
}
public static String expectKeyName(JsonParser parser, JsonParser.Event event) {
JsonpUtils.expectEvent(parser, JsonParser.Event.KEY_NAME, event);
return parser.getString();
}
public static void ensureAccepts(JsonpDeserializer<?> deserializer, JsonParser parser, JsonParser.Event event) {
if (!deserializer.acceptedEvents().contains(event)) {
throw new UnexpectedJsonEventException(parser, event, deserializer.acceptedEvents());
}
}
public static void ensureCustomVariantsAllowed(JsonParser parser, JsonpMapper mapper) {
if (mapper.attribute(JsonpMapperFeatures.FORBID_CUSTOM_VARIANTS, false)) {
throw new JsonpMappingException("Json mapper configuration forbids custom variants", parser.getLocation());
}
}
/**
* Skip the value at the next position of the parser.
*/
public static void skipValue(JsonParser parser) {
skipValue(parser, parser.next());
}
/**
* Skip the value at the current position of the parser.
*/
public static void skipValue(JsonParser parser, Event event) {
switch(event) {
case START_OBJECT:
parser.skipObject();
break;
case START_ARRAY:
parser.skipArray();
break;
default:
// Not a structure, no additional skipping needed
break;
}
}
/**
* Copy the JSON value at the current parser location to a JSON generator.
*/
public static void copy(JsonParser parser, JsonGenerator generator) {
copy(parser, generator, parser.next());
}
/**
* Copy the JSON value at the current parser location to a JSON generator.
*/
public static void copy(JsonParser parser, JsonGenerator generator, JsonParser.Event event) {
switch (event) {
case START_OBJECT:
generator.writeStartObject();
while ((event = parser.next()) != Event.END_OBJECT) {
expectEvent(parser, Event.KEY_NAME, event);
generator.writeKey(parser.getString());
copy(parser, generator, parser.next());
}
generator.writeEnd();
break;
case START_ARRAY:
generator.writeStartArray();
while ((event = parser.next()) != Event.END_ARRAY) {
copy(parser, generator, event);
}
generator.writeEnd();
break;
case VALUE_STRING:
generator.write(parser.getString());
break;
case VALUE_FALSE:
generator.write(false);
break;
case VALUE_TRUE:
generator.write(true);
break;
case VALUE_NULL:
generator.writeNull();
break;
case VALUE_NUMBER:
if (parser.isIntegralNumber()) {
generator.write(parser.getLong());
} else {
generator.write(parser.getBigDecimal());
}
break;
default:
throw new UnexpectedJsonEventException(parser, event);
}
}
public static <T> void serialize(T value, JsonGenerator generator, @Nullable JsonpSerializer<T> serializer, JsonpMapper mapper) {
if (serializer != null) {
serializer.serialize(value, generator, mapper);
} else if (value instanceof JsonpSerializable) {
((JsonpSerializable) value).serialize(generator, mapper);
} else {
mapper.serialize(value, generator);
}
}
/**
* Looks ahead a field value in the Json object from the upcoming object in a parser, which should be on the
* START_OBJECT event.
* <p>
* Returns a pair containing that value and a parser that should be used to actually parse the object
* (the object has been consumed from the original one).
*/
public static Map.Entry<String, JsonParser> lookAheadFieldValue(
String name, String defaultValue, JsonParser parser, JsonpMapper mapper
) {
JsonLocation location = parser.getLocation();
if (parser instanceof LookAheadJsonParser) {
// Fast buffered path
Map.Entry<String, JsonParser> result = ((LookAheadJsonParser) parser).lookAheadFieldValue(name, defaultValue);
if (result.getKey() == null) {
throw new JsonpMappingException("Property '" + name + "' not found", location);
}
return result;
} else {
// Unbuffered path: parse the object into a JsonObject, then extract the value and parse it again
JsonObject object = parser.getObject();
String result = null;
JsonValue value = object.get(name);
// Handle enums and booleans promoted to enums
if (value != null) {
if (value.getValueType() == JsonValue.ValueType.STRING) {
result = ((JsonString) value).getString();
} else if (value.getValueType() == JsonValue.ValueType.TRUE) {
result = "true";
} else if (value.getValueType() == JsonValue.ValueType.FALSE) {
result = "false";
}
}
if (result == null) {
result = defaultValue;
}
if (result == null) {
result = defaultValue;
}
if (result == null) {
throw new JsonpMappingException("Property '" + name + "' not found", location);
}
JsonParser newParser = jsonValueParser(object, mapper);
// Pin location to the start of the look ahead, as the new parser will return locations in its own buffer
newParser = new DelegatingJsonParser(newParser) {
@Override
public JsonLocation getLocation() {
return new JsonLocationImpl(location.getLineNumber(), location.getColumnNumber(), location.getStreamOffset()) {
@Override
public String toString() {
return "(in object at " + super.toString().substring(1);
}
};
}
};
return new AbstractMap.SimpleImmutableEntry<>(result, newParser);
}
}
/**
* In union types, find the variant to be used by looking up property names in the JSON stream until we find one that
* uniquely identifies the variant.
*
* @param <Variant> the type of variant descriptors used by the caller.
* @param variants a map of variant descriptors, keyed by the property name that uniquely identifies the variant.
* @return a pair containing the variant descriptor (or {@code null} if not found), and a parser to be used to read the JSON object.
*/
public static <Variant> Map.Entry<Variant, JsonParser> findVariant(
Map<String, Variant> variants, JsonParser parser, JsonpMapper mapper
) {
if (parser instanceof LookAheadJsonParser) {
return ((LookAheadJsonParser) parser).findVariant(variants);
} else {
// If it's an object, find matching field names
Variant variant = null;
JsonValue value = parser.getValue();
if (value instanceof JsonObject) {
for (String field: value.asJsonObject().keySet()) {
variant = variants.get(field);
if (variant != null) {
break;
}
}
}
// Traverse the object we have inspected
parser = JsonpUtils.jsonValueParser(value, mapper);
return new AbstractMap.SimpleImmutableEntry<>(variant, parser);
}
}
/**
* Create a parser that traverses a JSON object
*
* @deprecated use {@link #jsonValueParser(JsonValue, JsonpMapper)}
*/
@Deprecated
public static JsonParser objectParser(JsonObject object, JsonpMapper mapper) {
return jsonValueParser(object, mapper);
}
/**
* Create a parser that traverses a JSON value
*/
public static JsonParser jsonValueParser(JsonValue value, JsonpMapper mapper) {
// FIXME: we should have used createParser(object), but this doesn't work as it creates a
// org.glassfish.json.JsonStructureParser that doesn't implement the JsonP 1.0.1 features, in particular
// parser.getObject(). So deserializing recursive internally-tagged union would fail with UnsupportedOperationException
// While glassfish has this issue or until we write our own, we roundtrip through a string.
String strObject = value.toString();
return mapper.jsonProvider().createParser(new StringReader(strObject));
}
public static String toString(JsonValue value) {
switch(value.getValueType()) {
case OBJECT:
throw new IllegalArgumentException("Json objects cannot be used as string");
case ARRAY:
return value.asJsonArray().stream()
.map(JsonpUtils::toString)
.collect(Collectors.joining(","));
case STRING:
return ((JsonString)value).getString();
case TRUE:
return "true";
case FALSE:
return "false";
case NULL:
return "null";
case NUMBER:
return value.toString();
default:
throw new IllegalArgumentException("Unknown JSON value type: '" + value + "'");
}
}
public static void serializeDoubleOrNull(JsonGenerator generator, double value, double defaultValue) {
if (!Double.isFinite(value)) {
generator.writeNull();
} else {
generator.write(value);
}
}
public static void serializeIntOrNull(JsonGenerator generator, int value, int defaultValue) {
if (value == defaultValue && (defaultValue == Integer.MAX_VALUE || defaultValue == Integer.MIN_VALUE)) {
generator.writeNull();
} else {
generator.write(value);
}
}
/**
* Renders a <code>JsonpSerializable</code> as a string by serializing it to JSON, prefixed by the class name. Any object of an
* application-specific class in the object graph is rendered using that object's <code>toString()</code> representation as a JSON
* string value.
* <p>
* The size of the string is limited to {@link #maxToStringLength()}.
*
* @see #maxToStringLength()
*/
public static String toString(JsonpSerializable value) {
StringBuilder sb = new StringBuilder(value.getClass().getSimpleName()).append(": ");
return toString(value, ToStringMapper.INSTANCE, sb).toString();
}
/**
* Set the maximum length of the JSON representation of a <code>JsonpSerializable</code> in the result of its <code>toString()</code>
* method. The default is 10k characters.
*/
public static void maxToStringLength(int length) {
MAX_TO_STRING_LENGTH = length;
}
/**
* Get the maximum length of the JSON representation of a <code>JsonpSerializable</code> in the result of its <code>toString()</code>
* method. The default is 10k characters.
*/
public static int maxToStringLength() {
return MAX_TO_STRING_LENGTH;
}
/**
* @deprecated use {@link #maxToStringLength(int)}
*/
@Deprecated
public static int MAX_TO_STRING_LENGTH = 10000;
private static class ToStringTooLongException extends RuntimeException {
}
/**
* Renders a <code>JsonpSerializable</code> as a string in a destination <code>StringBuilder</code>by serializing it to JSON.
* <p>
* The size of the string is limited to {@link #maxToStringLength()}.
*
* @return the <code>dest</code> parameter, for chaining.
* @see #toString(JsonpSerializable)
* @see #maxToStringLength()
*/
public static StringBuilder toString(JsonpSerializable value, JsonpMapper mapper, StringBuilder dest) {
Writer writer = new Writer() {
int length = 0;
@Override
public void write(char[] cbuf, int off, int len) {
int max = maxToStringLength();
length += len;
if (length > max) {
dest.append(cbuf, off, len - (length - max));
dest.append("...");
throw new ToStringTooLongException();
} else {
dest.append(cbuf, off, len);
}
}
@Override
public void flush() {
}
@Override
public void close() {
}
};
try(JsonGenerator rawGenerator = mapper.jsonProvider().createGenerator(writer)) {
// When the missing-properties workaround is active, wrap the generator so that null
// reference values are serialized as JSON null instead of throwing NPE. See #557.
JsonGenerator generator = ApiTypeHelper.requiredPropertiesCheckDisabled()
? new NullSafeJsonGenerator(rawGenerator)
: rawGenerator;
value.serialize(generator, mapper);
} catch (ToStringTooLongException e) {
// Ignore
}
return dest;
}
public static String toJsonString(Object value, JsonpMapper mapper) {
StringWriter writer = new StringWriter();
JsonGenerator rawGenerator = mapper.jsonProvider().createGenerator(writer);
// When the missing-properties workaround is active, wrap the generator so that null
// reference values are serialized as JSON null instead of throwing NPE. See #557.
JsonGenerator generator = ApiTypeHelper.requiredPropertiesCheckDisabled()
? new NullSafeJsonGenerator(rawGenerator)
: rawGenerator;
mapper.serialize(value, generator);
generator.close();
return writer.toString();
}
/**
* Renders a <code>JsonpSerializable</code> as a string in a destination <code>StringBuilder</code>by serializing it to JSON.
* Any object of an application-specific class in the object graph is rendered using that object's <code>toString()</code>
* representation as a JSON string value.
* <p>
* The size of the string is limited to {@link #maxToStringLength()}.
*
* @return the <code>dest</code> parameter, for chaining.
* @see #toString(JsonpSerializable)
* @see #maxToStringLength()
*/
public static StringBuilder toString(JsonpSerializable value, StringBuilder dest) {
return toString(value, ToStringMapper.INSTANCE, dest);
}
}