Skip to content

Commit 4f84c02

Browse files
committed
Decouple more classes from XContentBuilder and make builder strict (#29197)
This commit decouples `BytesRef`, `Releaseable`, and `TimeValue` from XContentBuilder, and paves the way for doupling `ByteSizeValue` as well. It moves much of the Lucene and Joda encoding into a new SPI extension that is loaded by XContentBuilder to know how to encode these values. Part of doing this also allows us to make JSON encoding strict, as we no longer allow just any old object to be passed (in the past it was possible to get json that was `"field": "java.lang.Object@d8355a8"` if no one was careful about what was passed in). Relates to #28504
1 parent 683bb1c commit 4f84c02

File tree

18 files changed

+171
-65
lines changed

18 files changed

+171
-65
lines changed

server/src/main/java/org/elasticsearch/Version.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.elasticsearch.common.io.stream.StreamInput;
2626
import org.elasticsearch.common.io.stream.StreamOutput;
2727
import org.elasticsearch.common.settings.Settings;
28+
import org.elasticsearch.common.xcontent.ToXContentFragment;
29+
import org.elasticsearch.common.xcontent.XContentBuilder;
2830
import org.elasticsearch.monitor.jvm.JvmInfo;
2931

3032
import java.io.IOException;
@@ -34,7 +36,7 @@
3436
import java.util.Collections;
3537
import java.util.List;
3638

37-
public class Version implements Comparable<Version> {
39+
public class Version implements Comparable<Version>, ToXContentFragment {
3840
/*
3941
* The logic for ID is: XXYYZZAA, where XX is major version, YY is minor version, ZZ is revision, and AA is alpha/beta/rc indicator AA
4042
* values below 25 are for alpha builder (since 5.0), and above 25 and below 50 are beta builds, and below 99 are RC builds, with 99
@@ -409,6 +411,11 @@ public int compareTo(Version other) {
409411
return Integer.compare(this.id, other.id);
410412
}
411413

414+
@Override
415+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
416+
return builder.value(toString());
417+
}
418+
412419
/*
413420
* We need the declared versions when computing the minimum compatibility version. As computing the declared versions uses reflection it
414421
* is not cheap. Since computing the minimum compatibility version can occur often, we use this holder to compute the declared versions

server/src/main/java/org/elasticsearch/common/bytes/BytesReference.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.lucene.util.BytesRefIterator;
2424
import org.elasticsearch.common.io.stream.BytesStream;
2525
import org.elasticsearch.common.io.stream.StreamInput;
26+
import org.elasticsearch.common.xcontent.ToXContentFragment;
2627
import org.elasticsearch.common.xcontent.XContentBuilder;
2728

2829
import java.io.ByteArrayOutputStream;
@@ -35,7 +36,7 @@
3536
/**
3637
* A reference to bytes.
3738
*/
38-
public abstract class BytesReference implements Accountable, Comparable<BytesReference> {
39+
public abstract class BytesReference implements Accountable, Comparable<BytesReference>, ToXContentFragment {
3940

4041
private Integer hash = null; // we cache the hash of this reference since it can be quite costly to re-calculated it
4142

@@ -301,4 +302,10 @@ public long skip(long n) throws IOException {
301302
return input.skip(n);
302303
}
303304
}
305+
306+
@Override
307+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
308+
BytesRef bytes = toBytesRef();
309+
return builder.value(bytes.bytes, bytes.offset, bytes.length);
310+
}
304311
}

server/src/main/java/org/elasticsearch/common/document/DocumentField.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.common.document;
2121

22+
import org.apache.lucene.util.BytesRef;
2223
import org.elasticsearch.common.bytes.BytesReference;
2324
import org.elasticsearch.common.io.stream.StreamInput;
2425
import org.elasticsearch.common.io.stream.StreamOutput;
@@ -127,11 +128,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
127128
// Stored fields values are converted using MappedFieldType#valueForDisplay.
128129
// As a result they can either be Strings, Numbers, or Booleans, that's
129130
// all.
130-
if (value instanceof BytesReference) {
131-
builder.binaryValue(((BytesReference) value).toBytesRef());
132-
} else {
133-
builder.value(value);
134-
}
131+
builder.value(value);
135132
}
136133
builder.endArray();
137134
return builder;

server/src/main/java/org/elasticsearch/common/text/Text.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.elasticsearch.common.text;
2020

21+
import org.apache.lucene.util.BytesRef;
2122
import org.elasticsearch.common.bytes.BytesArray;
2223
import org.elasticsearch.common.bytes.BytesReference;
2324
import org.elasticsearch.common.xcontent.ToXContent;
@@ -125,7 +126,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
125126
} else {
126127
// TODO: TextBytesOptimization we can use a buffer here to convert it? maybe add a
127128
// request to jackson to support InputStream as well?
128-
return builder.utf8Value(this.bytes().toBytesRef());
129+
BytesRef br = this.bytes().toBytesRef();
130+
return builder.utf8Value(br.bytes, br.offset, br.length);
129131
}
130132
}
131133
}

