diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java index fcf1446e53c44..b5b4dcd00caf5 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/AbstractObjectParser.java @@ -37,6 +37,8 @@ public abstract class AbstractObjectParser implements BiFunction, ContextParser { + final List requiredFieldSets = new ArrayList<>(); + /** * Declare some field. Usually it is easier to use {@link #declareString(BiConsumer, ParseField)} or * {@link #declareObject(BiConsumer, ContextParser, ParseField)} rather than call this directly. @@ -211,6 +213,61 @@ public void declareFieldArray(BiConsumer> consumer, ContextPa declareField(consumer, (p, c) -> parseArray(p, () -> itemParser.parse(p, c)), field, type); } + /** + * Declares a set of fields that are required for parsing to succeed. Only one of the values + * provided per String[] must be matched. + * + * E.g. declareRequiredFieldSet("foo", "bar"); means at least one of "foo" or + * "bar" fields must be present. If neither of those fields are present, an exception will be thrown. + * + * Multiple required sets can be configured: + * + *

+     *   parser.declareRequiredFieldSet("foo", "bar");
+     *   parser.declareRequiredFieldSet("bizz", "buzz");
+     * 
+ * + * requires that one of "foo" or "bar" fields are present, and also that one of "bizz" or + * "buzz" fields are present. + * + * In JSON, it means any of these combinations are acceptable: + * + *
    + *
  • {"foo":"...", "bizz": "..."}
  • + *
  • {"bar":"...", "bizz": "..."}
  • + *
  • {"foo":"...", "buzz": "..."}
  • + *
  • {"bar":"...", "buzz": "..."}
  • + *
  • {"foo":"...", "bar":"...", "bizz": "..."}
  • + *
  • {"foo":"...", "bar":"...", "buzz": "..."}
  • + *
  • {"foo":"...", "bizz":"...", "buzz": "..."}
  • + *
  • {"bar":"...", "bizz":"...", "buzz": "..."}
  • + *
  • {"foo":"...", "bar":"...", "bizz": "...", "buzz": "..."}
  • + *
+ * + * The following would however be rejected: + * + * + * + * + * + * + * + * + * + * + * + *
failure cases
Provided JSONReason for failure
{"foo":"..."}Missing "bizz" or "buzz" field
{"bar":"..."}Missing "bizz" or "buzz" field
{"bizz": "..."}Missing "foo" or "bar" field
{"buzz": "..."}Missing "foo" or "bar" field
{"foo":"...", "bar": "..."}Missing "bizz" or "buzz" field
{"bizz":"...", "buzz": "..."}Missing "foo" or "bar" field
{"unrelated":"..."} Missing "foo" or "bar" field, and missing "bizz" or "buzz" field
+ * + * @param requiredSet + * A set of required fields, where at least one of the fields in the array _must_ be present + */ + public void declareRequiredFieldSet(String... requiredSet) { + if (requiredSet.length == 0) { + return; + } + this.requiredFieldSets.add(requiredSet); + } + private interface IOSupplier { T get() throws IOException; } diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java index 30810637044d7..f173223eed79c 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java @@ -28,6 +28,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -272,6 +273,8 @@ public Value parse(XContentParser parser, Value value, Context context) throws I FieldParser fieldParser = null; String currentFieldName = null; XContentLocation currentPosition = null; + List requiredFields = new ArrayList<>(this.requiredFieldSets); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); @@ -285,11 +288,32 @@ public Value parse(XContentParser parser, Value value, Context context) throws I unknownFieldParser.acceptUnknownField(this, currentFieldName, currentPosition, parser, value, context); } else { fieldParser.assertSupports(name, parser, currentFieldName); + + // Check to see if this field is a required field, if it is we can + // remove the entry as the requirement is satisfied + Iterator iter = requiredFields.iterator(); + while (iter.hasNext()) { + String[] requriedFields = iter.next(); + for (String field : requriedFields) { + if (field.equals(currentFieldName)) { + iter.remove(); + break; + } + } + } + parseSub(parser, fieldParser, currentFieldName, value, context); } fieldParser = null; } } + if (requiredFields.isEmpty() == false) { + StringBuilder message = new StringBuilder(); + for (String[] fields : requiredFields) { + message.append("Required one of fields ").append(Arrays.toString(fields)).append(", but none were specified. "); + } + throw new IllegalArgumentException(message.toString()); + } return value; } diff --git a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java index ab56b8977ad5d..62ca73fa848ac 100644 --- a/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java +++ b/libs/x-content/src/test/java/org/elasticsearch/common/xcontent/ObjectParserTests.java @@ -42,6 +42,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.nullValue; public class ObjectParserTests extends ESTestCase { @@ -756,6 +757,89 @@ public void testConsumeUnknownFields() throws IOException { assertEquals(Map.of("field", "value", "field2", List.of("list1", "list2")), o.fields.get("test_nested")); } + public void testRequiredFieldSet() throws IOException { + class TestStruct { + private Long a; + private Long b; + + private void setA(long value) { + this.a = value; + } + + private void setB(long value) { + this.b = value; + } + } + + XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"a\": \"123\"}"); + ObjectParser objectParser = new ObjectParser<>("foo", true, TestStruct::new); + objectParser.declareLong(TestStruct::setA, new ParseField("a")); + objectParser.declareLong(TestStruct::setB, new ParseField("b")); + objectParser.declareRequiredFieldSet(new String[]{"a", "b"}); + + TestStruct obj = objectParser.apply(parser, null); + assertThat(obj.a, equalTo(123L)); + assertThat(obj.b, nullValue()); + + parser = createParser(JsonXContent.jsonXContent, "{\"b\": \"123\"}"); + objectParser = new ObjectParser<>("foo", true, TestStruct::new); + objectParser.declareLong(TestStruct::setA, new ParseField("a")); + objectParser.declareLong(TestStruct::setB, new ParseField("b")); + objectParser.declareRequiredFieldSet(new String[]{"a", "b"}); + + obj = objectParser.apply(parser, null); + assertThat(obj.a, nullValue()); + assertThat(obj.b, equalTo(123L)); + + parser = createParser(JsonXContent.jsonXContent, "{\"a\": \"123\", \"b\": \"456\"}"); + objectParser = new ObjectParser<>("foo", true, TestStruct::new); + objectParser.declareLong(TestStruct::setA, new ParseField("a")); + objectParser.declareLong(TestStruct::setB, new ParseField("b")); + objectParser.declareRequiredFieldSet(new String[]{"a", "b"}); + + obj = objectParser.apply(parser, null); + assertThat(obj.a, equalTo(123L)); + assertThat(obj.b, equalTo(456L)); + } + + public void testMultipleRequiredFieldSet() throws IOException { + class TestStruct { + private Long a; + private Long b; + private Long c; + private Long d; + + private void setA(long value) { + this.a = value; + } + + private void setB(long value) { + this.b = value; + } + + private void setC(long value) { + this.c = value; + } + + private void setD(long value) { + this.d = value; + } + } + + XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"unrelated\": \"123\"}"); + ObjectParser objectParser = new ObjectParser<>("foo", true, TestStruct::new); + objectParser.declareLong(TestStruct::setA, new ParseField("a")); + objectParser.declareLong(TestStruct::setB, new ParseField("b")); + objectParser.declareLong(TestStruct::setC, new ParseField("c")); + objectParser.declareLong(TestStruct::setD, new ParseField("d")); + objectParser.declareRequiredFieldSet(new String[]{"a", "b"}); + objectParser.declareRequiredFieldSet(new String[]{"c", "d"}); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> objectParser.apply(parser, null)); + assertThat(e.getMessage(), equalTo("Required one of fields [a, b], but none were specified. " + + "Required one of fields [c, d], but none were specified. ")); + } + @Override protected NamedXContentRegistry xContentRegistry() { return new NamedXContentRegistry(Arrays.asList(