Skip to content

Commit cce830b

Browse files
Enable strict duplicate checks for all XContent types
With this commit we enable the Jackson feature 'STRICT_DUPLICATE_DETECTION' by default for all XContent types (not only JSON). We have also changed the name of the system property to disable this feature from `es.json.strict_duplicate_detection` to the now more appropriate name `es.xcontent.strict_duplicate_detection`. Relates elastic#19614 Relates elastic#22073
1 parent 0e18782 commit cce830b

File tree

19 files changed

+93
-59
lines changed

19 files changed

+93
-59
lines changed

core/src/main/java/org/elasticsearch/common/xcontent/XContent.java

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

2020
package org.elasticsearch.common.xcontent;
2121

22+
import org.elasticsearch.common.Booleans;
2223
import org.elasticsearch.common.bytes.BytesReference;
2324

2425
import java.io.IOException;
@@ -33,6 +34,27 @@
3334
*/
3435
public interface XContent {
3536

37+
/*
38+
* NOTE: This comment is only meant for maintainers of the Elasticsearch code base and is intentionally not a Javadoc comment as it
39+
* describes an undocumented system property.
40+
*
41+
*
42+
* Determines whether the XContent parser will always check for duplicate keys. This behavior is enabled by default but
43+
* can be disabled by setting the otherwise undocumented system property "es.xcontent.strict_duplicate_detection to "false".
44+
*
45+
* Before we've enabled this mode, we had custom duplicate checks in various parts of the code base. As the user can still disable this
46+
* mode and fall back to the legacy duplicate checks, we still need to keep the custom duplicate checks around and we also need to keep
47+
* the tests around.
48+
*
49+
* If this fallback via system property is removed one day in the future you can remove all tests that call this method and also remove
50+
* the corresponding custom duplicate check code.
51+
*
52+
*/
53+
static boolean isStrictDuplicateDetectionEnabled() {
54+
// Don't allow duplicate keys in JSON content by default but let the user opt out
55+
return Booleans.parseBooleanExact(System.getProperty("es.xcontent.strict_duplicate_detection", "true"));
56+
}
57+
3658
/**
3759
* The type this content handles and produces.
3860
*/

core/src/main/java/org/elasticsearch/common/xcontent/cbor/CborXContent.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.fasterxml.jackson.core.JsonEncoding;
2323
import com.fasterxml.jackson.core.JsonGenerator;
24+
import com.fasterxml.jackson.core.JsonParser;
2425
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
2526
import org.elasticsearch.ElasticsearchParseException;
2627
import org.elasticsearch.common.bytes.BytesReference;
@@ -54,6 +55,7 @@ public static XContentBuilder contentBuilder() throws IOException {
5455
cborFactory.configure(CBORFactory.Feature.FAIL_ON_SYMBOL_HASH_OVERFLOW, false); // this trips on many mappings now...
5556
// Do not automatically close unclosed objects/arrays in com.fasterxml.jackson.dataformat.cbor.CBORGenerator#close() method
5657
cborFactory.configure(JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT, false);
58+
cborFactory.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, XContent.isStrictDuplicateDetectionEnabled());
5759
cborXContent = new CborXContent();
5860
}
5961