server/src/main/java/org/elasticsearch/common/transport/TransportAddress.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import org.elasticsearch.common.io.stream.StreamOutput;
2626
import org.elasticsearch.common.io.stream.Writeable;
2727
import org.elasticsearch.common.network.NetworkAddress;
28+
import org.elasticsearch.common.xcontent.ToXContent;
29+
import org.elasticsearch.common.xcontent.ToXContentFragment;
30+
import org.elasticsearch.common.xcontent.XContentBuilder;
2831

2932
import java.io.IOException;
3033
import java.net.InetAddress;
@@ -34,7 +37,7 @@
3437
/**
3538
* A transport address used for IP socket address (wraps {@link java.net.InetSocketAddress}).
3639
*/
37-
public final class TransportAddress implements Writeable {
40+
public final class TransportAddress implements Writeable, ToXContentFragment {
3841

3942
/**
4043
* A <a href="https://en.wikipedia.org/wiki/0.0.0.0">non-routeable v4 meta transport address</a> that can be used for
@@ -155,4 +158,9 @@ public int hashCode() {
155158
public String toString() {
156159
return NetworkAddress.format(address);
157160
}
161+
162+
@Override
163+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
164+
return builder.value(toString());
165+
}
158166
}

server/src/main/java/org/elasticsearch/common/unit/ByteSizeValue.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@
2727
import org.elasticsearch.common.io.stream.Writeable;
2828
import org.elasticsearch.common.logging.DeprecationLogger;
2929
import org.elasticsearch.common.logging.Loggers;
30+
import org.elasticsearch.common.xcontent.ToXContent;
31+
import org.elasticsearch.common.xcontent.ToXContentFragment;
32+
import org.elasticsearch.common.xcontent.XContentBuilder;
3033

3134
import java.io.IOException;
3235
import java.util.Locale;
3336
import java.util.Objects;
3437

35-
public class ByteSizeValue implements Writeable, Comparable<ByteSizeValue> {
38+
public class ByteSizeValue implements Writeable, Comparable<ByteSizeValue>, ToXContentFragment {
3639
private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(Loggers.getLogger(ByteSizeValue.class));
3740

3841
private final long size;
@@ -271,4 +274,9 @@ public int compareTo(ByteSizeValue other) {
271274
long otherValue = other.size * other.unit.toBytes(1);
272275
return Long.compare(thisValue, otherValue);
273276
}
277+
278+
@Override
279+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
280+
return builder.value(toString());
281+
}
274282
}

server/src/main/java/org/elasticsearch/common/unit/TimeValue.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import org.elasticsearch.common.io.stream.StreamInput;
2525
import org.elasticsearch.common.io.stream.StreamOutput;
2626
import org.elasticsearch.common.io.stream.Writeable;
27+
import org.elasticsearch.common.xcontent.ToXContent;
28+
import org.elasticsearch.common.xcontent.ToXContentFragment;
29+
import org.elasticsearch.common.xcontent.XContentBuilder;
2730
import org.joda.time.Period;
2831
import org.joda.time.PeriodType;
2932
import org.joda.time.format.PeriodFormat;
@@ -40,7 +43,7 @@
4043
import java.util.Set;
4144
import java.util.concurrent.TimeUnit;
4245

43-
public class TimeValue implements Writeable, Comparable<TimeValue> {
46+
public class TimeValue implements Writeable, Comparable<TimeValue>, ToXContentFragment {
4447

4548
/** How many nano-seconds in one milli-second */
4649
public static final long NSEC_PER_MSEC = TimeUnit.NANOSECONDS.convert(1, TimeUnit.MILLISECONDS);
@@ -398,4 +401,9 @@ public int compareTo(TimeValue timeValue) {
398401
double otherValue = ((double) timeValue.duration) * timeValue.timeUnit.toNanos(1);
399402
return Double.compare(thisValue, otherValue);
400403
}
404+
405+
@Override
406+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
407+
return builder.value(toString());
408+
}
401409
}

server/src/main/java/org/elasticsearch/common/xcontent/XContentBuilder.java

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,21 @@
1919

