parsers = registry.get(categoryClass);
+ if (parsers == null) {
+ if (registry.isEmpty()) {
+ // The "empty" registry will never work so we throw a better exception as a hint.
+ throw new NamedObjectNotFoundException("named objects are not supported for this parser");
+ }
+ throw new NamedObjectNotFoundException("unknown named object category [" + categoryClass.getName() + "]");
+ }
+ Entry entry = parsers.get(name);
+ if (entry == null) {
+ throw new NamedObjectNotFoundException(parser.getTokenLocation(), "unable to parse " + categoryClass.getSimpleName() +
+ " with name [" + name + "]: parser not found");
+ }
+ return categoryClass.cast(entry.parser.parse(parser, context));
+ }
+
+}
diff --git a/elx-http/src/main/java/org/xbib/elx/http/util/ObjectParser.java b/elx-http/src/main/java/org/xbib/elx/http/util/ObjectParser.java
new file mode 100644
index 0000000..febb64b
--- /dev/null
+++ b/elx-http/src/main/java/org/xbib/elx/http/util/ObjectParser.java
@@ -0,0 +1,441 @@
+package org.xbib.elx.http.util;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.common.xcontent.XContentParser.Token.START_ARRAY;
+import static org.elasticsearch.common.xcontent.XContentParser.Token.START_OBJECT;
+import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_BOOLEAN;
+import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_EMBEDDED_OBJECT;
+import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_NULL;
+import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_NUMBER;
+import static org.elasticsearch.common.xcontent.XContentParser.Token.VALUE_STRING;
+
+/**
+ * A declarative, stateless parser that turns XContent into setter calls. A single parser should be defined for each object being parsed,
+ * nested elements can be added via {@link #declareObject(BiConsumer, ContextParser, ParseField)} which should be satisfied where possible
+ * by passing another instance of {@link ObjectParser}, this one customized for that Object.
+ *
+ * This class works well for object that do have a constructor argument or that can be built using information available from earlier in the
+ * XContent.
+ *
+ *
+ * Instances of {@link ObjectParser} should be setup by declaring a constant field for the parsers and declaring all fields in a static
+ * block just below the creation of the parser. Like this:
+ *
+ * {@code
+ * private static final ObjectParser PARSER = new ObjectParser<>("thing", Thing::new));
+ * static {
+ * PARSER.declareInt(Thing::setMineral, new ParseField("mineral"));
+ * PARSER.declareInt(Thing::setFruit, new ParseField("fruit"));
+ * }
+ * }
+ * It's highly recommended to use the high level declare methods like {@link #declareString(BiConsumer, ParseField)} instead of
+ * {@link #declareField} which can be used to implement exceptional parsing operations not covered by the high level methods.
+ */
+public final class ObjectParser extends AbstractObjectParser {
+
+ private static final Logger logger = LogManager.getLogger(ObjectParser.class.getName());
+
+ public static BiConsumer> fromList(Class c,
+ BiConsumer consumer) {
+ return (Value v, List l) -> {
+ @SuppressWarnings("unchecked")
+ ElementValue[] array = (ElementValue[]) Array.newInstance(c, l.size());
+ consumer.accept(v, l.toArray(array));
+ };
+ }
+
+ private final Map fieldParserMap = new HashMap<>();
+
+ private final String name;
+
+ private final Supplier valueSupplier;
+
+ /**
+ * Should this parser ignore unknown fields? This should generally be set to true only when parsing responses from external systems,
+ * never when parsing requests from users.
+ */
+ private final boolean ignoreUnknownFields;
+
+ /**
+ * Creates a new ObjectParser instance with a name. This name is used to reference the parser in exceptions and messages.
+ */
+ public ObjectParser(String name) {
+ this(name, null);
+ }
+
+ /**
+ * Creates a new ObjectParser instance which a name.
+ * @param name the parsers name, used to reference the parser in exceptions and messages.
+ * @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
+ */
+ public ObjectParser(String name, @Nullable Supplier valueSupplier) {
+ this(name, false, valueSupplier);
+ }
+
+ /**
+ * Creates a new ObjectParser instance which a name.
+ * @param name the parsers name, used to reference the parser in exceptions and messages.
+ * @param ignoreUnknownFields Should this parser ignore unknown fields? This should generally be set to true only when parsing
+ * responses from external systems, never when parsing requests from users.
+ * @param valueSupplier a supplier that creates a new Value instance used when the parser is used as an inner object parser.
+ */
+ public ObjectParser(String name, boolean ignoreUnknownFields, @Nullable Supplier valueSupplier) {
+ this.name = name;
+ this.valueSupplier = valueSupplier;
+ this.ignoreUnknownFields = ignoreUnknownFields;
+ }
+
+ /**
+ * Parses a Value from the given {@link XContentParser}
+ * @param parser the parser to build a value from
+ * @param context context needed for parsing
+ * @return a new value instance drawn from the provided value supplier on {@link #ObjectParser(String, Supplier)}
+ * @throws IOException if an IOException occurs.
+ */
+ @Override
+ public Value parse(XContentParser parser, Context context) throws IOException {
+ if (valueSupplier == null) {
+ throw new NullPointerException("valueSupplier is not set");
+ }
+ return parse(parser, valueSupplier.get(), context);
+ }
+
+ /**
+ * Parses a Value from the given {@link XContentParser}
+ * @param parser the parser to build a value from
+ * @param value the value to fill from the parser
+ * @param context a context that is passed along to all declared field parsers
+ * @return the parsed value
+ * @throws IOException if an IOException occurs.
+ */
+ public Value parse(XContentParser parser, Value value, Context context) throws IOException {
+ logger.debug("parse");
+ XContentParser.Token token;
+ if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
+ token = parser.currentToken();
+ } else {
+ token = parser.nextToken();
+ if (token != XContentParser.Token.START_OBJECT) {
+ throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] Expected START_OBJECT but was: " + token);
+ }
+ }
+ FieldParser fieldParser = null;
+ String currentFieldName = null;
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+ fieldParser = getParser(currentFieldName, parser);
+ logger.debug("currentFieldName={} fieldParser={}", currentFieldName, fieldParser);
+ } else {
+ if (currentFieldName == null) {
+ throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] no field found");
+ }
+ if (fieldParser == null) {
+ assert ignoreUnknownFields : "this should only be possible if configured to ignore known fields";
+ parser.skipChildren(); // noop if parser points to a value, skips children if parser is start object or start array
+ } else {
+ fieldParser.assertSupports(name, parser, currentFieldName);
+ parseSub(parser, fieldParser, currentFieldName, value, context);
+ }
+ fieldParser = null;
+ }
+ }
+ return value;
+ }
+
+ @Override
+ public Value apply(XContentParser parser, Context context) {
+ if (valueSupplier == null) {
+ throw new NullPointerException("valueSupplier is not set");
+ }
+ try {
+ return parse(parser, valueSupplier.get(), context);
+ } catch (IOException e) {
+ throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] failed to parse object", e);
+ }
+ }
+
+ public interface Parser {
+ void parse(XContentParser parser, Value value, Context context) throws IOException;
+ }
+
+ public void declareField(Parser p, ParseField parseField, ValueType type) {
+ if (parseField == null) {
+ throw new IllegalArgumentException("[parseField] is required");
+ }
+ if (type == null) {
+ throw new IllegalArgumentException("[type] is required");
+ }
+ FieldParser fieldParser = new FieldParser(p, type.supportedTokens(), parseField, type);
+ for (String fieldValue : parseField.getAllNamesIncludedDeprecated()) {
+ fieldParserMap.putIfAbsent(fieldValue, fieldParser);
+ }
+ }
+
+ @Override
+ public void declareField(BiConsumer consumer, ContextParser parser, ParseField parseField,
+ ValueType type) {
+ if (consumer == null) {
+ throw new IllegalArgumentException("[consumer] is required");
+ }
+ if (parser == null) {
+ throw new IllegalArgumentException("[parser] is required");
+ }
+ declareField((p, v, c) -> consumer.accept(v, parser.parse(p, c)), parseField, type);
+ }
+
+ public void declareObjectOrDefault(BiConsumer consumer, BiFunction objectParser,
+ Supplier defaultValue, ParseField field) {
+ declareField((p, v, c) -> {
+ if (p.currentToken() == XContentParser.Token.VALUE_BOOLEAN) {
+ if (p.booleanValue()) {
+ consumer.accept(v, defaultValue.get());
+ }
+ } else {
+ consumer.accept(v, objectParser.apply(p, c));
+ }
+ }, field, ValueType.OBJECT_OR_BOOLEAN);
+ }
+
+ @Override
+ public void declareNamedObjects(BiConsumer> consumer, NamedObjectParser namedObjectParser,
+ Consumer orderedModeCallback, ParseField field) {
+ // This creates and parses the named object
+ BiFunction objectParser = (XContentParser p, Context c) -> {
+ if (p.currentToken() != XContentParser.Token.FIELD_NAME) {
+ throw new XContentParseException(p.getTokenLocation(), "[" + field + "] can be a single object with any number of "
+ + "fields or an array where each entry is an object with a single field");
+ }
+ // This messy exception nesting has the nice side effect of telling the use which field failed to parse
+ try {
+ String name = p.currentName();
+ try {
+ return namedObjectParser.parse(p, c, name);
+ } catch (Exception e) {
+ throw new XContentParseException(p.getTokenLocation(), "[" + field + "] failed to parse field [" + name + "]", e);
+ }
+ } catch (IOException e) {
+ throw new XContentParseException(p.getTokenLocation(), "[" + field + "] error while parsing", e);
+ }
+ };
+ declareField((XContentParser p, Value v, Context c) -> {
+ List fields = new ArrayList<>();
+ XContentParser.Token token;
+ if (p.currentToken() == XContentParser.Token.START_OBJECT) {
+ // Fields are just named entries in a single object
+ while ((token = p.nextToken()) != XContentParser.Token.END_OBJECT) {
+ fields.add(objectParser.apply(p, c));
+ }
+ } else if (p.currentToken() == XContentParser.Token.START_ARRAY) {
+ // Fields are objects in an array. Each object contains a named field.
+ orderedModeCallback.accept(v);
+ while ((token = p.nextToken()) != XContentParser.Token.END_ARRAY) {
+ if (token != XContentParser.Token.START_OBJECT) {
+ throw new XContentParseException(p.getTokenLocation(), "[" + field + "] can be a single object with any number of "
+ + "fields or an array where each entry is an object with a single field");
+ }
+ p.nextToken(); // Move to the first field in the object
+ fields.add(objectParser.apply(p, c));
+ p.nextToken(); // Move past the object, should be back to into the array
+ if (p.currentToken() != XContentParser.Token.END_OBJECT) {
+ throw new XContentParseException(p.getTokenLocation(), "[" + field + "] can be a single object with any number of "
+ + "fields or an array where each entry is an object with a single field");
+ }
+ }
+ }
+ consumer.accept(v, fields);
+ }, field, ValueType.OBJECT_ARRAY);
+ }
+
+ @Override
+ public void declareNamedObjects(BiConsumer> consumer, NamedObjectParser namedObjectParser,
+ ParseField field) {
+ Consumer orderedModeCallback = (v) -> {
+ throw new IllegalArgumentException("[" + field + "] doesn't support arrays. Use a single object with multiple fields.");
+ };
+ declareNamedObjects(consumer, namedObjectParser, orderedModeCallback, field);
+ }
+
+ /**
+ * Functional interface for instantiating and parsing named objects. See ObjectParserTests#NamedObject for the canonical way to
+ * implement this for objects that themselves have a parser.
+ */
+ @FunctionalInterface
+ public interface NamedObjectParser {
+ T parse(XContentParser p, Context c, String name) throws IOException;
+ }
+
+ /**
+ * Get the name of the parser.
+ */
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ private void parseArray(XContentParser parser, FieldParser fieldParser, String currentFieldName, Value value, Context context)
+ throws IOException {
+ assert parser.currentToken() == XContentParser.Token.START_ARRAY : "Token was: " + parser.currentToken();
+ parseValue(parser, fieldParser, currentFieldName, value, context);
+ }
+
+ private void parseValue(XContentParser parser, FieldParser fieldParser, String currentFieldName, Value value, Context context)
+ throws IOException {
+ try {
+ fieldParser.parser.parse(parser, value, context);
+ } catch (Exception ex) {
+ throw new XContentParseException(parser.getTokenLocation(),
+ "[" + name + "] failed to parse field [" + currentFieldName + "]", ex);
+ }
+ }
+
+ private void parseSub(XContentParser parser, FieldParser fieldParser, String currentFieldName, Value value, Context context)
+ throws IOException {
+ final XContentParser.Token token = parser.currentToken();
+ switch (token) {
+ case START_OBJECT:
+ parseValue(parser, fieldParser, currentFieldName, value, context);
+ /*
+ * Well behaving parsers should consume the entire object but
+ * asserting that they do that is not something we can do
+ * efficiently here. Instead we can check that they end on an
+ * END_OBJECT. They could end on the *wrong* end object and
+ * this test won't catch them, but that is the price that we pay
+ * for having a cheap test.
+ */
+ if (parser.currentToken() != XContentParser.Token.END_OBJECT) {
+ throw new IllegalStateException("parser for [" + currentFieldName + "] did not end on END_OBJECT");
+ }
+ break;
+ case START_ARRAY:
+ parseArray(parser, fieldParser, currentFieldName, value, context);
+ /*
+ * Well behaving parsers should consume the entire array but
+ * asserting that they do that is not something we can do
+ * efficiently here. Instead we can check that they end on an
+ * END_ARRAY. They could end on the *wrong* end array and
+ * this test won't catch them, but that is the price that we pay
+ * for having a cheap test.
+ */
+ if (parser.currentToken() != XContentParser.Token.END_ARRAY) {
+ throw new IllegalStateException("parser for [" + currentFieldName + "] did not end on END_ARRAY");
+ }
+ break;
+ case END_OBJECT:
+ case END_ARRAY:
+ case FIELD_NAME:
+ throw new XContentParseException(parser.getTokenLocation(), "[" + name + "]" + token + " is unexpected");
+ case VALUE_STRING:
+ case VALUE_NUMBER:
+ case VALUE_BOOLEAN:
+ case VALUE_EMBEDDED_OBJECT:
+ case VALUE_NULL:
+ parseValue(parser, fieldParser, currentFieldName, value, context);
+ }
+ }
+
+ private FieldParser getParser(String fieldName, XContentParser xContentParser) {
+ FieldParser parser = fieldParserMap.get(fieldName);
+ if (parser == null && false == ignoreUnknownFields) {
+ throw new XContentParseException(xContentParser.getTokenLocation(),
+ "[" + name + "] unknown field [" + fieldName + "], parser not found");
+ }
+ return parser;
+ }
+
+ private class FieldParser {
+ private final Parser parser;
+ private final EnumSet supportedTokens;
+ private final ParseField parseField;
+ private final ValueType type;
+
+ FieldParser(Parser parser, EnumSet supportedTokens, ParseField parseField, ValueType type) {
+ this.parser = parser;
+ this.supportedTokens = supportedTokens;
+ this.parseField = parseField;
+ this.type = type;
+ }
+
+ void assertSupports(String parserName, XContentParser parser, String currentFieldName) {
+ if (!supportedTokens.contains(parser.currentToken())) {
+ throw new XContentParseException(parser.getTokenLocation(),
+ "[" + parserName + "] " + currentFieldName + " doesn't support values of type: " + parser.currentToken());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "FieldParser{" +
+ "preferred_name=" + parseField.getPreferredName() +
+ ", supportedTokens=" + supportedTokens +
+ ", type=" + type.name() +
+ '}';
+ }
+ }
+
+ public enum ValueType {
+ STRING(VALUE_STRING),
+ STRING_OR_NULL(VALUE_STRING, VALUE_NULL),
+ FLOAT(VALUE_NUMBER, VALUE_STRING),
+ FLOAT_OR_NULL(VALUE_NUMBER, VALUE_STRING, VALUE_NULL),
+ DOUBLE(VALUE_NUMBER, VALUE_STRING),
+ DOUBLE_OR_NULL(VALUE_NUMBER, VALUE_STRING, VALUE_NULL),
+ LONG(VALUE_NUMBER, VALUE_STRING),
+ LONG_OR_NULL(VALUE_NUMBER, VALUE_STRING, VALUE_NULL),
+ INT(VALUE_NUMBER, VALUE_STRING),
+ INT_OR_NULL(VALUE_NUMBER, VALUE_STRING, VALUE_NULL),
+ BOOLEAN(VALUE_BOOLEAN, VALUE_STRING),
+ STRING_ARRAY(START_ARRAY, VALUE_STRING),
+ FLOAT_ARRAY(START_ARRAY, VALUE_NUMBER, VALUE_STRING),
+ DOUBLE_ARRAY(START_ARRAY, VALUE_NUMBER, VALUE_STRING),
+ LONG_ARRAY(START_ARRAY, VALUE_NUMBER, VALUE_STRING),
+ INT_ARRAY(START_ARRAY, VALUE_NUMBER, VALUE_STRING),
+ BOOLEAN_ARRAY(START_ARRAY, VALUE_BOOLEAN),
+ OBJECT(START_OBJECT),
+ OBJECT_OR_NULL(START_OBJECT, VALUE_NULL),
+ OBJECT_ARRAY(START_OBJECT, START_ARRAY),
+ OBJECT_OR_BOOLEAN(START_OBJECT, VALUE_BOOLEAN),
+ OBJECT_OR_STRING(START_OBJECT, VALUE_STRING),
+ OBJECT_OR_LONG(START_OBJECT, VALUE_NUMBER),
+ OBJECT_ARRAY_BOOLEAN_OR_STRING(START_OBJECT, START_ARRAY, VALUE_BOOLEAN, VALUE_STRING),
+ OBJECT_ARRAY_OR_STRING(START_OBJECT, START_ARRAY, VALUE_STRING),
+ VALUE(VALUE_BOOLEAN, VALUE_NULL, VALUE_EMBEDDED_OBJECT, VALUE_NUMBER, VALUE_STRING),
+ VALUE_OBJECT_ARRAY(VALUE_BOOLEAN, VALUE_NULL, VALUE_EMBEDDED_OBJECT, VALUE_NUMBER, VALUE_STRING, START_OBJECT, START_ARRAY),
+ VALUE_ARRAY(VALUE_BOOLEAN, VALUE_NULL, VALUE_NUMBER, VALUE_STRING, START_ARRAY);
+
+ private final EnumSet tokens;
+
+ ValueType(XContentParser.Token first, XContentParser.Token... rest) {
+ this.tokens = EnumSet.of(first, rest);
+ }
+
+ public EnumSet supportedTokens() {
+ return this.tokens;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ObjectParser{" +
+ "name='" + name + '\'' +
+ ", fields=" + fieldParserMap.values() +
+ '}';
+ }
+}
diff --git a/elx-http/src/main/java/org/xbib/elx/http/util/XContentParseException.java b/elx-http/src/main/java/org/xbib/elx/http/util/XContentParseException.java
new file mode 100644
index 0000000..254179f
--- /dev/null
+++ b/elx-http/src/main/java/org/xbib/elx/http/util/XContentParseException.java
@@ -0,0 +1,47 @@
+package org.xbib.elx.http.util;
+
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.xcontent.XContentLocation;
+
+import java.util.Optional;
+
+/**
+ * Thrown when one of the XContent parsers cannot parse something.
+ */
+public class XContentParseException extends IllegalArgumentException {
+
+ private final Optional location;
+
+ public XContentParseException(String message) {
+ this(null, message);
+ }
+
+ public XContentParseException(XContentLocation location, String message) {
+ super(message);
+ this.location = Optional.ofNullable(location);
+ }
+
+ public XContentParseException(XContentLocation location, String message, Exception cause) {
+ super(message, cause);
+ this.location = Optional.ofNullable(location);
+ }
+
+ public int getLineNumber() {
+ return location.map(l -> l.lineNumber).orElse(-1);
+ }
+
+ public int getColumnNumber() {
+ return location.map(l -> l.columnNumber).orElse(-1);
+ }
+
+ @Nullable
+ public XContentLocation getLocation() {
+ return location.orElse(null);
+ }
+
+ @Override
+ public String getMessage() {
+ return location.map(l -> "[" + l.toString() + "] ").orElse("") + super.getMessage();
+ }
+
+}
diff --git a/elx-http/src/main/java/org/xbib/elx/http/util/XContentParserUtils.java b/elx-http/src/main/java/org/xbib/elx/http/util/XContentParserUtils.java
new file mode 100644
index 0000000..fb78890
--- /dev/null
+++ b/elx-http/src/main/java/org/xbib/elx/http/util/XContentParserUtils.java
@@ -0,0 +1,68 @@
+package org.xbib.elx.http.util;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.xcontent.XContentLocation;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.search.aggregations.Aggregation;
+import org.xbib.elx.http.util.aggregations.ParsedStringTerms;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class XContentParserUtils {
+
+ private static final NamedXContentRegistry xContentRegistry = new NamedXContentRegistry(getDefaultNamedXContents());
+
+ public static void ensureExpectedToken(XContentParser.Token expected, XContentParser.Token actual, Supplier location) {
+ if (actual != expected) {
+ String message = "Failed to parse object: expecting token of type [%s] but found [%s]";
+ throw new ElasticsearchException(location.get() + ":" + String.format(Locale.ROOT, message, expected, actual));
+ }
+ }
+
+ public static void parseTypedKeysObject(XContentParser parser, String delimiter, Class objectClass, Consumer consumer)
+ throws IOException {
+ if (parser.currentToken() != XContentParser.Token.START_OBJECT && parser.currentToken() != XContentParser.Token.START_ARRAY) {
+ throwUnknownToken(parser.currentToken(), parser.getTokenLocation());
+ }
+ String currentFieldName = parser.currentName();
+ if (Strings.hasLength(currentFieldName)) {
+ int position = currentFieldName.indexOf(delimiter);
+ if (position > 0) {
+ String type = currentFieldName.substring(0, position);
+ String name = currentFieldName.substring(position + 1);
+ consumer.accept(namedObject(parser, objectClass, type, name));
+ return;
+ }
+ // if we didn't find a delimiter we ignore the object or array for forward compatibility instead of throwing an error
+ parser.skipChildren();
+ } else {
+ throw new ElasticsearchException(parser.getTokenLocation() + ":" + "Failed to parse object: empty key");
+ }
+ }
+
+ public static void throwUnknownToken(XContentParser.Token token, XContentLocation location) {
+ String message = "Failed to parse object: unexpected token [%s] found";
+ throw new ElasticsearchException(location + ":" + String.format(Locale.ROOT, message, token));
+ }
+
+ static T namedObject(XContentParser parser, Class categoryClass, String name, Object context) throws IOException {
+ return xContentRegistry.parseNamedObject(categoryClass, name, parser, context);
+ }
+
+ public static List getDefaultNamedXContents() {
+ Map> map = new HashMap<>();
+ //map.put("terms", (p, c) -> ParsedStringTerms.fromXContent(p, (String) c));
+ return map.entrySet().stream()
+ .map(entry -> new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(entry.getKey()), entry.getValue()))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/CommonFields.java b/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/CommonFields.java
new file mode 100644
index 0000000..27c1c55
--- /dev/null
+++ b/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/CommonFields.java
@@ -0,0 +1,18 @@
+package org.xbib.elx.http.util.aggregations;
+
+import org.elasticsearch.common.ParseField;
+
+final class CommonFields {
+ public static final ParseField META = new ParseField("meta");
+ public static final ParseField BUCKETS = new ParseField("buckets");
+ public static final ParseField VALUE = new ParseField("value");
+ public static final ParseField VALUES = new ParseField("values");
+ public static final ParseField VALUE_AS_STRING = new ParseField("value_as_string");
+ public static final ParseField DOC_COUNT = new ParseField("doc_count");
+ public static final ParseField KEY = new ParseField("key");
+ public static final ParseField KEY_AS_STRING = new ParseField("key_as_string");
+ public static final ParseField FROM = new ParseField("from");
+ public static final ParseField FROM_AS_STRING = new ParseField("from_as_string");
+ public static final ParseField TO = new ParseField("to");
+ public static final ParseField TO_AS_STRING = new ParseField("to_as_string");
+}
diff --git a/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/ParsedAggregation.java b/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/ParsedAggregation.java
new file mode 100644
index 0000000..b110aa6
--- /dev/null
+++ b/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/ParsedAggregation.java
@@ -0,0 +1,40 @@
+package org.xbib.elx.http.util.aggregations;
+
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentParser.Token;
+import org.elasticsearch.search.aggregations.Aggregation;
+import org.xbib.elx.http.util.ObjectParser;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * An implementation of {@link Aggregation} that is parsed from a REST response.
+ * Serves as a base class for all aggregation implementations that are parsed from REST.
+ */
+public abstract class ParsedAggregation implements Aggregation {
+
+ protected static void declareAggregationFields(ObjectParser extends ParsedAggregation, Void> objectParser) {
+ objectParser.declareObject((parsedAgg, metadata) -> parsedAgg.metadata = Collections.unmodifiableMap(metadata),
+ (parser, context) -> parser.map(), CommonFields.META);
+ }
+
+ private String name;
+ protected Map metadata;
+
+ @Override
+ public final String getName() {
+ return name;
+ }
+
+ protected void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public final Map getMetaData() {
+ return metadata;
+ }
+}
\ No newline at end of file
diff --git a/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/ParsedMultiBucketAggregation.java b/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/ParsedMultiBucketAggregation.java
new file mode 100644
index 0000000..bd0c81d
--- /dev/null
+++ b/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/ParsedMultiBucketAggregation.java
@@ -0,0 +1,149 @@
+package org.xbib.elx.http.util.aggregations;
+
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.search.aggregations.Aggregations;
+import org.elasticsearch.search.aggregations.InternalAggregation;
+import org.elasticsearch.search.aggregations.InternalAggregations;
+import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
+import org.xbib.elx.http.util.CheckedBiConsumer;
+import org.xbib.elx.http.util.CheckedFunction;
+import org.xbib.elx.http.util.ObjectParser;
+import org.xbib.elx.http.util.XContentParserUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.xbib.elx.http.util.XContentParserUtils.ensureExpectedToken;
+
+public abstract class ParsedMultiBucketAggregation
+ extends ParsedAggregation implements MultiBucketsAggregation {
+
+ protected final List buckets = new ArrayList<>();
+
+ protected boolean keyed = false;
+
+ protected static void declareMultiBucketAggregationFields(final ObjectParser extends ParsedMultiBucketAggregation, Void> objectParser,
+ final CheckedFunction bucketParser,
+ final CheckedFunction keyedBucketParser) {
+ declareAggregationFields(objectParser);
+ objectParser.declareField((parser, aggregation, context) -> {
+ XContentParser.Token token = parser.currentToken();
+ if (token == XContentParser.Token.START_OBJECT) {
+ aggregation.keyed = true;
+ while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
+ aggregation.buckets.add(keyedBucketParser.apply(parser));
+ }
+ } else if (token == XContentParser.Token.START_ARRAY) {
+ aggregation.keyed = false;
+ while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
+ aggregation.buckets.add(bucketParser.apply(parser));
+ }
+ }
+ }, CommonFields.BUCKETS, ObjectParser.ValueType.OBJECT_ARRAY);
+ }
+
+ public abstract static class ParsedBucket implements MultiBucketsAggregation.Bucket {
+
+ private Aggregations aggregations;
+ private String keyAsString;
+ private long docCount;
+ private boolean keyed;
+
+ protected void setKeyAsString(String keyAsString) {
+ this.keyAsString = keyAsString;
+ }
+
+ @Override
+ public String getKeyAsString() {
+ return keyAsString;
+ }
+
+ protected void setDocCount(long docCount) {
+ this.docCount = docCount;
+ }
+
+ @Override
+ public long getDocCount() {
+ return docCount;
+ }
+
+ public void setKeyed(boolean keyed) {
+ this.keyed = keyed;
+ }
+
+ protected boolean isKeyed() {
+ return keyed;
+ }
+
+ protected void setAggregations(Aggregations aggregations) {
+ this.aggregations = aggregations;
+ }
+
+ @Override
+ public Aggregations getAggregations() {
+ return aggregations;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ /*if (keyed) {
+ builder.startObject(getKeyAsString());
+ } else {
+ builder.startObject();
+ }
+ if (keyAsString != null) {
+ builder.field(CommonFields.KEY_AS_STRING.getPreferredName(), getKeyAsString());
+ }
+ keyToXContent(builder);
+ builder.field(CommonFields.DOC_COUNT.getPreferredName(), docCount);
+ aggregations.toXContentInternal(builder, params);
+ builder.endObject();*/
+ return builder;
+ }
+
+ protected XContentBuilder keyToXContent(XContentBuilder builder) throws IOException {
+ return builder.field(CommonFields.KEY.getPreferredName(), getKey());
+ }
+
+ protected static B parseXContent(final XContentParser parser,
+ final boolean keyed,
+ final Supplier bucketSupplier,
+ final CheckedBiConsumer keyConsumer)
+ throws IOException {
+ final B bucket = bucketSupplier.get();
+ bucket.setKeyed(keyed);
+ XContentParser.Token token = parser.currentToken();
+ String currentFieldName = parser.currentName();
+ if (keyed) {
+ ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation);
+ ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
+ }
+ List aggregations = new ArrayList<>();
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+ } else if (token.isValue()) {
+ if (CommonFields.KEY_AS_STRING.getPreferredName().equals(currentFieldName)) {
+ bucket.setKeyAsString(parser.text());
+ } else if (CommonFields.KEY.getPreferredName().equals(currentFieldName)) {
+ keyConsumer.accept(parser, bucket);
+ } else if (CommonFields.DOC_COUNT.getPreferredName().equals(currentFieldName)) {
+ bucket.setDocCount(parser.longValue());
+ }
+ } else if (token == XContentParser.Token.START_OBJECT) {
+ if (CommonFields.KEY.getPreferredName().equals(currentFieldName)) {
+ keyConsumer.accept(parser, bucket);
+ } else {
+ XContentParserUtils.parseTypedKeysObject(parser, "#", InternalAggregation.class,
+ aggregations::add);
+ }
+ }
+ }
+ bucket.setAggregations(new InternalAggregations(aggregations));
+ return bucket;
+ }
+ }
+}
diff --git a/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/ParsedStringTerms.java b/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/ParsedStringTerms.java
new file mode 100644
index 0000000..b2f759b
--- /dev/null
+++ b/elx-http/src/main/java/org/xbib/elx/http/util/aggregations/ParsedStringTerms.java
@@ -0,0 +1,103 @@
+package org.xbib.elx.http.util.aggregations;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.xbib.elx.http.util.ObjectParser;
+
+import java.io.IOException;
+import java.nio.CharBuffer;
+import java.util.List;
+
+public class ParsedStringTerms extends ParsedTerms {
+
+ public String getType() {
+ return "terms";
+ }
+
+ private static ObjectParser