Skip to content

Commit

Permalink
[7.x] Implement XContentParser.genericMap and XContentParser.genericM…
Browse files Browse the repository at this point in the history
…apOrdered methods (elastic#42059) (elastic#43575)
  • Loading branch information
przemekwitek authored Jun 25, 2019
1 parent b15e40f commit c702cd7
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 52 deletions.
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

0 comments on commit c702cd7

Please sign in to comment.