2020
package org.elasticsearch.common.xcontent;
2121

22-
import org.apache.lucene.util.BytesRef;
23-
import org.elasticsearch.common.lease.Releasable;
2422
import org.elasticsearch.common.unit.ByteSizeValue;
25-
import org.elasticsearch.common.unit.TimeValue;
2623
import org.elasticsearch.common.util.CollectionUtils;
2724
import org.joda.time.DateTimeZone;
2825
import org.joda.time.ReadableInstant;
2926
import org.joda.time.format.DateTimeFormatter;
3027
import org.joda.time.format.ISODateTimeFormat;
3128

3229
import java.io.ByteArrayOutputStream;
30+
import java.io.Closeable;
3331
import java.io.Flushable;
3432
import java.io.IOException;
3533
import java.io.InputStream;
3634
import java.io.OutputStream;
3735
import java.nio.file.Path;
36+
import java.time.ZonedDateTime;
3837
import java.util.Arrays;
3938
import java.util.Calendar;
4039
import java.util.Collections;
@@ -49,7 +48,7 @@
4948
/**
5049
* A utility to build XContent (ie json).
5150
*/
52-
public final class XContentBuilder implements Releasable, Flushable {
51+
public final class XContentBuilder implements Closeable, Flushable {
5352

5453
/**
5554
* Create a new {@link XContentBuilder} using the given {@link XContent} content.
@@ -91,7 +90,6 @@ public static XContentBuilder builder(XContent xContent, Set<String> includes, S
9190
writers.put(Boolean.class, (b, v) -> b.value((Boolean) v));
9291
writers.put(Byte.class, (b, v) -> b.value((Byte) v));
9392
writers.put(byte[].class, (b, v) -> b.value((byte[]) v));
94-
writers.put(BytesRef.class, (b, v) -> b.binaryValue((BytesRef) v));
9593
writers.put(Date.class, (b, v) -> b.value((Date) v));
9694
writers.put(Double.class, (b, v) -> b.value((Double) v));
9795
writers.put(double[].class, (b, v) -> b.values((double[]) v));
@@ -105,12 +103,12 @@ public static XContentBuilder builder(XContent xContent, Set<String> includes, S
105103
writers.put(short[].class, (b, v) -> b.values((short[]) v));
106104
writers.put(String.class, (b, v) -> b.value((String) v));
107105
writers.put(String[].class, (b, v) -> b.values((String[]) v));
106+
writers.put(Locale.class, (b, v) -> b.value(v.toString()));
107+
writers.put(Class.class, (b, v) -> b.value(v.toString()));
108+
writers.put(ZonedDateTime.class, (b, v) -> b.value(v.toString()));
108109

109110

110111
Map<Class<?>, HumanReadableTransformer> humanReadableTransformer = new HashMap<>();
111-
// These will be moved to a different class at a later time to decouple them from XContentBuilder
112-
humanReadableTransformer.put(TimeValue.class, v -> ((TimeValue) v).millis());
113-
humanReadableTransformer.put(ByteSizeValue.class, v -> ((ByteSizeValue) v).getBytes());
114112

115113
// Load pluggable extensions
116114
for (XContentBuilderExtension service : ServiceLoader.load(XContentBuilderExtension.class)) {
@@ -613,49 +611,25 @@ public XContentBuilder value(byte[] value, int offset, int length) throws IOExce
613611
}
614612

615613
/**
616-
* Writes the binary content of the given {@link BytesRef}.
617-
*
618-
* Use {@link org.elasticsearch.common.xcontent.XContentParser#binaryValue()} to read the value back
619-
*/
620-
public XContentBuilder field(String name, BytesRef value) throws IOException {
621-
return field(name).binaryValue(value);
622-
}
623-
624-
/**
625-
* Writes the binary content of the given {@link BytesRef} as UTF-8 bytes.
614+
* Writes the binary content of the given byte array as UTF-8 bytes.
626615
*
627616
* Use {@link XContentParser#charBuffer()} to read the value back
628617
*/
629-
public XContentBuilder utf8Field(String name, BytesRef value) throws IOException {
630-
return field(name).utf8Value(value);
631-
}
632-
633-
/**
634-
* Writes the binary content of the given {@link BytesRef}.
635-
*
636-
* Use {@link org.elasticsearch.common.xcontent.XContentParser#binaryValue()} to read the value back
637-
*/
638-
public XContentBuilder binaryValue(BytesRef value) throws IOException {
639-
if (value == null) {
640-
return nullValue();
641-
}
642-
value(value.bytes, value.offset, value.length);
643-
return this;
618+
public XContentBuilder utf8Field(String name, byte[] bytes, int offset, int length) throws IOException {
619+
return field(name).utf8Value(bytes, offset, length);
644620
}
645621

646622
/**
647-
* Writes the binary content of the given {@link BytesRef} as UTF-8 bytes.
623+
* Writes the binary content of the given byte array as UTF-8 bytes.
648624
*
649625
* Use {@link XContentParser#charBuffer()} to read the value back
650626
*/
651-
public XContentBuilder utf8Value(BytesRef value) throws IOException {
652-
if (value == null) {
653-
return nullValue();
654-
}
655-
generator.writeUTF8String(value.bytes, value.offset, value.length);
627+
public XContentBuilder utf8Value(byte[] bytes, int offset, int length) throws IOException {
628+
generator.writeUTF8String(bytes, offset, length);
656629
return this;
657630
}
658631

632+
659633
////////////////////////////////////////////////////////////////////////////
660634
// Date
661635
//////////////////////////////////
@@ -793,10 +767,11 @@ private void unknownValue(Object value, boolean ensureNoSelfReferences) throws I
793767
value((ReadableInstant) value);
794768
} else if (value instanceof ToXContent) {
795769
value((ToXContent) value);
796-
} else {
797-
// This is a "value" object (like enum, DistanceUnit, etc) just toString() it
798-
// (yes, it can be misleading when toString a Java class, but really, jackson should be used in that case)
770+
} else if (value instanceof Enum<?>) {
771+
// Write out the Enum toString
799772
value(Objects.toString(value));
773+
} else {
774+
throw new IllegalArgumentException("cannot write xcontent for unknown value of type " + value.getClass());
800775
}
801776
}
802777

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common.xcontent;
21+
22+
import org.apache.lucene.util.BytesRef;
23+
import org.elasticsearch.common.bytes.BytesReference;
24+
import org.elasticsearch.common.unit.ByteSizeValue;
25+
import org.elasticsearch.common.unit.TimeValue;
26+
import org.joda.time.DateTimeZone;
27+
import org.joda.time.tz.CachedDateTimeZone;
28+
import org.joda.time.tz.FixedDateTimeZone;
29+
30+
import java.util.HashMap;
31+
import java.util.Map;
32+
import java.util.Objects;
33+
34+
/**
35+
* SPI extensions for Elasticsearch-specific classes (like the Lucene or Joda
36+
* dependency classes) that need to be encoded by {@link XContentBuilder} in a
37+
* specific way.
38+
*/
39+
public class XContentElasticsearchExtension implements XContentBuilderExtension {
40+
41+
@Override
42+
public Map<Class<?>, XContentBuilder.Writer> getXContentWriters() {
43+
Map<Class<?>, XContentBuilder.Writer> writers = new HashMap<>();
44+
45+
// Fully-qualified here to reduce ambiguity around our (ES') Version class
46+
writers.put(org.apache.lucene.util.Version.class, (b, v) -> b.value(Objects.toString(v)));
47+
writers.put(DateTimeZone.class, (b, v) -> b.value(Objects.toString(v)));
48+
writers.put(CachedDateTimeZone.class, (b, v) -> b.value(Objects.toString(v)));
49+
writers.put(FixedDateTimeZone.class, (b, v) -> b.value(Objects.toString(v)));
50+
51+
writers.put(BytesReference.class, (b, v) -> {
52+
if (v == null) {
53+
b.nullValue();
54+
} else {
55+
BytesRef bytes = ((BytesReference) v).toBytesRef();
56+
b.value(bytes.bytes, bytes.offset, bytes.length);
57+
}
58+
});
59+
60+
writers.put(BytesRef.class, (b, v) -> {
61+
if (v == null) {
62+
b.nullValue();
63+
} else {
64+
BytesRef bytes = (BytesRef) v;
65+
b.value(bytes.bytes, bytes.offset, bytes.length);
66+
}
67+
});
68+
return writers;
69+
}
70+
71+
@Override
72+
public Map<Class<?>, XContentBuilder.HumanReadableTransformer> getXContentHumanReadableTransformers() {
73+
Map<Class<?>, XContentBuilder.HumanReadableTransformer> transformers = new HashMap<>();
74+
transformers.put(TimeValue.class, v -> ((TimeValue) v).millis());
75+
transformers.put(ByteSizeValue.class, v -> ((ByteSizeValue) v).getBytes());
76+
return transformers;
77+
}
78+
}

0 commit comments

Comments
 (0)