ソースを参照

Use ObjectParser in highlighting

Nik Everett 9 年 前
コミット
c7780e6e0a

+ 0 - 5
buildSrc/src/main/resources/checkstyle_suppressions.xml

@@ -373,7 +373,6 @@
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]util[/\\]concurrent[/\\]PrioritizedEsThreadPoolExecutor.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]util[/\\]concurrent[/\\]ThreadBarrier.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]util[/\\]concurrent[/\\]ThreadContext.java" checks="LineLength" />
-  <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]xcontent[/\\]ObjectParser.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]xcontent[/\\]XContentBuilder.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]xcontent[/\\]XContentFactory.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]xcontent[/\\]XContentHelper.java" checks="LineLength" />
@@ -828,9 +827,7 @@
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]fetch[/\\]innerhits[/\\]InnerHitsParseElement.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]fetch[/\\]script[/\\]ScriptFieldsParseElement.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]fetch[/\\]source[/\\]FetchSourceContext.java" checks="LineLength" />
-  <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]highlight[/\\]AbstractHighlighterBuilder.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]highlight[/\\]FastVectorHighlighter.java" checks="LineLength" />
-  <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]highlight[/\\]HighlightBuilder.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]highlight[/\\]HighlightPhase.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]highlight[/\\]HighlightUtils.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]highlight[/\\]HighlighterParseElement.java" checks="LineLength" />
@@ -1051,7 +1048,6 @@
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]util[/\\]LongObjectHashMapTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]util[/\\]concurrent[/\\]EsExecutorsTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]util[/\\]concurrent[/\\]PrioritizedExecutorsTests.java" checks="LineLength" />
-  <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]xcontent[/\\]ObjectParserTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]xcontent[/\\]XContentFactoryTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]xcontent[/\\]builder[/\\]XContentBuilderTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]common[/\\]xcontent[/\\]cbor[/\\]JsonVsCborTests.java" checks="LineLength" />
@@ -1347,7 +1343,6 @@
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]geo[/\\]GeoBoundingBoxIT.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]geo[/\\]GeoFilterIT.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]geo[/\\]GeoShapeQueryTests.java" checks="LineLength" />
-  <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]highlight[/\\]HighlightBuilderTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]highlight[/\\]HighlighterSearchIT.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]innerhits[/\\]InnerHitsIT.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]matchedqueries[/\\]MatchedQueriesIT.java" checks="LineLength" />

+ 187 - 33
core/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java

@@ -23,6 +23,7 @@ import org.elasticsearch.common.ParseFieldMatcher;
 import org.elasticsearch.common.ParsingException;
 
 import java.io.IOException;
