diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentParser.java index eaac5c8afb148..94172087f1611 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentParser.java @@ -19,11 +19,14 @@ package org.elasticsearch.common.xcontent; +import org.elasticsearch.common.CheckedFunction; + import java.io.Closeable; import java.io.IOException; import java.nio.CharBuffer; import java.util.List; import java.util.Map; +import java.util.function.Supplier; /** * Interface for pull - parsing {@link XContent} see {@link XContentType} for supported types. @@ -135,6 +138,18 @@ enum NumberType { Map mapStringsOrdered() throws IOException; + /** + * Returns an instance of {@link Map} holding parsed map. + * Serves as a replacement for the "map", "mapOrdered", "mapStrings" and "mapStringsOrdered" methods above. + * + * @param mapFactory factory for creating new {@link Map} objects + * @param mapValueParser parser for parsing a single map value + * @param map value type + * @return {@link Map} object + */ + Map map( + Supplier> mapFactory, CheckedFunction mapValueParser) throws IOException; + List list() throws IOException; List listOrderedMap() throws IOException; diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentSubParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentSubParser.java index adcbf6ef1bee0..252bfea7ca9c0 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentSubParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/XContentSubParser.java @@ -19,10 +19,13 @@ package org.elasticsearch.common.xcontent; +import org.elasticsearch.common.CheckedFunction; + import java.io.IOException; import java.nio.CharBuffer; import java.util.List; import java.util.Map; +import java.util.function.Supplier; /** * Wrapper for a XContentParser that makes a single object/array look like a complete document. @@ -110,6 +113,12 @@ public Map mapStringsOrdered() throws IOException { return parser.mapStringsOrdered(); } + @Override + public Map map( + Supplier> mapFactory, CheckedFunction mapValueParser) throws IOException { + return parser.map(mapFactory, mapValueParser); + } + @Override public List list() throws IOException { return parser.list(); diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/AbstractXContentParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/AbstractXContentParser.java index 34598a77df1f3..66c98b93e4a63 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/AbstractXContentParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/support/AbstractXContentParser.java @@ -20,6 +20,7 @@ package org.elasticsearch.common.xcontent.support; import org.elasticsearch.common.Booleans; +import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParseException; @@ -34,6 +35,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; public abstract class AbstractXContentParser implements XContentParser { @@ -279,6 +281,12 @@ public Map mapStringsOrdered() throws IOException { return readOrderedMapStrings(this); } + @Override + public Map map( + Supplier> mapFactory, CheckedFunction mapValueParser) throws IOException { + return readGenericMap(this, mapFactory, mapValueParser); + } + @Override public List list() throws IOException { return readList(this); @@ -289,21 +297,13 @@ public List listOrderedMap() throws IOException { return readListOrderedMap(this); } - public interface MapFactory { - Map newMap(); - } - - interface MapStringsFactory { - Map newMap(); - } - - static final MapFactory SIMPLE_MAP_FACTORY = HashMap::new; + static final Supplier> SIMPLE_MAP_FACTORY = HashMap::new; - static final MapFactory ORDERED_MAP_FACTORY = LinkedHashMap::new; + static final Supplier> ORDERED_MAP_FACTORY = LinkedHashMap::new; - static final MapStringsFactory SIMPLE_MAP_STRINGS_FACTORY = HashMap::new; + static final Supplier> SIMPLE_MAP_STRINGS_FACTORY = HashMap::new; - static final MapStringsFactory ORDERED_MAP_STRINGS_FACTORY = LinkedHashMap::new; + static final Supplier> ORDERED_MAP_STRINGS_FACTORY = LinkedHashMap::new; static Map readMap(XContentParser parser) throws IOException { return readMap(parser, SIMPLE_MAP_FACTORY); @@ -329,28 +329,19 @@ static List readListOrderedMap(XContentParser parser) throws IOException return readList(parser, ORDERED_MAP_FACTORY); } - static Map readMap(XContentParser parser, MapFactory mapFactory) throws IOException { - Map map = mapFactory.newMap(); - XContentParser.Token token = parser.currentToken(); - if (token == null) { - token = parser.nextToken(); - } - if (token == XContentParser.Token.START_OBJECT) { - token = parser.nextToken(); - } - for (; token == XContentParser.Token.FIELD_NAME; token = parser.nextToken()) { - // Must point to field name - String fieldName = parser.currentName(); - // And then the value... - token = parser.nextToken(); - Object value = readValue(parser, mapFactory, token); - map.put(fieldName, value); - } - return map; + static Map readMap(XContentParser parser, Supplier> mapFactory) throws IOException { + return readGenericMap(parser, mapFactory, p -> readValue(p, mapFactory)); } - static Map readMapStrings(XContentParser parser, MapStringsFactory mapStringsFactory) throws IOException { - Map map = mapStringsFactory.newMap(); + static Map readMapStrings(XContentParser parser, Supplier> mapFactory) throws IOException { + return readGenericMap(parser, mapFactory, XContentParser::text); + } + + static Map readGenericMap( + XContentParser parser, + Supplier> mapFactory, + CheckedFunction mapValueParser) throws IOException { + Map map = mapFactory.get(); XContentParser.Token token = parser.currentToken(); if (token == null) { token = parser.nextToken(); @@ -363,13 +354,13 @@ static Map readMapStrings(XContentParser parser, MapStringsFacto String fieldName = parser.currentName(); // And then the value... parser.nextToken(); - String value = parser.text(); + T value = mapValueParser.apply(parser); map.put(fieldName, value); } return map; } - static List readList(XContentParser parser, MapFactory mapFactory) throws IOException { + static List readList(XContentParser parser, Supplier> mapFactory) throws IOException { XContentParser.Token token = parser.currentToken(); if (token == null) { token = parser.nextToken(); @@ -386,28 +377,22 @@ static List readList(XContentParser parser, MapFactory mapFactory) throw ArrayList list = new ArrayList<>(); for (; token != null && token != XContentParser.Token.END_ARRAY; token = parser.nextToken()) { - list.add(readValue(parser, mapFactory, token)); + list.add(readValue(parser, mapFactory)); } return list; } - public static Object readValue(XContentParser parser, MapFactory mapFactory, XContentParser.Token token) throws IOException { - if (token == XContentParser.Token.VALUE_NULL) { - return null; - } else if (token == XContentParser.Token.VALUE_STRING) { - return parser.text(); - } else if (token == XContentParser.Token.VALUE_NUMBER) { - return parser.numberValue(); - } else if (token == XContentParser.Token.VALUE_BOOLEAN) { - return parser.booleanValue(); - } else if (token == XContentParser.Token.START_OBJECT) { - return readMap(parser, mapFactory); - } else if (token == XContentParser.Token.START_ARRAY) { - return readList(parser, mapFactory); - } else if (token == XContentParser.Token.VALUE_EMBEDDED_OBJECT) { - return parser.binaryValue(); + public static Object readValue(XContentParser parser, Supplier> mapFactory) throws IOException { + switch (parser.currentToken()) { + case VALUE_STRING: return parser.text(); + case VALUE_NUMBER: return parser.numberValue(); + case VALUE_BOOLEAN: return parser.booleanValue(); + case START_OBJECT: return readMap(parser, mapFactory); + case START_ARRAY: return readList(parser, mapFactory); + case VALUE_EMBEDDED_OBJECT: return parser.binaryValue(); + case VALUE_NULL: + default: return null; } - return null; } @Override diff --git a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/SimpleStruct.java b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/SimpleStruct.java new file mode 100644 index 0000000000000..72bff3500be35 --- /dev/null +++ b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/SimpleStruct.java @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.common.xcontent; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * Simple structure with 3 fields: int, double and String. + * Used for testing parsers. + */ +class SimpleStruct implements ToXContentObject { + + static SimpleStruct fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + private static final ParseField I = new ParseField("i"); + private static final ParseField D = new ParseField("d"); + private static final ParseField S = new ParseField("s"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>( + "simple_struct", true, args -> new SimpleStruct((int) args[0], (double) args[1], (String) args[2])); + + static { + PARSER.declareInt(constructorArg(), I); + PARSER.declareDouble(constructorArg(), D); + PARSER.declareString(constructorArg(), S); + } + + private final int i; + private final double d; + private final String s; + + SimpleStruct(int i, double d, String s) { + this.i = i; + this.d = d; + this.s = s; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder + .startObject() + .field(I.getPreferredName(), i) + .field(D.getPreferredName(), d) + .field(S.getPreferredName(), s) + .endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SimpleStruct other = (SimpleStruct) o; + return i == other.i && d == other.d && Objects.equals(s, other.s); + } + + @Override + public int hashCode() { + return Objects.hash(i, d, s); + } + + @Override + public String toString() { + return Strings.toString(this); + } +} + diff --git a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/XContentParserTests.java b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/XContentParserTests.java index 606d019f3c4f7..85f2e47ecccea 100644 --- a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/XContentParserTests.java +++ b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/XContentParserTests.java @@ -30,18 +30,21 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.nullValue; +import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; public class XContentParserTests extends ESTestCase { @@ -329,6 +332,65 @@ public void testNestedMapInList() throws IOException { } } + public void testGenericMap() throws IOException { + String content = "{" + + "\"c\": { \"i\": 3, \"d\": 0.3, \"s\": \"ccc\" }, " + + "\"a\": { \"i\": 1, \"d\": 0.1, \"s\": \"aaa\" }, " + + "\"b\": { \"i\": 2, \"d\": 0.2, \"s\": \"bbb\" }" + + "}"; + SimpleStruct structA = new SimpleStruct(1, 0.1, "aaa"); + SimpleStruct structB = new SimpleStruct(2, 0.2, "bbb"); + SimpleStruct structC = new SimpleStruct(3, 0.3, "ccc"); + Map expectedMap = new HashMap<>(); + expectedMap.put("a", structA); + expectedMap.put("b", structB); + expectedMap.put("c", structC); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, content)) { + Map actualMap = parser.map(HashMap::new, SimpleStruct::fromXContent); + // Verify map contents, ignore the iteration order. + assertThat(actualMap, equalTo(expectedMap)); + assertThat(actualMap.values(), containsInAnyOrder(structA, structB, structC)); + assertNull(parser.nextToken()); + } + } + + public void testGenericMapOrdered() throws IOException { + String content = "{" + + "\"c\": { \"i\": 3, \"d\": 0.3, \"s\": \"ccc\" }, " + + "\"a\": { \"i\": 1, \"d\": 0.1, \"s\": \"aaa\" }, " + + "\"b\": { \"i\": 2, \"d\": 0.2, \"s\": \"bbb\" }" + + "}"; + SimpleStruct structA = new SimpleStruct(1, 0.1, "aaa"); + SimpleStruct structB = new SimpleStruct(2, 0.2, "bbb"); + SimpleStruct structC = new SimpleStruct(3, 0.3, "ccc"); + Map expectedMap = new HashMap<>(); + expectedMap.put("a", structA); + expectedMap.put("b", structB); + expectedMap.put("c", structC); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, content)) { + Map actualMap = parser.map(LinkedHashMap::new, SimpleStruct::fromXContent); + // Verify map contents, ignore the iteration order. + assertThat(actualMap, equalTo(expectedMap)); + // Verify that map's iteration order is the same as the order in which fields appear in JSON. + assertThat(actualMap.values(), contains(structC, structA, structB)); + assertNull(parser.nextToken()); + } + } + + public void testGenericMap_Failure_MapContainingUnparsableValue() throws IOException { + String content = "{" + + "\"a\": { \"i\": 1, \"d\": 0.1, \"s\": \"aaa\" }, " + + "\"b\": { \"i\": 2, \"d\": 0.2, \"s\": 666 }, " + + "\"c\": { \"i\": 3, \"d\": 0.3, \"s\": \"ccc\" }" + + "}"; + try (XContentParser parser = createParser(JsonXContent.jsonXContent, content)) { + XContentParseException exception = expectThrows( + XContentParseException.class, + () -> parser.map(HashMap::new, SimpleStruct::fromXContent)); + assertThat(exception, hasMessage(containsString("s doesn't support values of type: VALUE_NUMBER"))); + } + } + public void testSubParserObject() throws IOException { XContentBuilder builder = XContentFactory.jsonBuilder(); int numberOfTokens; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java index 8a73820850844..378af28ea853f 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -282,7 +282,7 @@ public void parse(ParseContext context) throws IOException { String valuePreview = ""; try { XContentParser parser = context.parser(); - Object complexValue = AbstractXContentParser.readValue(parser, HashMap::new, parser.currentToken()); + Object complexValue = AbstractXContentParser.readValue(parser, HashMap::new); if (complexValue == null) { valuePreview = "null"; } else { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/WatcherXContentParser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/WatcherXContentParser.java index fcb3802ca6b76..1d155a5f0c02d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/WatcherXContentParser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/WatcherXContentParser.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.watcher.support.xcontent; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -21,6 +22,7 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Map; +import java.util.function.Supplier; /** * A xcontent parser that is used by watcher. This is a special parser that is @@ -123,6 +125,12 @@ public Map mapStringsOrdered() throws IOException { return parser.mapStringsOrdered(); } + @Override + public Map map( + Supplier> mapFactory, CheckedFunction mapValueParser) throws IOException { + return parser.map(mapFactory, mapValueParser); + } + @Override public List list() throws IOException { return parser.list();