core/src/main/java/org/elasticsearch/common/xcontent/json/JsonXContent.java

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.fasterxml.jackson.core.JsonFactory;
2424
import com.fasterxml.jackson.core.JsonGenerator;
2525
import com.fasterxml.jackson.core.JsonParser;
26-
import org.elasticsearch.common.Booleans;
2726
import org.elasticsearch.common.bytes.BytesReference;
2827
import org.elasticsearch.common.io.FastStringReader;
2928
import org.elasticsearch.common.xcontent.XContent;
@@ -50,35 +49,14 @@ public static XContentBuilder contentBuilder() throws IOException {
5049

5150
public static final JsonXContent jsonXContent;
5251

53-
/*
54-
* NOTE: This comment is only meant for maintainers of the Elasticsearch code base and is intentionally not a Javadoc comment as it
55-
* describes an undocumented system property.
56-
*
57-
*
58-
* Determines whether the JSON parser will always check for duplicate keys in JSON content. This behavior is enabled by default but
59-
* can be disabled by setting the otherwise undocumented system property "es.json.strict_duplicate_detection" to "false".
60-
*
61-
* Before we've enabled this mode, we had custom duplicate checks in various parts of the code base. As the user can still disable this
62-
* mode and fall back to the legacy duplicate checks, we still need to keep the custom duplicate checks around and we also need to keep
63-
* the tests around.
64-
*
65-
* If this fallback via system property is removed one day in the future you can remove all tests that call this method and also remove
66-
* the corresponding custom duplicate check code.
67-
*
68-
*/
69-
public static boolean isStrictDuplicateDetectionEnabled() {
70-
// Don't allow duplicate keys in JSON content by default but let the user opt out
71-
return Booleans.parseBooleanExact(System.getProperty("es.json.strict_duplicate_detection", "true"));
72-
}
73-
7452
static {
7553
jsonFactory = new JsonFactory();
7654
jsonFactory.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, true);
7755
jsonFactory.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
7856
jsonFactory.configure(JsonFactory.Feature.FAIL_ON_SYMBOL_HASH_OVERFLOW, false); // this trips on many mappings now...
7957
// Do not automatically close unclosed objects/arrays in com.fasterxml.jackson.core.json.UTF8JsonGenerator#close() method
8058
jsonFactory.configure(JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT, false);
81-
jsonFactory.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, isStrictDuplicateDetectionEnabled());
59+
jsonFactory.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, XContent.isStrictDuplicateDetectionEnabled());
8260
jsonXContent = new JsonXContent();
8361
}
8462

core/src/main/java/org/elasticsearch/common/xcontent/smile/SmileXContent.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.fasterxml.jackson.core.JsonEncoding;
2323
import com.fasterxml.jackson.core.JsonGenerator;
24+
import com.fasterxml.jackson.core.JsonParser;
2425
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
2526
import com.fasterxml.jackson.dataformat.smile.SmileGenerator;
2627
import org.elasticsearch.common.bytes.BytesReference;
@@ -55,6 +56,7 @@ public static XContentBuilder contentBuilder() throws IOException {
5556
smileFactory.configure(SmileFactory.Feature.FAIL_ON_SYMBOL_HASH_OVERFLOW, false); // this trips on many mappings now...
5657
// Do not automatically close unclosed objects/arrays in com.fasterxml.jackson.dataformat.smile.SmileGenerator#close() method
5758
smileFactory.configure(JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT, false);
59+
smileFactory.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, XContent.isStrictDuplicateDetectionEnabled());
5860
smileXContent = new SmileXContent();
5961
}
6062

core/src/main/java/org/elasticsearch/common/xcontent/yaml/YamlXContent.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.common.xcontent.yaml;
2121

2222
import com.fasterxml.jackson.core.JsonEncoding;
23+
import com.fasterxml.jackson.core.JsonParser;
2324
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
2425
import org.elasticsearch.ElasticsearchParseException;
2526
import org.elasticsearch.common.bytes.BytesReference;
@@ -50,6 +51,7 @@ public static XContentBuilder contentBuilder() throws IOException {
5051

5152
static {
5253
yamlFactory = new YAMLFactory();
54+
yamlFactory.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, XContent.isStrictDuplicateDetectionEnabled());
5355
yamlXContent = new YamlXContent();
5456
}
5557

core/src/test/java/org/elasticsearch/common/settings/loader/JsonSettingsLoaderTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import org.elasticsearch.ElasticsearchParseException;
2323
import org.elasticsearch.common.settings.Settings;
2424
import org.elasticsearch.common.settings.SettingsException;
25-
import org.elasticsearch.common.xcontent.json.JsonXContent;
25+
import org.elasticsearch.common.xcontent.XContent;
2626
import org.elasticsearch.test.ESTestCase;
2727

2828
import static org.hamcrest.CoreMatchers.containsString;
@@ -49,8 +49,8 @@ public void testSimpleJsonSettings() throws Exception {
4949
}
5050

5151
public void testDuplicateKeysThrowsException() {
52-
assumeFalse("Test only makes sense if JSON parser doesn't have strict duplicate checks enabled",
53-
JsonXContent.isStrictDuplicateDetectionEnabled());
52+
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
53+
XContent.isStrictDuplicateDetectionEnabled());
5454
final String json = "{\"foo\":\"bar\",\"foo\":\"baz\"}";
5555
final SettingsException e = expectThrows(SettingsException.class, () -> Settings.builder().loadFromSource(json).build());
5656
assertEquals(e.getCause().getClass(), ElasticsearchParseException.class);

