Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <R extends Record, Context> ConstructingObjectParser<R, Context> forRecord(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about restructuring this a bit for readability and have recordBuilder return the actual builder BiFunction as expected by ConstructingObjectParser using

  • <R extends Record> MethodHandle recordConstructor(Class<R> recordClass, MethodHandles.Lookup lookup) and
  • <R extends Record, Context> BiFunction<Object[], Context, R> recordBuilder(MethodHandle ctor)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I thought the same thing actually. I should do that.

String name,
boolean ignoreUnknownFields,
Class<R> recordClass,
MethodHandles.Lookup lookup
) {
Function<Object[], R> builder = recordBuilder(recordClass, lookup);
return new ConstructingObjectParser<>(name, ignoreUnknownFields, (args, context) -> builder.apply(args));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(For the future... if this were able to consume the context argument too, there would be a lot more cases where it would apply. The current one applies only to cases where context is ignored, which seems to be roughly half the places where records are used.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, looking into this some more, my own CPS requirements actually need this functionality, so that means this PR is insufficient for my own needs in its current form.

}

private static <R extends Record> Function<Object[], R> recordBuilder(Class<R> 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically this area is rather performance sensitive, not sure how much we have to worry here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering how invoke on an array-spreading method handle (asSpreader(Object[].class, ctorArgs.length)) compares to invokeWithArguments...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The javadocs say invokeWithArguments is equivalent to this:

MethodHandle invoker = MethodHandles.spreadInvoker(this.type(), 0);
Object result = invoker.invokeExact(this, arguments);

I could perhaps do the spreadInvoker ahead of time, leaving only the invokeExact.

} catch (Throwable e) {
throw new IllegalStateException("Unable to call record constructor", e);
}
return result;
}

/**
* Build the parser.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -259,10 +260,12 @@ public String getWriteableName() {

private static final ParseField ACCOUNT_ID = new ParseField("account_id");

private static final ConstructingObjectParser<Maxmind, Void> PARSER = new ConstructingObjectParser<>("maxmind", false, (a, id) -> {
String accountId = (String) a[0];
return new Maxmind(accountId);
});
private static final ConstructingObjectParser<Maxmind, Void> PARSER = ConstructingObjectParser.forRecord(
"maxmind",
false,
Maxmind.class,
MethodHandles.lookup()
);

static {
PARSER.declareString(ConstructingObjectParser.constructorArg(), ACCOUNT_ID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.elasticsearch.xcontent.XContentParser;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.function.LongSupplier;

/**
Expand All @@ -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<DataStreamAutoShardingEvent, Void> PARSER = new ConstructingObjectParser<>(
public static final ConstructingObjectParser<DataStreamAutoShardingEvent, Void> PARSER = ConstructingObjectParser.forRecord(
"auto_sharding",
false,
(args, unused) -> new DataStreamAutoShardingEvent((String) args[0], (int) args[1], (long) args[2])
DataStreamAutoShardingEvent.class,
MethodHandles.lookup()
);

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DataStreamFailureStore, Void> PARSER = new ConstructingObjectParser<>(
public static final ConstructingObjectParser<DataStreamFailureStore, Void> PARSER = ConstructingObjectParser.forRecord(
FAILURE_STORE,
false,
(args, unused) -> new DataStreamFailureStore((Boolean) args[0], (DataStreamLifecycle) args[1])
DataStreamFailureStore.class,
MethodHandles.lookup()
);

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<DataStreamOptions, Void> PARSER = new ConstructingObjectParser<>(
public static final ConstructingObjectParser<DataStreamOptions, Void> PARSER = ConstructingObjectParser.forRecord(
"options",
false,
(args, unused) -> new DataStreamOptions((DataStreamFailureStore) args[0])
DataStreamOptions.class,
MethodHandles.lookup()
);

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<AverageShardSize, Void> PARSER = new ConstructingObjectParser<>(
private static final ConstructingObjectParser<AverageShardSize, Void> PARSER = ConstructingObjectParser.forRecord(
"average_shard_size",
false,
(args, unused) -> new AverageShardSize((long) args[0], (int) args[1])
AverageShardSize.class,
MethodHandles.lookup()
);

static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -125,31 +126,18 @@ public static ProfileDocument fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}

@SuppressWarnings("unchecked")
static final ConstructingObjectParser<ProfileDocumentUser, Void> PROFILE_DOC_USER_PARSER = new ConstructingObjectParser<>(
static final ConstructingObjectParser<ProfileDocumentUser, Void> PROFILE_DOC_USER_PARSER = ConstructingObjectParser.forRecord(
"user_profile_document_user",
false,
(args, v) -> new ProfileDocumentUser(
(String) args[0],
(List<String>) args[1],
(Authentication.RealmRef) args[2],
(String) args[3],
(String) args[4]
)
ProfileDocumentUser.class,
MethodHandles.lookup()
);

@SuppressWarnings("unchecked")
static final ConstructingObjectParser<ProfileDocument, Void> PROFILE_DOC_PARSER = new ConstructingObjectParser<>(
static final ConstructingObjectParser<ProfileDocument, Void> 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<String, Object>) args[4],
(BytesReference) args[5]
)
ProfileDocument.class,
MethodHandles.lookup()
);

static final ConstructingObjectParser<ProfileDocument, Void> PARSER = new ConstructingObjectParser<>(
Expand Down