+import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.EnumSet;
@@ -31,17 +32,37 @@ 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 Object parser to parse any kind of XContent structures into existing object with setters.
- * The Parser is designed to be declarative and stateless. A single parser is defined for one object level, nested
- * elements can be added via {@link #declareObject(BiConsumer, BiFunction, ParseField)} which is commonly done by
- * declaring yet another instance of {@link ObjectParser}. Instances of {@link ObjectParser} are thread-safe and can be
- * re-used across parsing operations. It's 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.
+ * A declarative Object parser to parse any kind of XContent structures into existing object with setters. The Parser is designed to be
+ * declarative and stateless. A single parser is defined for one object level, nested elements can be added via
+ * {@link #declareObject(BiConsumer, BiFunction, ParseField)} which is commonly done by declaring yet another instance of
+ * {@link ObjectParser}. Instances of {@link ObjectParser} are thread-safe and can be re-used across parsing operations. It's 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<Value, Context> implements BiFunction<XContentParser, Context, Value> {
+    /**
+     * Adapts an array (or varags) setter into a list setter.
+     */
+    public static <Value, ElementValue> BiConsumer<Value, List<ElementValue>> fromList(Class<ElementValue> c,
+            BiConsumer<Value, ElementValue[]> consumer) {
+        return (Value v, List<ElementValue> l) -> {
+            @SuppressWarnings("unchecked")
+            ElementValue[] array = (ElementValue[]) Array.newInstance(c, l.size());
+            consumer.accept(v, l.toArray(array));
+        };
+    }
 
     private final String name;
     private final Supplier<Value> valueSupplier;
@@ -125,12 +146,14 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
         return value;
     }
 
-    private void parseArray(XContentParser parser, FieldParser<Value> fieldParser, String currentFieldName, Value value, Context context) throws IOException {
+    private void parseArray(XContentParser parser, FieldParser<Value> 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<Value> fieldParser, String currentFieldName, Value value, Context context) throws IOException {
+    private void parseValue(XContentParser parser, FieldParser<Value> fieldParser, String currentFieldName, Value value, Context context)
+            throws IOException {
         try {
             fieldParser.parser.parse(parser, value, context);
         } catch (Exception ex) {
@@ -138,7 +161,8 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
         }
     }
 
-    private void parseSub(XContentParser parser, FieldParser<Value> fieldParser, String currentFieldName, Value value, Context context) throws IOException {
+    private void parseSub(XContentParser parser, FieldParser<Value> fieldParser, String currentFieldName, Value value, Context context)
+            throws IOException {
         final XContentParser.Token token = parser.currentToken();
         switch (token) {
             case START_OBJECT:
@@ -237,12 +261,14 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
         declareField((p, v, c) -> consumer.accept(v, objectParser.apply(p, c)), field, ValueType.OBJECT);
     }
 
-    public <T> void declareObjectArray(BiConsumer<Value, List<T>> consumer, BiFunction<XContentParser, Context, T> objectParser, ParseField field) {
+    public <T> void declareObjectArray(BiConsumer<Value, List<T>> consumer, BiFunction<XContentParser, Context, T> objectParser,
+            ParseField field) {
         declareField((p, v, c) -> consumer.accept(v, parseArray(p, () -> objectParser.apply(p, c))), field, ValueType.OBJECT_ARRAY);
     }
 
 
-    public <T> void declareObjectOrDefault(BiConsumer<Value, T> consumer, BiFunction<XContentParser, Context, T> objectParser, Supplier<T> defaultValue, ParseField field) {
+    public <T> void declareObjectOrDefault(BiConsumer<Value, T> consumer, BiFunction<XContentParser, Context, T> objectParser,
+            Supplier<T> defaultValue, ParseField field) {
         declareField((p, v, c) -> {
             if (p.currentToken() == XContentParser.Token.VALUE_BOOLEAN) {
                 if (p.booleanValue()) {
@@ -251,7 +277,7 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
             } else {
                 consumer.accept(v, objectParser.apply(p, c));
             }
-        }, field, ValueType.OBJECT_OR_BOOLEAN);
+        } , field, ValueType.OBJECT_OR_BOOLEAN);
     }
 
 
@@ -280,13 +306,135 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
     }
 
     public void declareStringOrNull(BiConsumer<Value, String> consumer, ParseField field) {
-        declareField((p, v, c) -> consumer.accept(v, p.currentToken() == XContentParser.Token.VALUE_NULL ? null : p.text()), field, ValueType.STRING_OR_NULL);
+        declareField((p, v, c) -> consumer.accept(v, p.currentToken() == XContentParser.Token.VALUE_NULL ? null : p.text()), field,
+                ValueType.STRING_OR_NULL);
     }
 
     public void declareBoolean(BiConsumer<Value, Boolean> consumer, ParseField field) {
         declareField((p, v, c) -> consumer.accept(v, p.booleanValue()), field, ValueType.BOOLEAN);
     }
 
+    /**
+     * Declares named objects in the style of highlighting's field element. These are usually named inside and object like this:
+     * <pre><code>
+     * {
+     *   "highlight": {
+     *     "fields": {        &lt;------ this one
+     *       "title": {},
+     *       "body": {},
+     *       "category": {}
+     *     }
+     *   }
+     * }
+     * </code></pre>
+     * but, when order is important, some may be written this way:
+     * <pre><code>
+     * {
+     *   "highlight": {
+     *     "fields": [        &lt;------ this one
+     *       {"title": {}},
+     *       {"body": {}},
+     *       {"category": {}}
+     *     ]
+     *   }
+     * }
+     * </code></pre>
+     * This is because json doesn't enforce ordering. Elasticsearch reads it in the order sent but tools that generate json are free to put
+     * object members in an unordered Map, jumbling them. Thus, if you care about order you can send the object in the second way.
+     *
+     * See NamedObjectHolder in ObjectParserTests for examples of how to invoke this.
+     *
+     * @param consumer sets the values once they have been parsed
+     * @param namedObjectParser parses each named object
+     * @param orderedModeCallback called when the named object is parsed using the "ordered" mode (the array of objects)
+     * @param field the field to parse
+     */
+    public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
+            Consumer<Value> orderedModeCallback, ParseField field) {
+        // This creates and parses the named object
+        BiFunction<XContentParser, Context, T> objectParser = (XContentParser p, Context c) -> {
+            if (p.currentToken() != XContentParser.Token.FIELD_NAME) {
+                throw new ParsingException(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 ParsingException(p.getTokenLocation(), "[" + field + "] failed to parse field [" + name + "]", e);
+                }
+            } catch (IOException e) {
+                throw new ParsingException(p.getTokenLocation(), "[" + field + "] error while parsing", e);
+            }
+        };
+        declareField((XContentParser p, Value v, Context c) -> {
+            List<T> 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 ParsingException(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 ParsingException(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);
+    }
+
+    /**
+     * Declares named objects in the style of aggregations. These are named inside and object like this:
+     * <pre><code>
+     * {
+     *   "aggregations": {
+     *     "name_1": { "aggregation_type": {} },
+     *     "name_2": { "aggregation_type": {} },
+     *     "name_3": { "aggregation_type": {} }
+     *     }
+     *   }
+     * }
+     * </code></pre>
+     * Unlike the other version of this method, "ordered" mode (arrays of objects) is not supported.
+     *
+     * See NamedObjectHolder in ObjectParserTests for examples of how to invoke this.
+     *
+     * @param consumer sets the values once they have been parsed
+     * @param namedObjectParser parses each named object
+     * @param field the field to parse
+     */
+    public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedObjectParser<T, Context> namedObjectParser,
+            ParseField field) {
+        Consumer<Value> orderedModeCallback = (Value 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, Context> {
+        T parse(XContentParser p, Context c, String name) throws IOException;
+    }
+
     public static class FieldParser<T> {
         private final Parser parser;
         private final EnumSet<XContentParser.Token> supportedTokens;
@@ -305,7 +453,8 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
                 throw new IllegalStateException("[" + parserName  + "] parsefield doesn't accept: " + currentFieldName);
             }
             if (supportedTokens.contains(token) == false) {
-                throw new IllegalArgumentException("[" + parserName  + "] " + currentFieldName + " doesn't support values of type: " + token);
+                throw new IllegalArgumentException(
+                        "[" + parserName + "] " + currentFieldName + " doesn't support values of type: " + token);
             }
         }
 
@@ -313,10 +462,14 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
         public String toString() {
             String[] deprecatedNames = parseField.getDeprecatedNames();
             String allReplacedWith = parseField.getAllReplacedWith();
+            String deprecated = "";
+            if (deprecatedNames != null && deprecatedNames.length > 0) {
+                deprecated = ", deprecated_names="  + Arrays.toString(deprecatedNames);
+            }
             return "FieldParser{" +
                     "preferred_name=" + parseField.getPreferredName() +
                     ", supportedTokens=" + supportedTokens +
-                    (deprecatedNames == null || deprecatedNames.length == 0 ? "" : ", deprecated_names="  + Arrays.toString(deprecatedNames )) +
+                    deprecated +
                     (allReplacedWith == null ? "" : ", replaced_with=" + allReplacedWith) +
                     ", type=" + type.name() +
                     '}';
@@ -325,27 +478,28 @@ public final class ObjectParser<Value, Context> implements BiFunction<XContentPa
     }
 
     public enum ValueType {
-        STRING(EnumSet.of(XContentParser.Token.VALUE_STRING)),
-        STRING_OR_NULL(EnumSet.of(XContentParser.Token.VALUE_STRING, XContentParser.Token.VALUE_NULL)),
-        FLOAT(EnumSet.of(XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
-        DOUBLE(EnumSet.of(XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
-        LONG(EnumSet.of(XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
-        INT(EnumSet.of(XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
-        BOOLEAN(EnumSet.of(XContentParser.Token.VALUE_BOOLEAN)), STRING_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_STRING)),
-        FLOAT_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
-        DOUBLE_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
-        LONG_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
-        INT_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_NUMBER, XContentParser.Token.VALUE_STRING)),
-        BOOLEAN_ARRAY(EnumSet.of(XContentParser.Token.START_ARRAY, XContentParser.Token.VALUE_BOOLEAN)),
-        OBJECT(EnumSet.of(XContentParser.Token.START_OBJECT)),
-        OBJECT_ARRAY(EnumSet.of(XContentParser.Token.START_OBJECT, XContentParser.Token.START_ARRAY)),
-        OBJECT_OR_BOOLEAN(EnumSet.of(XContentParser.Token.START_OBJECT, XContentParser.Token.VALUE_BOOLEAN)),
-        VALUE(EnumSet.of(XContentParser.Token.VALUE_BOOLEAN, XContentParser.Token.VALUE_NULL ,XContentParser.Token.VALUE_EMBEDDED_OBJECT,XContentParser.Token.VALUE_NUMBER,XContentParser.Token.VALUE_STRING));
+        STRING(VALUE_STRING),
+        STRING_OR_NULL(VALUE_STRING, VALUE_NULL),
+        FLOAT(VALUE_NUMBER, VALUE_STRING),
+        DOUBLE(VALUE_NUMBER, VALUE_STRING),
+        LONG(VALUE_NUMBER, VALUE_STRING),
+        INT(VALUE_NUMBER, VALUE_STRING),
+        BOOLEAN(VALUE_BOOLEAN),
+        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_ARRAY(START_OBJECT, START_ARRAY),
+        OBJECT_OR_BOOLEAN(START_OBJECT, VALUE_BOOLEAN),
+        VALUE(VALUE_BOOLEAN, VALUE_NULL, VALUE_EMBEDDED_OBJECT, VALUE_NUMBER, VALUE_STRING);
 
         private final EnumSet<XContentParser.Token> tokens;
 
-        ValueType(EnumSet<XContentParser.Token> tokens) {
-            this.tokens = tokens;
+        ValueType(XContentParser.Token first, XContentParser.Token... rest) {
+            this.tokens = EnumSet.of(first, rest);
         }
 
         public EnumSet<XContentParser.Token> supportedTokens() {

+ 2 - 7
core/src/main/java/org/elasticsearch/index/query/support/InnerHitBuilder.java

@@ -98,13 +98,8 @@ public final class InnerHitBuilder extends ToXContentToBytes implements Writeabl
                 throw new ParsingException(p.getTokenLocation(), "Could not parse inner _source definition", e);
             }
         }, SearchSourceBuilder._SOURCE_FIELD, ObjectParser.ValueType.OBJECT_OR_BOOLEAN);
-        PARSER.declareObject(InnerHitBuilder::setHighlightBuilder, (p, c) -> {
-            try {
-                return HighlightBuilder.PROTOTYPE.fromXContent(c);
-            } catch (IOException e) {
-                throw new ParsingException(p.getTokenLocation(), "Could not parse inner highlight definition", e);
-            }
-        }, SearchSourceBuilder.HIGHLIGHT_FIELD);
+        PARSER.declareObject(InnerHitBuilder::setHighlightBuilder, (p, c) -> HighlightBuilder.fromXContent(c),
+                SearchSourceBuilder.HIGHLIGHT_FIELD);
         PARSER.declareObject(InnerHitBuilder::setQuery, (p, c) ->{
             try {
                 return c.parseInnerQueryBuilder();

+ 1 - 1
core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsParser.java

@@ -121,7 +121,7 @@ public class TopHitsParser implements Aggregator.Parser {
                     }
                     factory.scriptFields(scriptFields);
                 } else if (context.parseFieldMatcher().match(currentFieldName, SearchSourceBuilder.HIGHLIGHT_FIELD)) {
-                    factory.highlighter(HighlightBuilder.PROTOTYPE.fromXContent(context));
+                    factory.highlighter(HighlightBuilder.fromXContent(context));
                 } else if (context.parseFieldMatcher().match(currentFieldName, SearchSourceBuilder.SORT_FIELD)) {
                     List<SortBuilder<?>> sorts = SortBuilder.fromXContent(context);
                     factory.sorts(sorts);

+ 1 - 1
core/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java

@@ -863,7 +863,7 @@ public final class SearchSourceBuilder extends ToXContentToBytes implements Writ
                 } else if (context.parseFieldMatcher().match(currentFieldName, AGGREGATIONS_FIELD)) {
                     aggregations = aggParsers.parseAggregators(parser, context);
                 } else if (context.parseFieldMatcher().match(currentFieldName, HIGHLIGHT_FIELD)) {
-                    highlightBuilder = HighlightBuilder.PROTOTYPE.fromXContent(context);
+                    highlightBuilder = HighlightBuilder.fromXContent(context);
                 } else if (context.parseFieldMatcher().match(currentFieldName, INNER_HITS_FIELD)) {
                     innerHitsBuilder = InnerHitsBuilder.fromXContent(parser, context);
                 } else if (context.parseFieldMatcher().match(currentFieldName, SUGGEST_FIELD)) {

+ 44 - 93
core/src/main/java/org/elasticsearch/search/highlight/AbstractHighlighterBuilder.java

@@ -26,6 +26,7 @@ import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.ParsingException;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ObjectParser;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.index.query.QueryBuilder;
@@ -33,11 +34,12 @@ import org.elasticsearch.index.query.QueryParseContext;
 import org.elasticsearch.search.highlight.HighlightBuilder.Order;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.function.BiFunction;
+
+import static org.elasticsearch.common.xcontent.ObjectParser.fromList;
 
 /**
  * This abstract class holds parameters shared by {@link HighlightBuilder} and {@link HighlightBuilder.Field}
@@ -49,7 +51,6 @@ public abstract class AbstractHighlighterBuilder<HB extends AbstractHighlighterB
     public static final ParseField POST_TAGS_FIELD = new ParseField("post_tags");
     public static final ParseField FIELDS_FIELD = new ParseField("fields");
     public static final ParseField ORDER_FIELD = new ParseField("order");
-    public static final ParseField TAGS_SCHEMA_FIELD = new ParseField("tags_schema");
     public static final ParseField HIGHLIGHT_FILTER_FIELD = new ParseField("highlight_filter");
     public static final ParseField FRAGMENT_SIZE_FIELD = new ParseField("fragment_size");
     public static final ParseField FRAGMENT_OFFSET_FIELD = new ParseField("fragment_offset");
@@ -444,100 +445,50 @@ public abstract class AbstractHighlighterBuilder<HB extends AbstractHighlighterB
         }
     }
 
-    /**
-     * Creates a new {@link HighlightBuilder} from the highlighter held by the {@link QueryParseContext}
-     * in {@link org.elasticsearch.common.xcontent.XContent} format
-     *
-     * @param parseContext containing the parser positioned at the structure to be parsed from.
-     * the state on the parser contained in this context will be changed as a side effect of this
-     * method call
-     * @return the new {@link AbstractHighlighterBuilder}
-     */
-    public HB fromXContent(QueryParseContext parseContext) throws IOException {
-        XContentParser parser = parseContext.parser();
-        XContentParser.Token token = parser.currentToken();
-        String currentFieldName = null;
-        HB highlightBuilder = createInstance(parser);
-        while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
-            if (token == XContentParser.Token.FIELD_NAME) {
-                currentFieldName = parser.currentName();
-            } else if (token == XContentParser.Token.START_ARRAY) {
-                if (parseContext.parseFieldMatcher().match(currentFieldName, PRE_TAGS_FIELD)) {
-                    List<String> preTagsList = new ArrayList<>();
-                    while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
-                        preTagsList.add(parser.text());
-                    }
-                    highlightBuilder.preTags(preTagsList.toArray(new String[preTagsList.size()]));
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, POST_TAGS_FIELD)) {
-                    List<String> postTagsList = new ArrayList<>();
-                    while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
-                        postTagsList.add(parser.text());
-                    }
-                    highlightBuilder.postTags(postTagsList.toArray(new String[postTagsList.size()]));
-                } else if (false == highlightBuilder.doFromXContent(parseContext, currentFieldName, token)) {
-                    throw new ParsingException(parser.getTokenLocation(), "cannot parse array with name [{}]", currentFieldName);
-                }
-            } else if (token.isValue()) {
-                if (parseContext.parseFieldMatcher().match(currentFieldName, ORDER_FIELD)) {
-                    highlightBuilder.order(Order.fromString(parser.text()));
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, HIGHLIGHT_FILTER_FIELD)) {
-                    highlightBuilder.highlightFilter(parser.booleanValue());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, FRAGMENT_SIZE_FIELD)) {
-                    highlightBuilder.fragmentSize(parser.intValue());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, NUMBER_OF_FRAGMENTS_FIELD)) {
-                    highlightBuilder.numOfFragments(parser.intValue());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, REQUIRE_FIELD_MATCH_FIELD)) {
-                    highlightBuilder.requireFieldMatch(parser.booleanValue());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, BOUNDARY_MAX_SCAN_FIELD)) {
-                    highlightBuilder.boundaryMaxScan(parser.intValue());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, BOUNDARY_CHARS_FIELD)) {
-                    highlightBuilder.boundaryChars(parser.text().toCharArray());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, TYPE_FIELD)) {
-                    highlightBuilder.highlighterType(parser.text());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, FRAGMENTER_FIELD)) {
-                    highlightBuilder.fragmenter(parser.text());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, NO_MATCH_SIZE_FIELD)) {
-                    highlightBuilder.noMatchSize(parser.intValue());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, FORCE_SOURCE_FIELD)) {
-                    highlightBuilder.forceSource(parser.booleanValue());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, PHRASE_LIMIT_FIELD)) {
-                    highlightBuilder.phraseLimit(parser.intValue());
-                } else if (false == highlightBuilder.doFromXContent(parseContext, currentFieldName, token)) {
-                    throw new ParsingException(parser.getTokenLocation(), "unexpected fieldname [{}]", currentFieldName);
-                }
-            } else if (token == XContentParser.Token.START_OBJECT && currentFieldName != null) {
-                if (parseContext.parseFieldMatcher().match(currentFieldName, OPTIONS_FIELD)) {
-                    highlightBuilder.options(parser.map());
-                } else if (parseContext.parseFieldMatcher().match(currentFieldName, HIGHLIGHT_QUERY_FIELD)) {
-                    highlightBuilder.highlightQuery(parseContext.parseInnerQueryBuilder());
-                } else if (false == highlightBuilder.doFromXContent(parseContext, currentFieldName, token)) {
-                    throw new ParsingException(parser.getTokenLocation(), "cannot parse object with name [{}]", currentFieldName);
+    static <HB extends AbstractHighlighterBuilder<HB>> BiFunction<QueryParseContext, HB, HB> setupParser(
+            ObjectParser<HB, QueryParseContext> parser) {
+        parser.declareStringArray(fromList(String.class, HB::preTags), PRE_TAGS_FIELD);
+        parser.declareStringArray(fromList(String.class, HB::postTags), POST_TAGS_FIELD);
+        parser.declareString(HB::order, ORDER_FIELD);
+        parser.declareBoolean(HB::highlightFilter, HIGHLIGHT_FILTER_FIELD);
+        parser.declareInt(HB::fragmentSize, FRAGMENT_SIZE_FIELD);
+        parser.declareInt(HB::numOfFragments, NUMBER_OF_FRAGMENTS_FIELD);
+        parser.declareBoolean(HB::requireFieldMatch, REQUIRE_FIELD_MATCH_FIELD);
+        parser.declareInt(HB::boundaryMaxScan, BOUNDARY_MAX_SCAN_FIELD);
+        parser.declareString((HB hb, String bc) -> hb.boundaryChars(bc.toCharArray()) , BOUNDARY_CHARS_FIELD);
+        parser.declareString(HB::highlighterType, TYPE_FIELD);
+        parser.declareString(HB::fragmenter, FRAGMENTER_FIELD);
+        parser.declareInt(HB::noMatchSize, NO_MATCH_SIZE_FIELD);
+        parser.declareBoolean(HB::forceSource, FORCE_SOURCE_FIELD);
+        parser.declareInt(HB::phraseLimit, PHRASE_LIMIT_FIELD);
+        parser.declareObject(HB::options, (XContentParser p, QueryParseContext c) -> {
+            try {
+                return p.map();
+            } catch (IOException e) {
+                throw new RuntimeException("Error parsing options", e);
+            }
+        }, OPTIONS_FIELD);
+        parser.declareObject(HB::highlightQuery, (XContentParser p, QueryParseContext c) -> {
+            try {
+                return c.parseInnerQueryBuilder();
+            } catch (IOException e) {
+                throw new RuntimeException("Error parsing query", e);
+            }
+        }, HIGHLIGHT_QUERY_FIELD);
+        return (QueryParseContext c, HB hb) -> {
+            try {
+                parser.parse(c.parser(), hb, c);
+                if (hb.preTags() != null && hb.postTags() == null) {
+                    throw new ParsingException(c.parser().getTokenLocation(),
+                            "pre_tags are set but post_tags are not set");
                 }
-            } else if (currentFieldName != null) {
-                throw new ParsingException(parser.getTokenLocation(), "unexpected token [{}] after [{}]", token, currentFieldName);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
             }
-        }
-
-        if (highlightBuilder.preTags() != null && highlightBuilder.postTags() == null) {
-            throw new ParsingException(parser.getTokenLocation(), "Highlighter global preTags are set, but global postTags are not set");
-        }
-        return highlightBuilder;
+            return hb;
+        };
     }
 
-    /**
-     * @param parser the input parser. Implementing classes might advance the parser depending on the
-     * information they need to instantiate a new instance
-     * @return a new instance
-     */
-    protected abstract HB createInstance(XContentParser parser) throws IOException;
-
-    /**
-     * Implementing subclasses can handle parsing special options depending on the
-     * current token, field name and the parse context.
-     * @return <tt>true</tt> if an option was found and successfully parsed, otherwise <tt>false</tt>
-     */
-    protected abstract boolean doFromXContent(QueryParseContext parseContext, String currentFieldName, XContentParser.Token endMarkerToken) throws IOException;
-
     @Override
     public final int hashCode() {
         return Objects.hash(getClass(), Arrays.hashCode(preTags), Arrays.hashCode(postTags), fragmentSize,

+ 41 - 78
core/src/main/java/org/elasticsearch/search/highlight/HighlightBuilder.java

@@ -25,9 +25,16 @@ import org.elasticsearch.common.ParsingException;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.ExceptionsHelper;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ObjectParser;
+import org.elasticsearch.common.xcontent.ObjectParser.NamedObjectParser;
+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.index.query.QueryBuilder;
 import org.elasticsearch.index.query.QueryParseContext;
 import org.elasticsearch.index.query.QueryShardContext;
@@ -43,6 +50,9 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.BiFunction;
+
+import static org.elasticsearch.common.xcontent.ObjectParser.fromList;
 
 /**
  * A builder for search highlighting. Settings can control how large fields
@@ -157,6 +167,10 @@ public class HighlightBuilder extends AbstractHighlighterBuilder<HighlightBuilde
         return this;
     }
 
+    void fields(List<Field> fields) {
+        this.fields.addAll(fields);
+    }
+
     public List<Field> fields() {
         return this.fields;
     }
@@ -217,46 +231,25 @@ public class HighlightBuilder extends AbstractHighlighterBuilder<HighlightBuilde
         return this.useExplicitFieldOrder;
     }
 
-    /**
-     * parse options only present in top level highlight builder (`tags_schema`, `encoder` and nested `fields`)
-     */
     @Override
-    protected boolean doFromXContent(QueryParseContext parseContext, String currentFieldName, Token currentToken) throws IOException {
-        XContentParser parser = parseContext.parser();
-        XContentParser.Token token;
-        boolean foundCurrentFieldMatch = false;
-        if (currentToken.isValue()) {
-            if (parseContext.parseFieldMatcher().match(currentFieldName, TAGS_SCHEMA_FIELD)) {
-                tagsSchema(parser.text());
-                foundCurrentFieldMatch = true;
-            } else if (parseContext.parseFieldMatcher().match(currentFieldName, ENCODER_FIELD)) {
-                encoder(parser.text());
-                foundCurrentFieldMatch = true;
-            }
-        } else if (currentToken == Token.START_ARRAY && parseContext.parseFieldMatcher().match(currentFieldName, FIELDS_FIELD)) {
-            useExplicitFieldOrder(true);
-            while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
-                if (token == XContentParser.Token.START_OBJECT) {
-                    while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
-                        if (token == XContentParser.Token.FIELD_NAME) {
-                            field(HighlightBuilder.Field.PROTOTYPE.fromXContent(parseContext));
-                        }
-                    }
-                    foundCurrentFieldMatch = true;
-                } else {
-                    throw new ParsingException(parser.getTokenLocation(),
-                            "If highlighter fields is an array it must contain objects containing a single field");
-                }
-            }
-        } else if (currentToken == Token.START_OBJECT && parseContext.parseFieldMatcher().match(currentFieldName, FIELDS_FIELD)) {
-            while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
-                if (token == XContentParser.Token.FIELD_NAME) {
-                    field(HighlightBuilder.Field.PROTOTYPE.fromXContent(parseContext));
-                }
-            }
-            foundCurrentFieldMatch = true;
-        }
-        return foundCurrentFieldMatch;
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        innerXContent(builder);
+        builder.endObject();
+        return builder;
+    }
+
+    private static final BiFunction<QueryParseContext, HighlightBuilder, HighlightBuilder> PARSER;
+    static {
+        ObjectParser<HighlightBuilder, QueryParseContext> parser = new ObjectParser<>("highlight");
+        parser.declareString(HighlightBuilder::tagsSchema, new ParseField("tags_schema"));
+        parser.declareString(HighlightBuilder::encoder, ENCODER_FIELD);
+        parser.declareNamedObjects(HighlightBuilder::fields, Field.PARSER, (HighlightBuilder hb) -> hb.useExplicitFieldOrder(true),
+                FIELDS_FIELD);
+        PARSER = setupParser(parser);
+    }
+    public static HighlightBuilder fromXContent(QueryParseContext c) {
+        return PARSER.apply(c, new HighlightBuilder());
     }
 
     public SearchContextHighlight build(QueryShardContext context) throws IOException {
@@ -388,11 +381,6 @@ public class HighlightBuilder extends AbstractHighlighterBuilder<HighlightBuilde
         }
     }
 