core/src/test/java/org/elasticsearch/common/settings/loader/YamlSettingsLoaderTests.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.elasticsearch.ElasticsearchParseException;
2323
import org.elasticsearch.common.settings.Settings;
2424
import org.elasticsearch.common.settings.SettingsException;
25+
import org.elasticsearch.common.xcontent.XContent;
2526
import org.elasticsearch.test.ESTestCase;
2627

2728
import java.nio.charset.StandardCharsets;
@@ -68,6 +69,9 @@ public void testIndentationWithExplicitDocumentStart() throws Exception {
6869
}
6970

7071
public void testDuplicateKeysThrowsException() {
72+
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
73+
XContent.isStrictDuplicateDetectionEnabled());
74+
7175
String yaml = "foo: bar\nfoo: baz";
7276
SettingsException e = expectThrows(SettingsException.class, () -> {
7377
Settings.builder().loadFromSource(yaml);

core/src/test/java/org/elasticsearch/common/xcontent/BaseXContentTestCase.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.fasterxml.jackson.core.JsonGenerationException;
2323
import com.fasterxml.jackson.core.JsonGenerator;
2424

25+
import com.fasterxml.jackson.core.JsonParseException;
2526
import org.apache.lucene.util.BytesRef;
2627
import org.apache.lucene.util.Constants;
2728
import org.elasticsearch.cluster.metadata.IndexMetaData;
@@ -66,6 +67,7 @@
6667
import static org.hamcrest.Matchers.equalTo;
6768
import static org.hamcrest.Matchers.instanceOf;
6869
import static org.hamcrest.Matchers.notNullValue;
70+
import static org.hamcrest.Matchers.startsWith;
6971

7072
public abstract class BaseXContentTestCase extends ESTestCase {
7173

@@ -984,6 +986,22 @@ public void testSelfReferencingIterableTwoLevels() throws IOException {
984986
assertThat(e.getMessage(), containsString("Object has already been built and is self-referencing itself"));
985987
}
986988

989+
public void testChecksForDuplicates() throws Exception {
990+
assumeTrue("Test only makes sense if XContent parser has strict duplicate checks enabled",
991+
XContent.isStrictDuplicateDetectionEnabled());
992+
993+
BytesReference bytes = builder()
994+
.startObject()
995+
.field("key", 1)
996+
.field("key", 2)
997+
.endObject()
998+
.bytes();
999+
1000+
JsonParseException pex = expectThrows(JsonParseException.class, () -> createParser(xcontentType().xContent(), bytes).map());
1001+
assertThat(pex.getMessage(), startsWith("Duplicate field 'key'"));
1002+
}
1003+
1004+
9871005
private static void expectUnclosedException(ThrowingRunnable runnable) {
9881006
IllegalStateException e = expectThrows(IllegalStateException.class, runnable);
9891007
assertThat(e.getMessage(), containsString("Failed to close the XContentBuilder"));

core/src/test/java/org/elasticsearch/common/xcontent/ConstructingObjectParserTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ public void testMissingFirstConstructorArgButNotRequired() throws IOException {
169169
}
170170

171171
public void testRepeatedConstructorParam() throws IOException {
172-
assumeFalse("Test only makes sense if JSON parser doesn't have strict duplicate checks enabled",
173-
JsonXContent.isStrictDuplicateDetectionEnabled());
172+
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
173+
XContent.isStrictDuplicateDetectionEnabled());
174174
XContentParser parser = createParser(JsonXContent.jsonXContent,
175175
"{\n"
176176
+ " \"vegetable\": 1,\n"

core/src/test/java/org/elasticsearch/common/xcontent/json/JsonXContentTests.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.fasterxml.jackson.core.JsonFactory;
2323
import com.fasterxml.jackson.core.JsonGenerator;
2424

25-
import com.fasterxml.jackson.core.JsonParseException;
2625
import org.elasticsearch.common.xcontent.BaseXContentTestCase;
2726
import org.elasticsearch.common.xcontent.XContentType;
2827

@@ -40,13 +39,4 @@ public void testBigInteger() throws Exception {
4039
JsonGenerator generator = new JsonFactory().createGenerator(os);
4140
doTestBigInteger(generator, os);
4241
}
43-
44-
public void testChecksForDuplicates() throws Exception {
45-
assumeTrue("Test only makes sense if JSON parser doesn't have strict duplicate checks enabled",
46-
JsonXContent.isStrictDuplicateDetectionEnabled());
47-
48-
JsonParseException pex = expectThrows(JsonParseException.class,
49-
() -> XContentType.JSON.xContent().createParser("{ \"key\": 1, \"key\": 2 }").map());
50-
assertEquals("Duplicate field 'key'", pex.getMessage());
51-
}
5242
}

core/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.lucene.search.Query;
2727
import org.elasticsearch.common.ParseFieldMatcher;
2828
import org.elasticsearch.common.ParsingException;
29+
import org.elasticsearch.common.xcontent.XContent;
2930
import org.elasticsearch.common.xcontent.XContentBuilder;
3031
import org.elasticsearch.common.xcontent.XContentFactory;
3132
import org.elasticsearch.common.xcontent.XContentType;
@@ -340,8 +341,8 @@ public void testUnknownQueryName() throws IOException {
340341
* test that two queries in object throws error
341342
*/
342343
public void testTooManyQueriesInObject() throws IOException {
343-
assumeFalse("Test only makes sense if JSON parser doesn't have strict duplicate checks enabled",
344-
JsonXContent.isStrictDuplicateDetectionEnabled());
344+
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
345+
XContent.isStrictDuplicateDetectionEnabled());
345346
String clauseType = randomFrom("must", "should", "must_not", "filter");
346347
// should also throw error if invalid query is preceded by a valid one
347348
String query = "{\n" +

core/src/test/java/org/elasticsearch/index/query/ConstantScoreQueryBuilderTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.lucene.search.ConstantScoreQuery;
2323
import org.apache.lucene.search.Query;
2424
import org.elasticsearch.common.ParsingException;
25+
import org.elasticsearch.common.xcontent.XContent;
2526
import org.elasticsearch.common.xcontent.json.JsonXContent;
2627
import org.elasticsearch.search.internal.SearchContext;
2728
import org.elasticsearch.test.AbstractQueryTestCase;
@@ -66,8 +67,8 @@ public void testFilterElement() throws IOException {
6667
* test that multiple "filter" elements causes {@link ParsingException}
6768
*/
6869
public void testMultipleFilterElements() throws IOException {
69-
assumeFalse("Test only makes sense if JSON parser doesn't have strict duplicate checks enabled",
70-
JsonXContent.isStrictDuplicateDetectionEnabled());
70+
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
71+
XContent.isStrictDuplicateDetectionEnabled());
7172
String queryString = "{ \"" + ConstantScoreQueryBuilder.NAME + "\" : {\n" +
7273
"\"filter\" : { \"term\": { \"foo\": \"a\" } },\n" +
7374
"\"filter\" : { \"term\": { \"foo\": \"x\" } },\n" +

core/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
3737
import org.elasticsearch.common.lucene.search.function.WeightFactorFunction;
3838
import org.elasticsearch.common.unit.DistanceUnit;
39+
import org.elasticsearch.common.xcontent.XContent;
3940
import org.elasticsearch.common.xcontent.XContentParser;
4041
import org.elasticsearch.common.xcontent.XContentType;
4142
import org.elasticsearch.common.xcontent.json.JsonXContent;
@@ -739,8 +740,8 @@ public void testMalformedQueryMultipleQueryObjects() throws IOException {
739740
}
740741

741742
public void testMalformedQueryMultipleQueryElements() throws IOException {
742-
assumeFalse("Test only makes sense if JSON parser doesn't have strict duplicate checks enabled",
743-
JsonXContent.isStrictDuplicateDetectionEnabled());
743+
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
744+
XContent.isStrictDuplicateDetectionEnabled());
744745
String json = "{\n" +
745746
" \"function_score\":{\n" +
746747
" \"query\":{\n" +

core/src/test/java/org/elasticsearch/search/aggregations/AggregatorParsingTests.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.elasticsearch.common.ParseFieldMatcher;
2323
import org.elasticsearch.common.ParsingException;
2424
import org.elasticsearch.common.settings.Settings;
25+
import org.elasticsearch.common.xcontent.XContent;
2526
import org.elasticsearch.common.xcontent.XContentBuilder;
2627
import org.elasticsearch.common.xcontent.XContentParser;
2728
import org.elasticsearch.common.xcontent.json.JsonXContent;
@@ -104,8 +105,8 @@ public void testTwoTypes() throws Exception {
104105
}
105106

106107
public void testTwoAggs() throws Exception {
107-
assumeFalse("Test only makes sense if JSON parser doesn't have strict duplicate checks enabled",
108-
JsonXContent.isStrictDuplicateDetectionEnabled());
108+
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
109+
XContent.isStrictDuplicateDetectionEnabled());
109110
XContentBuilder source = JsonXContent.contentBuilder()
110111
.startObject()
111112
.startObject("by_date")
@@ -180,8 +181,8 @@ public void testInvalidAggregationName() throws Exception {
180181
}
181182

182183
public void testSameAggregationName() throws Exception {
183-
assumeFalse("Test only makes sense if JSON parser doesn't have strict duplicate checks enabled",
184-
JsonXContent.isStrictDuplicateDetectionEnabled());
184+
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
185+
XContent.isStrictDuplicateDetectionEnabled());
185186
final String name = randomAsciiOfLengthBetween(1, 10);
186187
XContentBuilder source = JsonXContent.contentBuilder()
187188
.startObject()

core/src/test/java/org/elasticsearch/search/suggest/phrase/DirectCandidateGeneratorTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.elasticsearch.common.ParsingException;
2424
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
2525
import org.elasticsearch.common.xcontent.ToXContent;
26+
import org.elasticsearch.common.xcontent.XContent;
2627
import org.elasticsearch.common.xcontent.XContentBuilder;
2728
import org.elasticsearch.common.xcontent.XContentFactory;
2829
import org.elasticsearch.common.xcontent.XContentParser;
@@ -149,7 +150,7 @@ public void testIllegalXContent() throws IOException {
149150
"Required [field]");
150151

151152
// test two fieldnames
152-
if (JsonXContent.isStrictDuplicateDetectionEnabled()) {
153+
if (XContent.isStrictDuplicateDetectionEnabled()) {
153154
logger.info("Skipping test as it uses a custom duplicate check that is obsolete when strict duplicate checks are enabled.");
154155
} else {
155156
directGenerator = "{ \"field\" : \"f1\", \"field\" : \"f2\" }";

docs/reference/migration/migrate_6_0/rest.asciidoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ This feature was removed in the 5.x series, but a backwards-compatibility layer
88
system property `elasticsearch.json.allow_unquoted_field_names`. This backwards-compatibility layer
99
has been removed in Elasticsearch 6.0.0.
1010

11-
==== Duplicate Keys in JSON
11+
==== Duplicate Keys in Content
1212

13-
In previous versions of Elasticsearch, JSON documents were allowed to contain duplicate keys. Elasticsearch 6.0.0
14-
enforces that all keys are unique.
13+
In previous versions of Elasticsearch, documents were allowed to contain duplicate keys. Elasticsearch 6.0.0
14+
enforces that all keys are unique. This applies to all content types: JSON, CBOR, Yaml and Smile.
1515

1616
==== Analyze API changes
1717

test/framework/src/test/java/org/elasticsearch/test/rest/yaml/parser/AbstractClientYamlTestFragmentParserTestCase.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ public abstract class AbstractClientYamlTestFragmentParserTestCase extends ESTes
3636
@After
3737
public void tearDown() throws Exception {
3838
super.tearDown();
39-
//this is the way to make sure that we consumed the whole yaml
40-
assertThat(parser.currentToken(), nullValue());
41-
parser.close();
39+
// test may be skipped so we did not create a parser instance
40+
if (parser != null) {
41+
//this is the way to make sure that we consumed the whole yaml
42+
assertThat(parser.currentToken(), nullValue());
43+
parser.close();
44+
}
4245
}
4346
}

0 commit comments

Comments
 (0)