Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] Implement XContentParser.genericMap and XContentParser.genericMapOrdered methods (#42059) #43575

Merged
merged 4 commits into from
Jun 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -135,6 +138,18 @@ enum NumberType {

Map<String, String> 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 <T> map value type
* @return {@link Map} object
*/
<T> Map<String, T> map(
Supplier<Map<String, T>> mapFactory, CheckedFunction<XContentParser, T, IOException> mapValueParser) throws IOException;

List<Object> list() throws IOException;

List<Object> listOrderedMap() throws IOException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -110,6 +113,12 @@ public Map<String, String> mapStringsOrdered() throws IOException {
return parser.mapStringsOrdered();
}

@Override
public <T> Map<String, T> map(
Supplier<Map<String, T>> mapFactory, CheckedFunction<XContentParser, T, IOException> mapValueParser) throws IOException {
return parser.map(mapFactory, mapValueParser);
}

@Override
public List<Object> list() throws IOException {
return parser.list();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -279,6 +281,12 @@ public Map<String, String> mapStringsOrdered() throws IOException {
return readOrderedMapStrings(this);
}

@Override
public <T> Map<String, T> map(
Supplier<Map<String, T>> mapFactory, CheckedFunction<XContentParser, T, IOException> mapValueParser) throws IOException {
return readGenericMap(this, mapFactory, mapValueParser);
}

@Override
public List<Object> list() throws IOException {
return readList(this);
Expand All @@ -289,21 +297,13 @@ public List<Object> listOrderedMap() throws IOException {
return readListOrderedMap(this);
}

public interface MapFactory {
Map<String, Object> newMap();
}

interface MapStringsFactory {
Map<String, String> newMap();
}

static final MapFactory SIMPLE_MAP_FACTORY = HashMap::new;
static final Supplier<Map<String, Object>> SIMPLE_MAP_FACTORY = HashMap::new;

static final MapFactory ORDERED_MAP_FACTORY = LinkedHashMap::new;
static final Supplier<Map<String, Object>> ORDERED_MAP_FACTORY = LinkedHashMap::new;

static final MapStringsFactory SIMPLE_MAP_STRINGS_FACTORY = HashMap::new;
static final Supplier<Map<String, String>> SIMPLE_MAP_STRINGS_FACTORY = HashMap::new;

static final MapStringsFactory ORDERED_MAP_STRINGS_FACTORY = LinkedHashMap::new;
static final Supplier<Map<String, String>> ORDERED_MAP_STRINGS_FACTORY = LinkedHashMap::new;

static Map<String, Object> readMap(XContentParser parser) throws IOException {
return readMap(parser, SIMPLE_MAP_FACTORY);
Expand All @@ -329,28 +329,19 @@ static List<Object> readListOrderedMap(XContentParser parser) throws IOException
return readList(parser, ORDERED_MAP_FACTORY);
}

static Map<String, Object> readMap(XContentParser parser, MapFactory mapFactory) throws IOException {
Map<String, Object> 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<String, Object> readMap(XContentParser parser, Supplier<Map<String, Object>> mapFactory) throws IOException {
return readGenericMap(parser, mapFactory, p -> readValue(p, mapFactory));
}

static Map<String, String> readMapStrings(XContentParser parser, MapStringsFactory mapStringsFactory) throws IOException {
Map<String, String> map = mapStringsFactory.newMap();
static Map<String, String> readMapStrings(XContentParser parser, Supplier<Map<String, String>> mapFactory) throws IOException {
return readGenericMap(parser, mapFactory, XContentParser::text);
}

static <T> Map<String, T> readGenericMap(
XContentParser parser,
Supplier<Map<String, T>> mapFactory,
CheckedFunction<XContentParser, T, IOException> mapValueParser) throws IOException {
Map<String, T> map = mapFactory.get();
XContentParser.Token token = parser.currentToken();
if (token == null) {
token = parser.nextToken();
Expand All @@ -363,13 +354,13 @@ static Map<String, String> 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<Object> readList(XContentParser parser, MapFactory mapFactory) throws IOException {
static List<Object> readList(XContentParser parser, Supplier<Map<String, Object>> mapFactory) throws IOException {
XContentParser.Token token = parser.currentToken();
if (token == null) {
token = parser.nextToken();
Expand All @@ -386,28 +377,22 @@ static List<Object> readList(XContentParser parser, MapFactory mapFactory) throw

ArrayList<Object> 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<Map<String, Object>> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SimpleStruct, Void> 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);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<String, SimpleStruct> expectedMap = new HashMap<>();
expectedMap.put("a", structA);
expectedMap.put("b", structB);
expectedMap.put("c", structC);
try (XContentParser parser = createParser(JsonXContent.jsonXContent, content)) {
Map<String, SimpleStruct> 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<String, SimpleStruct> expectedMap = new HashMap<>();
expectedMap.put("a", structA);
expectedMap.put("b", structB);
expectedMap.put("c", structC);
try (XContentParser parser = createParser(JsonXContent.jsonXContent, content)) {
Map<String, SimpleStruct> 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;
Expand Down
Loading