-    @Override
-    protected HighlightBuilder createInstance(XContentParser parser) {
-        return new HighlightBuilder();
-    }
-
     @Override
     protected int doHashCode() {
         return Objects.hash(encoder, useExplicitFieldOrder, fields);
@@ -431,6 +419,14 @@ public class HighlightBuilder extends AbstractHighlighterBuilder<HighlightBuilde
 
     public static class Field extends AbstractHighlighterBuilder<Field> implements Writeable<Field> {
         static final Field PROTOTYPE = new Field("_na_");
+        static final NamedObjectParser<Field, QueryParseContext> PARSER;
+        static {
+            ObjectParser<Field, QueryParseContext> parser = new ObjectParser<>("highlight_field");
+            parser.declareInt(Field::fragmentOffset, FRAGMENT_OFFSET_FIELD);
+            parser.declareStringArray(fromList(String.class, Field::matchedFields), MATCHED_FIELDS_FIELD);
+            BiFunction<QueryParseContext, Field, Field> decoratedParser = setupParser(parser);
+            PARSER = (XContentParser p, QueryParseContext c, String name) -> decoratedParser.apply(c, new Field(name));
+        }
 
         private final String name;
 
@@ -476,39 +472,6 @@ public class HighlightBuilder extends AbstractHighlighterBuilder<HighlightBuilde
             builder.endObject();
         }
 
-        /**
-         * parse options only present in field highlight builder (`fragment_offset`, `matched_fields`)
-         */
-        @Override
-        protected boolean doFromXContent(QueryParseContext parseContext, String currentFieldName, Token currentToken) throws IOException {
-            XContentParser parser = parseContext.parser();
-            boolean foundCurrentFieldMatch = false;
-            if (parseContext.parseFieldMatcher().match(currentFieldName, FRAGMENT_OFFSET_FIELD) && currentToken.isValue()) {
-                fragmentOffset(parser.intValue());
-                foundCurrentFieldMatch = true;
-            } else if (parseContext.parseFieldMatcher().match(currentFieldName, MATCHED_FIELDS_FIELD)
-                    && currentToken == XContentParser.Token.START_ARRAY) {
-                List<String> matchedFields = new ArrayList<>();
-                while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
-                    matchedFields.add(parser.text());
-                }
-                matchedFields(matchedFields.toArray(new String[matchedFields.size()]));
-                foundCurrentFieldMatch = true;
-            }
-            return foundCurrentFieldMatch;
-        }
-
-        @Override
-        protected Field createInstance(XContentParser parser) throws IOException {
-            if (parser.currentToken() == XContentParser.Token.FIELD_NAME) {
-                String fieldname = parser.currentName();
-                return new Field(fieldname);
-            } else {
-                throw new ParsingException(parser.getTokenLocation(), "unknown token type [{}], expected field name",
-                        parser.currentToken());
-            }
-        }
-
         @Override
         protected int doHashCode() {
             return Objects.hash(name, fragmentOffset, Arrays.hashCode(matchedFields));

+ 162 - 34
core/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java

@@ -21,6 +21,8 @@ package org.elasticsearch.common.xcontent;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.ParseFieldMatcher;
 import org.elasticsearch.common.ParsingException;
+import org.elasticsearch.common.xcontent.ObjectParser.NamedObjectParser;
+import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
 import org.elasticsearch.test.ESTestCase;
 
 import java.io.IOException;
@@ -28,10 +30,17 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
+import static org.hamcrest.Matchers.hasSize;
+
 public class ObjectParserTests extends ESTestCase {
 
     public void testBasics() throws IOException {
-        XContentParser parser = XContentType.JSON.xContent().createParser("{\"test\" : \"foo\", \"test_number\" : 2, \"testArray\":  [1,2,3,4]}");
+        XContentParser parser = XContentType.JSON.xContent().createParser(
+                  "{\n"
+                + "  \"test\" : \"foo\",\n"
+                + "  \"test_number\" : 2,\n"
+                + "  \"testArray\":  [1,2,3,4]\n"
+                + "}");
         class TestStruct {
             public String test;
             int testNumber;
@@ -44,7 +53,7 @@ public class ObjectParserTests extends ESTestCase {
                 this.ints = ints;
             }
         }
-        ObjectParser<TestStruct, Void> objectParser = new ObjectParser("foo");
+        ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
         TestStruct s = new TestStruct();
 
         objectParser.declareField((i, c, x) -> c.test = i.text(), new ParseField("test"), ObjectParser.ValueType.STRING);
@@ -55,29 +64,17 @@ public class ObjectParserTests extends ESTestCase {
         assertEquals(s.test, "foo");
         assertEquals(s.testNumber, 2);
         assertEquals(s.ints, Arrays.asList(1, 2, 3, 4));
-        assertEquals(objectParser.toString(), "ObjectParser{name='foo', fields=[FieldParser{preferred_name=test, supportedTokens=[VALUE_STRING], type=STRING}, FieldParser{preferred_name=test_number, supportedTokens=[VALUE_STRING, VALUE_NUMBER], type=INT}, FieldParser{preferred_name=test_array, supportedTokens=[START_ARRAY, VALUE_STRING, VALUE_NUMBER], type=INT_ARRAY}, FieldParser{preferred_name=test_array, supportedTokens=[START_ARRAY, VALUE_STRING, VALUE_NUMBER], type=INT_ARRAY}, FieldParser{preferred_name=test_number, supportedTokens=[VALUE_STRING, VALUE_NUMBER], type=INT}]}");
-    }
-
-    public void testEmptyObject() throws Exception {
-        XContentParser parser = XContentType.JSON.xContent().createParser("{}");
-        class TestStruct {
-            public String val = null;
-            public void setVal(String val) {
-                this.val = val;
-            }
-        }
-
-        ObjectParser<TestStruct, Void> objectParser = new ObjectParser("eggplant");
-        TestStruct s = new TestStruct();
-
-        objectParser.declareString(TestStruct::setVal, new ParseField("anything"));
-        objectParser.parse(parser, s);
-        assertNull("s.val should be null", s.val);
+        assertEquals(objectParser.toString(), "ObjectParser{name='foo', fields=["
+                + "FieldParser{preferred_name=test, supportedTokens=[VALUE_STRING], type=STRING}, "
+                + "FieldParser{preferred_name=test_number, supportedTokens=[VALUE_STRING, VALUE_NUMBER], type=INT}, "
+                + "FieldParser{preferred_name=test_array, supportedTokens=[START_ARRAY, VALUE_STRING, VALUE_NUMBER], type=INT_ARRAY}, "
+                + "FieldParser{preferred_name=test_array, supportedTokens=[START_ARRAY, VALUE_STRING, VALUE_NUMBER], type=INT_ARRAY}, "
+                + "FieldParser{preferred_name=test_number, supportedTokens=[VALUE_STRING, VALUE_NUMBER], type=INT}]}");
     }
 
     public void testObjectOrDefault() throws IOException {
         XContentParser parser = XContentType.JSON.xContent().createParser("{\"object\" : { \"test\": 2}}");
-        ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser("foo", StaticTestStruct::new);
+        ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser<>("foo", StaticTestStruct::new);
         objectParser.declareInt(StaticTestStruct::setTest, new ParseField("test"));
         objectParser.declareObjectOrDefault(StaticTestStruct::setObject, objectParser, StaticTestStruct::new, new ParseField("object"));
         StaticTestStruct s = objectParser.parse(parser);
@@ -96,13 +93,10 @@ public class ObjectParserTests extends ESTestCase {
     public void testExceptions() throws IOException {
         XContentParser parser = XContentType.JSON.xContent().createParser("{\"test\" : \"foo\"}");
         class TestStruct {
-            public int test;
-
             public void setTest(int test) {
-                this.test = test;
             }
         }
-        ObjectParser<TestStruct, TestStruct> objectParser = new ObjectParser("the_parser");
+        ObjectParser<TestStruct, TestStruct> objectParser = new ObjectParser<>("the_parser");
         TestStruct s = new TestStruct();
         objectParser.declareInt(TestStruct::setTest, new ParseField("test"));
 
@@ -128,7 +122,7 @@ public class ObjectParserTests extends ESTestCase {
         class TestStruct {
             public String test;
         }
-        ObjectParser<TestStruct, Void> objectParser = new ObjectParser("foo");
+        ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
         TestStruct s = new TestStruct();
 
         objectParser.declareField((i, v, c) -> v.test = i.text(), new ParseField("test", "old_test"), ObjectParser.ValueType.STRING);
@@ -153,7 +147,7 @@ public class ObjectParserTests extends ESTestCase {
         class TestStruct {
             public String test;
         }
-        ObjectParser<TestStruct, Void> objectParser = new ObjectParser("foo");
+        ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
         TestStruct s = new TestStruct();
 
         objectParser.declareField((i, c, x) -> c.test = i.text(), new ParseField("numeric_value"), ObjectParser.ValueType.FLOAT);
@@ -172,11 +166,11 @@ public class ObjectParserTests extends ESTestCase {
             public int test;
             TestStruct object;
         }
-        ObjectParser<TestStruct, Void> objectParser = new ObjectParser("foo");
+        ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
         TestStruct s = new TestStruct();
         s.object = new TestStruct();
-        objectParser.declareField((i, c, x) -> c.test = i.intValue(), new ParseField("test"), ObjectParser.ValueType.INT);
-        objectParser.declareField((i, c, x) -> objectParser.parse(parser, c.object), new ParseField("object"), ObjectParser.ValueType.OBJECT);
+        objectParser.declareField((i, c, x) -> c.test = i.intValue(), new ParseField("test"), ValueType.INT);
+        objectParser.declareField((i, c, x) -> objectParser.parse(parser, c.object), new ParseField("object"), ValueType.OBJECT);
         objectParser.parse(parser, s);
         assertEquals(s.test, 1);
         assertEquals(s.object.test, 2);
@@ -184,7 +178,7 @@ public class ObjectParserTests extends ESTestCase {
 
     public void testParseNestedShortcut() throws IOException {
         XContentParser parser = XContentType.JSON.xContent().createParser("{ \"test\" : 1, \"object\" : { \"test\": 2}}");
-        ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser("foo", StaticTestStruct::new);
+        ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser<>("foo", StaticTestStruct::new);
         objectParser.declareInt(StaticTestStruct::setTest, new ParseField("test"));
         objectParser.declareObject(StaticTestStruct::setObject, objectParser, new ParseField("object"));
         StaticTestStruct s = objectParser.parse(parser);
@@ -192,9 +186,26 @@ public class ObjectParserTests extends ESTestCase {
         assertEquals(s.object.test, 2);
     }
 
+    public void testEmptyObject() throws IOException {
+        XContentParser parser = XContentType.JSON.xContent().createParser("{\"object\" : {}}");
+        ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser<>("foo", StaticTestStruct::new);
+        objectParser.declareObject(StaticTestStruct::setObject, objectParser, new ParseField("object"));
+        StaticTestStruct s = objectParser.parse(parser);
+        assertNotNull(s.object);
+    }
+
+    public void testEmptyObjectInArray() throws IOException {
+        XContentParser parser = XContentType.JSON.xContent().createParser("{\"object_array\" : [{}]}");
+        ObjectParser<StaticTestStruct, Void> objectParser = new ObjectParser<>("foo", StaticTestStruct::new);
+        objectParser.declareObjectArray(StaticTestStruct::setObjectArray, objectParser, new ParseField("object_array"));
+        StaticTestStruct s = objectParser.parse(parser);
+        assertNotNull(s.objectArray);
+    }
+
     static class StaticTestStruct {
-        public int test;
+        int test;
         StaticTestStruct object;
+        List<StaticTestStruct> objectArray;
 
         public void setTest(int test) {
             this.test = test;
@@ -203,6 +214,10 @@ public class ObjectParserTests extends ESTestCase {
         public void setObject(StaticTestStruct object) {
             this.object = object;
         }
+
+        public void setObjectArray(List<StaticTestStruct> objectArray) {
+            this.objectArray = objectArray;
+        }
     }
 
     enum TestEnum {
@@ -218,7 +233,7 @@ public class ObjectParserTests extends ESTestCase {
             }
         }
         XContentParser parser = XContentType.JSON.xContent().createParser("{ \"test\" : \"FOO\" }");
-        ObjectParser<TestStruct, Void> objectParser = new ObjectParser("foo");
+        ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
         objectParser.declareString((struct, value) -> struct.set(TestEnum.valueOf(value)), new ParseField("test"));
         TestStruct s = objectParser.parse(parser, new TestStruct());
         assertEquals(s.test, TestEnum.FOO);
@@ -314,7 +329,7 @@ public class ObjectParserTests extends ESTestCase {
                 this.string_or_null = string_or_null;
             }
         }
-        ObjectParser<TestStruct, Void> objectParser = new ObjectParser("foo");
+        ObjectParser<TestStruct, Void> objectParser = new ObjectParser<>("foo");
         objectParser.declareInt(TestStruct::setInt_field, new ParseField("int_field"));
         objectParser.declareIntArray(TestStruct::setInt_array_field, new ParseField("int_array_field"));
         objectParser.declareLong(TestStruct::setLong_field, new ParseField("long_field"));
@@ -352,4 +367,117 @@ public class ObjectParserTests extends ESTestCase {
         }
     }
 
+    public void testParseNamedObject() throws IOException {
+        XContentParser parser = XContentType.JSON.xContent().createParser(
+                  "{\"named\": {\n"
+                + "  \"a\": {}"
+                + "}}");
+        NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
+        assertThat(h.named, hasSize(1));
+        assertEquals("a", h.named.get(0).name);
+        assertFalse(h.namedSuppliedInOrder);
+    }
+
+    public void testParseNamedObjectInOrder() throws IOException {
+        XContentParser parser = XContentType.JSON.xContent().createParser(
+                  "{\"named\": [\n"
+                + "  {\"a\": {}}"
+                + "]}");
+        NamedObjectHolder h = NamedObjectHolder.PARSER.apply(parser, null);
+        assertThat(h.named, hasSize(1));
+        assertEquals("a", h.named.get(0).name);
+        assertTrue(h.namedSuppliedInOrder);
+    }
+
+    public void testParseNamedObjectTwoFieldsInArray() throws IOException {
+        XContentParser parser = XContentType.JSON.xContent().createParser(
+                  "{\"named\": [\n"
+                + "  {\"a\": {}, \"b\": {}}"
+                + "]}");
+        ParsingException e = expectThrows(ParsingException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
+        assertEquals("[named_object_holder] failed to parse field [named]", e.getMessage());
+        assertEquals(
+                "[named] can be a single object with any number of fields or an array where each entry is an object with a single field",
+                e.getCause().getMessage());
+    }
+
+    public void testParseNamedObjectNoFieldsInArray() throws IOException {
+        XContentParser parser = XContentType.JSON.xContent().createParser(
+                  "{\"named\": [\n"
+                + "  {}"
+                + "]}");
+        ParsingException e = expectThrows(ParsingException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
+        assertEquals("[named_object_holder] failed to parse field [named]", e.getMessage());
+        assertEquals(
+                "[named] can be a single object with any number of fields or an array where each entry is an object with a single field",
+                e.getCause().getMessage());
+    }
+
+    public void testParseNamedObjectJunkInArray() throws IOException {
+        XContentParser parser = XContentType.JSON.xContent().createParser(
+                  "{\"named\": [\n"
+                + "  \"junk\""
+                + "]}");
+        ParsingException e = expectThrows(ParsingException.class, () -> NamedObjectHolder.PARSER.apply(parser, null));
+        assertEquals("[named_object_holder] failed to parse field [named]", e.getMessage());
+        assertEquals(
+                "[named] can be a single object with any number of fields or an array where each entry is an object with a single field",
+                e.getCause().getMessage());
+    }
+
+    public void testParseNamedObjectInOrderNotSupported() throws IOException {
+        XContentParser parser = XContentType.JSON.xContent().createParser(
+                  "{\"named\": [\n"
+                + "  {\"a\": {}}"
+                + "]}");
+
+        // Create our own parser for this test so we can disable support for the "ordered" mode specified by the array above
+        ObjectParser<NamedObjectHolder, Void> objectParser = new ObjectParser<>("named_object_holder", NamedObjectHolder::new);
+        objectParser.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, new ParseField("named"));
+
+        // Now firing the xml through it fails
+        ParsingException e = expectThrows(ParsingException.class, () -> objectParser.apply(parser, null));
+        assertEquals("[named_object_holder] failed to parse field [named]", e.getMessage());
+        assertEquals("[named] doesn't support arrays. Use a single object with multiple fields.", e.getCause().getMessage());
+    }
+
+    static class NamedObjectHolder {
+        public static final ObjectParser<NamedObjectHolder, Void> PARSER = new ObjectParser<>("named_object_holder",
+                NamedObjectHolder::new);
+        static {
+            PARSER.declareNamedObjects(NamedObjectHolder::setNamed, NamedObject.PARSER, NamedObjectHolder::keepNamedInOrder,
+                    new ParseField("named"));
+        }
+
+        private List<NamedObject> named;
+        private boolean namedSuppliedInOrder = false;
+
+        public void setNamed(List<NamedObject> named) {
+            this.named = named;
+        }
+
+        public void keepNamedInOrder() {
+            namedSuppliedInOrder = true;
+        }
+    }
+
+    public static class NamedObject {
+        public static final NamedObjectParser<NamedObject, Void> PARSER;
+        static {
+            ObjectParser<NamedObject, Void> parser = new ObjectParser<>("named");
+            parser.declareInt(NamedObject::setFoo, new ParseField("foo"));
+            PARSER = (XContentParser p, Void v, String name) -> parser.parse(p, new NamedObject(name));
+        }
+
+        final String name;
+        int foo;
+
+        public NamedObject(String name) {
+            this.name = name;
+        }
+
+        public void setFoo(int foo) {
+            this.foo = foo;
+        }
+    }
 }

+ 130 - 108
core/src/test/java/org/elasticsearch/search/highlight/HighlightBuilderTests.java

@@ -122,14 +122,17 @@ public class HighlightBuilderTests extends ESTestCase {
             assertTrue("highlighter is not equal to self", secondBuilder.equals(secondBuilder));
             assertTrue("highlighter is not equal to its copy", firstBuilder.equals(secondBuilder));
             assertTrue("equals is not symmetric", secondBuilder.equals(firstBuilder));
-            assertThat("highlighter copy's hashcode is different from original hashcode", secondBuilder.hashCode(), equalTo(firstBuilder.hashCode()));
+            assertThat("highlighter copy's hashcode is different from original hashcode", secondBuilder.hashCode(),
+                    equalTo(firstBuilder.hashCode()));
 
             HighlightBuilder thirdBuilder = serializedCopy(secondBuilder);
             assertTrue("highlighter is not equal to self", thirdBuilder.equals(thirdBuilder));
             assertTrue("highlighter is not equal to its copy", secondBuilder.equals(thirdBuilder));
-            assertThat("highlighter copy's hashcode is different from original hashcode", secondBuilder.hashCode(), equalTo(thirdBuilder.hashCode()));
+            assertThat("highlighter copy's hashcode is different from original hashcode", secondBuilder.hashCode(),
+                    equalTo(thirdBuilder.hashCode()));
             assertTrue("equals is not transitive", firstBuilder.equals(thirdBuilder));
-            assertThat("highlighter copy's hashcode is different from original hashcode", firstBuilder.hashCode(), equalTo(thirdBuilder.hashCode()));
+            assertThat("highlighter copy's hashcode is different from original hashcode", firstBuilder.hashCode(),
+                    equalTo(thirdBuilder.hashCode()));
             assertTrue("equals is not symmetric", thirdBuilder.equals(secondBuilder));
             assertTrue("equals is not symmetric", thirdBuilder.equals(firstBuilder));
         }
@@ -152,7 +155,12 @@ public class HighlightBuilderTests extends ESTestCase {
             XContentParser parser = XContentHelper.createParser(builder.bytes());
             context.reset(parser);
             parser.nextToken();
-            HighlightBuilder secondHighlightBuilder = HighlightBuilder.PROTOTYPE.fromXContent(context);
+            HighlightBuilder secondHighlightBuilder;
+            try {
+                secondHighlightBuilder = HighlightBuilder.fromXContent(context);
+            } catch (RuntimeException e) {
+                throw new RuntimeException("Error parsing " + highlightBuilder, e);
+            }
             assertNotSame(highlightBuilder, secondHighlightBuilder);
             assertEquals(highlightBuilder, secondHighlightBuilder);
             assertEquals(highlightBuilder.hashCode(), secondHighlightBuilder.hashCode());
@@ -163,73 +171,56 @@ public class HighlightBuilderTests extends ESTestCase {
      * test that unknown array fields cause exception
      */
     public void testUnknownArrayNameExpection() throws IOException {
-        QueryParseContext context = new QueryParseContext(indicesQueriesRegistry);
-        context.parseFieldMatcher(new ParseFieldMatcher(Settings.EMPTY));
-        String highlightElement = "{\n" +
-                "    \"bad_fieldname\" : [ \"field1\" 1 \"field2\" ]\n" +
-                "}\n";
-        XContentParser parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
-
-        context.reset(parser);
-        try {
-            HighlightBuilder.PROTOTYPE.fromXContent(context);
-            fail("expected a parsing exception");
-        } catch (ParsingException e) {
-            assertEquals("cannot parse array with name [bad_fieldname]", e.getMessage());
+        {
+            IllegalArgumentException e = expectParseThrows(IllegalArgumentException.class, "{\n" +
+                    "    \"bad_fieldname\" : [ \"field1\" 1 \"field2\" ]\n" +
+                    "}\n");
+            assertEquals("[highlight] unknown field [bad_fieldname], parser not found", e.getMessage());
+        }
+
+        {
+            ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
+                    "  \"fields\" : {\n" +
+                    "     \"body\" : {\n" +
+                    "        \"bad_fieldname\" : [ \"field1\" , \"field2\" ]\n" +
+                    "     }\n" +
+                    "   }\n" +
+                    "}\n");
+            assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
+            assertEquals("[fields] failed to parse field [body]", e.getCause().getMessage());
+            assertEquals("[highlight_field] unknown field [bad_fieldname], parser not found", e.getCause().getCause().getMessage());
         }
+    }
 
-        highlightElement = "{\n" +
-                "  \"fields\" : {\n" +
-                "     \"body\" : {\n" +
-                "        \"bad_fieldname\" : [ \"field1\" , \"field2\" ]\n" +
-                "     }\n" +
-                "   }\n" +
-                "}\n";
-        parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
-
+    private <T extends Throwable> T expectParseThrows(Class<T> exceptionClass, String highlightElement) throws IOException {
+        XContentParser parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
+        QueryParseContext context = new QueryParseContext(indicesQueriesRegistry);
         context.reset(parser);
-        try {
-            HighlightBuilder.PROTOTYPE.fromXContent(context);
-            fail("expected a parsing exception");
-        } catch (ParsingException e) {
-            assertEquals("cannot parse array with name [bad_fieldname]", e.getMessage());
-        }
+        return expectThrows(exceptionClass, () -> HighlightBuilder.fromXContent(context));
     }
 
     /**
      * test that unknown field name cause exception
      */
     public void testUnknownFieldnameExpection() throws IOException {
-        QueryParseContext context = new QueryParseContext(indicesQueriesRegistry);
-        context.parseFieldMatcher(new ParseFieldMatcher(Settings.EMPTY));
-        String highlightElement = "{\n" +
-                "    \"bad_fieldname\" : \"value\"\n" +
-                "}\n";
-        XContentParser parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
-
-        context.reset(parser);
-        try {
-            HighlightBuilder.PROTOTYPE.fromXContent(context);
-            fail("expected a parsing exception");
-        } catch (ParsingException e) {
-            assertEquals("unexpected fieldname [bad_fieldname]", e.getMessage());
-        }
-
-        highlightElement = "{\n" +
-                "  \"fields\" : {\n" +
-                "     \"body\" : {\n" +
-                "        \"bad_fieldname\" : \"value\"\n" +
-                "     }\n" +
-                "   }\n" +
-                "}\n";
-        parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
-
-        context.reset(parser);
-        try {
-            HighlightBuilder.PROTOTYPE.fromXContent(context);
-            fail("expected a parsing exception");
-        } catch (ParsingException e) {
-            assertEquals("unexpected fieldname [bad_fieldname]", e.getMessage());
+        {
+            IllegalArgumentException e = expectParseThrows(IllegalArgumentException.class, "{\n" +
+                    "    \"bad_fieldname\" : \"value\"\n" +
+                    "}\n");
+            assertEquals("[highlight] unknown field [bad_fieldname], parser not found", e.getMessage());
+        }
+
+        {
+            ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
+                    "  \"fields\" : {\n" +
+                    "     \"body\" : {\n" +
+                    "        \"bad_fieldname\" : \"value\"\n" +
+                    "     }\n" +
+                    "   }\n" +
+                    "}\n");
+            assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
+            assertEquals("[fields] failed to parse field [body]", e.getCause().getMessage());
+            assertEquals("[highlight_field] unknown field [bad_fieldname], parser not found", e.getCause().getCause().getMessage());
         }
     }
 
@@ -237,38 +228,57 @@ public class HighlightBuilderTests extends ESTestCase {
      * test that unknown field name cause exception
      */
     public void testUnknownObjectFieldnameExpection() throws IOException {
-        QueryParseContext context = new QueryParseContext(indicesQueriesRegistry);
-        context.parseFieldMatcher(new ParseFieldMatcher(Settings.EMPTY));
-        String highlightElement = "{\n" +
-                "    \"bad_fieldname\" :  { \"field\" : \"value\" }\n \n" +
-                "}\n";
-        XContentParser parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
-
-        context.reset(parser);
-        try {
-            HighlightBuilder.PROTOTYPE.fromXContent(context);
-            fail("expected a parsing exception");
-        } catch (ParsingException e) {
-            assertEquals("cannot parse object with name [bad_fieldname]", e.getMessage());
+        {
+            IllegalArgumentException e = expectParseThrows(IllegalArgumentException.class, "{\n" +
+                    "    \"bad_fieldname\" :  { \"field\" : \"value\" }\n \n" +
+                    "}\n");
+            assertEquals("[highlight] unknown field [bad_fieldname], parser not found", e.getMessage());
+        }
+
+        {
+            ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
+                    "  \"fields\" : {\n" +
+                    "     \"body\" : {\n" +
+                    "        \"bad_fieldname\" : { \"field\" : \"value\" }\n" +
+                    "     }\n" +
+                    "   }\n" +
+                    "}\n"); 
+            assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
+            assertEquals("[fields] failed to parse field [body]", e.getCause().getMessage());
+            assertEquals("[highlight_field] unknown field [bad_fieldname], parser not found", e.getCause().getCause().getMessage());
         }
+    }
 
-        highlightElement = "{\n" +
-                "  \"fields\" : {\n" +
-                "     \"body\" : {\n" +
-                "        \"bad_fieldname\" : { \"field\" : \"value\" }\n" +
-                "     }\n" +
-                "   }\n" +
-                "}\n";
-        parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
+    public void testStringInFieldsArray() throws IOException {
+        ParsingException e = expectParseThrows(ParsingException.class, "{\"fields\" : [ \"junk\" ]}");
+        assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
+        assertEquals(
+                "[fields] can be a single object with any number of fields or an array where each entry is an object with a single field",
+                e.getCause().getMessage());
+    }
 
-        context.reset(parser);
-        try {
-            HighlightBuilder.PROTOTYPE.fromXContent(context);
-            fail("expected a parsing exception");
-        } catch (ParsingException e) {
-            assertEquals("cannot parse object with name [bad_fieldname]", e.getMessage());
-        }
-     }
+    public void testNoFieldsInObjectInFieldsArray() throws IOException {
+        ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
+                "  \"fields\" : [ {\n" +
+                "   }] \n" +
+                "}\n");
+        assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
+        assertEquals(
+                "[fields] can be a single object with any number of fields or an array where each entry is an object with a single field",
+                e.getCause().getMessage());
+    }
+
+    public void testTwoFieldsInObjectInFieldsArray() throws IOException {
+        ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
+                "  \"fields\" : [ {\n" +
+                "     \"body\" : {},\n" +
+                "     \"nope\" : {}\n" +
+                "   }] \n" +
+                "}\n");
+        assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
+        assertEquals(
+                "[fields] can be a single object with any number of fields or an array where each entry is an object with a single field",
+                e.getCause().getMessage());    }
 
      /**
      * test that build() outputs a {@link SearchContextHighlight} that is has similar parameters
@@ -280,7 +290,8 @@ public class HighlightBuilderTests extends ESTestCase {
         Index index = new Index(randomAsciiOfLengthBetween(1, 10), "_na_");
         IndexSettings idxSettings = IndexSettingsModule.newIndexSettings(index, indexSettings);
         // shard context will only need indicesQueriesRegistry for building Query objects nested in highlighter
-        QueryShardContext mockShardContext = new QueryShardContext(idxSettings, null, null, null, null, null, indicesQueriesRegistry, null) {
+        QueryShardContext mockShardContext = new QueryShardContext(idxSettings, null, null, null, null, null, indicesQueriesRegistry,
+                null) {
             @Override
             public MappedFieldType fieldMapper(String name) {
                 TextFieldMapper.Builder builder = new TextFieldMapper.Builder(name);
@@ -396,7 +407,7 @@ public class HighlightBuilderTests extends ESTestCase {
         XContentParser parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
 
         context.reset(parser);
-        HighlightBuilder highlightBuilder = HighlightBuilder.PROTOTYPE.fromXContent(context);
+        HighlightBuilder highlightBuilder = HighlightBuilder.fromXContent(context);
         assertArrayEquals("setting tags_schema 'styled' should alter pre_tags", HighlightBuilder.DEFAULT_STYLED_PRE_TAG,
                 highlightBuilder.preTags());
         assertArrayEquals("setting tags_schema 'styled' should alter post_tags", HighlightBuilder.DEFAULT_STYLED_POST_TAGS,
@@ -408,24 +419,17 @@ public class HighlightBuilderTests extends ESTestCase {
         parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
 
         context.reset(parser);
-        highlightBuilder = HighlightBuilder.PROTOTYPE.fromXContent(context);
+        highlightBuilder = HighlightBuilder.fromXContent(context);
         assertArrayEquals("setting tags_schema 'default' should alter pre_tags", HighlightBuilder.DEFAULT_PRE_TAGS,
                 highlightBuilder.preTags());
         assertArrayEquals("setting tags_schema 'default' should alter post_tags", HighlightBuilder.DEFAULT_POST_TAGS,
                 highlightBuilder.postTags());
 
-        highlightElement = "{\n" +
+        ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
                 "    \"tags_schema\" : \"somthing_else\"\n" +
-                "}\n";
-        parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
-
-        context.reset(parser);
-        try {
-            HighlightBuilder.PROTOTYPE.fromXContent(context);
-            fail("setting unknown tag schema should throw exception");
-        } catch (IllegalArgumentException e) {
-            assertEquals("Unknown tag schema [somthing_else]", e.getMessage());
-        }
+                "}\n");
+        assertEquals("[highlight] failed to parse field [tags_schema]", e.getMessage());
+        assertEquals("Unknown tag schema [somthing_else]", e.getCause().getMessage());
     }
 
     /**
@@ -438,24 +442,42 @@ public class HighlightBuilderTests extends ESTestCase {
         XContentParser parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
 
         context.reset(parser);
-        HighlightBuilder highlightBuilder = HighlightBuilder.PROTOTYPE.fromXContent(context);
+        HighlightBuilder highlightBuilder = HighlightBuilder.fromXContent(context);
         assertEquals("expected plain HighlightBuilder", new HighlightBuilder(), highlightBuilder);
 
         highlightElement = "{ \"fields\" : { } }";
         parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
 
         context.reset(parser);
-        highlightBuilder = HighlightBuilder.PROTOTYPE.fromXContent(context);
+        highlightBuilder = HighlightBuilder.fromXContent(context);
         assertEquals("defining no field should return plain HighlightBuilder", new HighlightBuilder(), highlightBuilder);
 
         highlightElement = "{ \"fields\" : { \"foo\" : { } } }";
         parser = XContentFactory.xContent(highlightElement).createParser(highlightElement);
 
         context.reset(parser);
-        highlightBuilder = HighlightBuilder.PROTOTYPE.fromXContent(context);
+        highlightBuilder = HighlightBuilder.fromXContent(context);
         assertEquals("expected HighlightBuilder with field", new HighlightBuilder().field(new Field("foo")), highlightBuilder);
     }
 
+    public void testPreTagsWithoutPostTags() throws IOException {
+        ParsingException e = expectParseThrows(ParsingException.class, "{\n" +
+                "    \"pre_tags\" : [\"<a>\"]\n" +
+                "}\n");
+        assertEquals("pre_tags are set but post_tags are not set", e.getMessage());
+
+        e = expectParseThrows(ParsingException.class, "{\n" +
+                "  \"fields\" : {\n" +
+                "     \"body\" : {\n" +
+                "        \"pre_tags\" : [\"<a>\"]\n" +
+                "     }\n" +
+                "   }\n" +
+                "}\n");
+        assertEquals("[highlight] failed to parse field [fields]", e.getMessage());
+        assertEquals("[fields] failed to parse field [body]", e.getCause().getMessage());
+        assertEquals("pre_tags are set but post_tags are not set", e.getCause().getCause().getMessage());
+    }
+
     /**
      * test ordinals of {@link Order}, since serialization depends on it
      */