diff --git a/libs/x-content/src/main/java/org/elasticsearch/xcontent/ConstructingObjectParser.java b/libs/x-content/src/main/java/org/elasticsearch/xcontent/ConstructingObjectParser.java index c3b322db0e3a5..d72dd1c4b11f7 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/xcontent/ConstructingObjectParser.java +++ b/libs/x-content/src/main/java/org/elasticsearch/xcontent/ConstructingObjectParser.java @@ -10,11 +10,17 @@ package org.elasticsearch.xcontent; import org.elasticsearch.core.RestApiVersion; +import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.xcontent.ObjectParser.NamedObjectParser; import org.elasticsearch.xcontent.ObjectParser.ValueType; import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.RecordComponent; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; import java.util.List; @@ -130,6 +136,50 @@ public ConstructingObjectParser(String name, boolean ignoreUnknownFields, Functi this(name, ignoreUnknownFields, (args, context) -> builder.apply(args)); } + /** + * Build a parser for the given {@code recordClass}. + * + * @param name The name given to the delegate ObjectParser for error identification. Use what you'd use if the object worked with + * ObjectParser. + * @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 recordClass the {@link Class} of the {@link Record} type to build + * @param lookup a {@link java.lang.invoke.MethodHandles.Lookup Lookup} object that can access the record's canonical constructor; + * typically just {@code MethodHandles.lookup()} called by some code that can access the constructor. + * @return a function suitable to use as the {@code builder} argument for one of the constructors of this class. + */ + public static ConstructingObjectParser forRecord( + String name, + boolean ignoreUnknownFields, + Class recordClass, + MethodHandles.Lookup lookup + ) { + Function builder = recordBuilder(recordClass, lookup); + return new ConstructingObjectParser<>(name, ignoreUnknownFields, (args, context) -> builder.apply(args)); + } + + private static Function recordBuilder(Class recordClass, MethodHandles.Lookup lookup) { + Class[] ctorArgs = Arrays.stream(recordClass.getRecordComponents()).map(RecordComponent::getType).toArray(Class[]::new); + MethodHandle ctor; + try { + ctor = lookup.findConstructor(recordClass, MethodType.methodType(void.class, ctorArgs)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException("Cannot access record constructor", e); + } + return (args) -> recordClass.cast(constructRecord(args, ctor)); + } + + @SuppressForbidden(reason = "We can't use invokeExact because we don't know the argument types statically") + private static Object constructRecord(Object[] args, MethodHandle ctor) { + Object result; + try { + result = ctor.invokeWithArguments(args); + } catch (Throwable e) { + throw new IllegalStateException("Unable to call record constructor", e); + } + return result; + } + /** * Build the parser. * diff --git a/libs/x-content/src/test/java/org/elasticsearch/xcontent/ConstructingObjectParserTests.java b/libs/x-content/src/test/java/org/elasticsearch/xcontent/ConstructingObjectParserTests.java index d385ce32e7d36..a7db7a2ddf08d 100644 --- a/libs/x-content/src/test/java/org/elasticsearch/xcontent/ConstructingObjectParserTests.java +++ b/libs/x-content/src/test/java/org/elasticsearch/xcontent/ConstructingObjectParserTests.java @@ -20,6 +20,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.util.List; import java.util.function.BiConsumer; @@ -850,4 +851,19 @@ class DoubleFieldDeclaration { assertThat(exception, instanceOf(IllegalArgumentException.class)); assertThat(exception.getMessage(), startsWith("Parser already registered for name=[name]")); } + + public void testForRecord() throws IOException { + record TestRecord(int field1, String field2) {} + var PARSER = ConstructingObjectParser.forRecord("record", false, TestRecord.class, MethodHandles.lookup()); + PARSER.declareInt(constructorArg(), new ParseField("field1")); + PARSER.declareString(constructorArg(), new ParseField("field2")); + + TestRecord actual = PARSER.parse(createParser(JsonXContent.jsonXContent, """ + { + "field1": 123, + "field2": "value2" + } + """), null); + assertEquals(new TestRecord(123, "value2"), actual); + } } diff --git a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/direct/DatabaseConfiguration.java b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/direct/DatabaseConfiguration.java index 08efe87e6fde5..e52bbf5315ce7 100644 --- a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/direct/DatabaseConfiguration.java +++ b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/direct/DatabaseConfiguration.java @@ -25,6 +25,7 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Objects; @@ -259,10 +260,12 @@ public String getWriteableName() { private static final ParseField ACCOUNT_ID = new ParseField("account_id"); - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("maxmind", false, (a, id) -> { - String accountId = (String) a[0]; - return new Maxmind(accountId); - }); + private static final ConstructingObjectParser PARSER = ConstructingObjectParser.forRecord( + "maxmind", + false, + Maxmind.class, + MethodHandles.lookup() + ); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), ACCOUNT_ID); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamAutoShardingEvent.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamAutoShardingEvent.java index 6fec15e0fbfe8..b6db5f8b08fec 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamAutoShardingEvent.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamAutoShardingEvent.java @@ -21,6 +21,7 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.util.function.LongSupplier; /** @@ -36,10 +37,11 @@ public record DataStreamAutoShardingEvent(String triggerIndexName, int targetNum public static final ParseField EVENT_TIME = new ParseField("event_time"); public static final ParseField EVENT_TIME_MILLIS = new ParseField("event_time_millis"); - public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + public static final ConstructingObjectParser PARSER = ConstructingObjectParser.forRecord( "auto_sharding", false, - (args, unused) -> new DataStreamAutoShardingEvent((String) args[0], (int) args[1], (long) args[2]) + DataStreamAutoShardingEvent.class, + MethodHandles.lookup() ); static { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamFailureStore.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamFailureStore.java index 5c8849e1282d8..6259facc77c83 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamFailureStore.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamFailureStore.java @@ -25,6 +25,7 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.lang.invoke.MethodHandles; /** * Holds the data stream failure store metadata that enable or disable the failure store of a data stream. Currently, it @@ -44,10 +45,11 @@ public record DataStreamFailureStore(@Nullable Boolean enabled, @Nullable DataSt public static final ParseField ENABLED_FIELD = new ParseField(ENABLED); public static final ParseField LIFECYCLE_FIELD = new ParseField(LIFECYCLE); - public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + public static final ConstructingObjectParser PARSER = ConstructingObjectParser.forRecord( FAILURE_STORE, false, - (args, unused) -> new DataStreamFailureStore((Boolean) args[0], (DataStreamLifecycle) args[1]) + DataStreamFailureStore.class, + MethodHandles.lookup() ); static { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamOptions.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamOptions.java index f68a6c17494d2..3ac05eef6cab6 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamOptions.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamOptions.java @@ -24,6 +24,7 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.lang.invoke.MethodHandles; import static org.elasticsearch.cluster.metadata.DataStreamFailureStore.FAILURE_STORE; @@ -42,10 +43,11 @@ public record DataStreamOptions(@Nullable DataStreamFailureStore failureStore) public static final DataStreamOptions FAILURE_STORE_DISABLED = new DataStreamOptions(new DataStreamFailureStore(false, null)); public static final DataStreamOptions EMPTY = new DataStreamOptions(null); - public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + public static final ConstructingObjectParser PARSER = ConstructingObjectParser.forRecord( "options", false, - (args, unused) -> new DataStreamOptions((DataStreamFailureStore) args[0]) + DataStreamOptions.class, + MethodHandles.lookup() ); static { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadataStats.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadataStats.java index 39475f56ddcdf..1746a8ad67fbf 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadataStats.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadataStats.java @@ -23,6 +23,7 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.util.Arrays; import java.util.Objects; @@ -131,11 +132,11 @@ public record AverageShardSize(long totalSizeInBytes, int numberOfShards) implem public static final ParseField TOTAL_SIZE_IN_BYTES_FIELD = new ParseField("total_size_in_bytes"); public static final ParseField SHARD_COUNT_FIELD = new ParseField("shard_count"); - @SuppressWarnings("unchecked") - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + private static final ConstructingObjectParser PARSER = ConstructingObjectParser.forRecord( "average_shard_size", false, - (args, unused) -> new AverageShardSize((long) args[0], (int) args[1]) + AverageShardSize.class, + MethodHandles.lookup() ); static { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/profile/ProfileDocument.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/profile/ProfileDocument.java index 11ba4c59cbcdf..b329ba99a4c64 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/profile/ProfileDocument.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/profile/ProfileDocument.java @@ -23,6 +23,7 @@ import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; +import java.lang.invoke.MethodHandles; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.Instant; @@ -125,31 +126,18 @@ public static ProfileDocument fromXContent(XContentParser parser) { return PARSER.apply(parser, null); } - @SuppressWarnings("unchecked") - static final ConstructingObjectParser PROFILE_DOC_USER_PARSER = new ConstructingObjectParser<>( + static final ConstructingObjectParser PROFILE_DOC_USER_PARSER = ConstructingObjectParser.forRecord( "user_profile_document_user", false, - (args, v) -> new ProfileDocumentUser( - (String) args[0], - (List) args[1], - (Authentication.RealmRef) args[2], - (String) args[3], - (String) args[4] - ) + ProfileDocumentUser.class, + MethodHandles.lookup() ); - @SuppressWarnings("unchecked") - static final ConstructingObjectParser PROFILE_DOC_PARSER = new ConstructingObjectParser<>( + static final ConstructingObjectParser PROFILE_DOC_PARSER = ConstructingObjectParser.forRecord( "user_profile_document", false, - (args, v) -> new ProfileDocument( - (String) args[0], - (boolean) args[1], - (long) args[2], - (ProfileDocumentUser) args[3], - (Map) args[4], - (BytesReference) args[5] - ) + ProfileDocument.class, + MethodHandles.lookup() ); static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(