Skip to content

Commit e531930

Browse files
committed
ConstructingObjectParser.forRecord
1 parent ef569cf commit e531930

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

libs/x-content/src/main/java/org/elasticsearch/xcontent/ConstructingObjectParser.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@
1010
package org.elasticsearch.xcontent;
1111

1212
import org.elasticsearch.core.RestApiVersion;
13+
import org.elasticsearch.core.SuppressForbidden;
1314
import org.elasticsearch.xcontent.ObjectParser.NamedObjectParser;
1415
import org.elasticsearch.xcontent.ObjectParser.ValueType;
1516

1617
import java.io.IOException;
18+
import java.lang.invoke.MethodHandle;
19+
import java.lang.invoke.MethodHandles;
20+
import java.lang.invoke.MethodType;
21+
import java.lang.reflect.RecordComponent;
1722
import java.util.ArrayList;
23+
import java.util.Arrays;
1824
import java.util.Collections;
1925
import java.util.EnumMap;
2026
import java.util.List;
@@ -130,6 +136,70 @@ public ConstructingObjectParser(String name, boolean ignoreUnknownFields, Functi
130136
this(name, ignoreUnknownFields, (args, context) -> builder.apply(args));
131137
}
132138

139+
/**
140+
* Build a parser for the given {@code recordClass}.
141+
*
142+
* @param name The name given to the delegate ObjectParser for error identification. Use what you'd use if the object worked with
143+
* ObjectParser.
144+
* @param ignoreUnknownFields Should this parser ignore unknown fields? This should generally be set to true only when parsing responses
145+
* from external systems, never when parsing requests from users.
146+
* @param recordClass the {@link Class} of the {@link Record} type to build
147+
* @param lookup a {@link java.lang.invoke.MethodHandles.Lookup Lookup} object that can access the record's canonical constructor;
148+
* typically just {@code MethodHandles.lookup()} called by some code that can access the constructor.
149+
* @return a function suitable to use as the {@code builder} argument for one of the constructors of this class.
150+
*/
151+
public static <R extends Record, Context> ConstructingObjectParser<R, Context> forRecord(
152+
String name,
153+
boolean ignoreUnknownFields,
154+
Class<R> recordClass,
155+
MethodHandles.Lookup lookup
156+
) {
157+
Function<Object[], R> builder = recordBuilder(recordClass, lookup);
158+
return new ConstructingObjectParser<>(name, ignoreUnknownFields, (args, context) -> builder.apply(args));
159+
}
160+
161+
/**
162+
* Build a parser for the given {@code recordClass}.
163+
*
164+
* @param name The name given to the delegate ObjectParser for error identification. Use what you'd use if the object worked with
165+
* ObjectParser.
166+
* @param ignoreUnknownFields Should this parser ignore unknown fields? This should generally be set to true only when parsing responses
167+
* from external systems, never when parsing requests from users.
168+
* @param recordClass the {@link Class} of the {@link Record} type to build.
169+
* It must be a public class with a public canonical constructor, in a package that is exported unconditionally;
170+
* otherwise, you'll need {@link #forRecord(String, boolean, Class, MethodHandles.Lookup)} instead.
171+
* @return a function suitable to use as the {@code builder} argument for one of the constructors of this class.
172+
*/
173+
public static <R extends Record, Context> ConstructingObjectParser<R, Context> forRecord(
174+
String name,
175+
boolean ignoreUnknownFields,
176+
Class<R> recordClass
177+
) {
178+
return forRecord(name, ignoreUnknownFields, recordClass, MethodHandles.publicLookup());
179+
}
180+
181+
private static <R extends Record> Function<Object[], R> recordBuilder(Class<R> recordClass, MethodHandles.Lookup lookup) {
182+
Class<?>[] ctorArgs = Arrays.stream(recordClass.getRecordComponents()).map(RecordComponent::getType).toArray(Class<?>[]::new);
183+
MethodHandle ctor;
184+
try {
185+
ctor = lookup.findConstructor(recordClass, MethodType.methodType(void.class, ctorArgs));
186+
} catch (NoSuchMethodException | IllegalAccessException e) {
187+
throw new IllegalStateException("Cannot access record constructor", e);
188+
}
189+
return (args) -> recordClass.cast(constructRecord(args, ctor));
190+
}
191+
192+
@SuppressForbidden(reason="We can't use invokeExact because we don't know the argument types statically")
193+
private static Object constructRecord(Object[] args, MethodHandle ctor) {
194+
Object result;
195+
try {
196+
result = ctor.invokeWithArguments(args);
197+
} catch (Throwable e) {
198+
throw new IllegalStateException("Unable to call record constructor", e);
199+
}
200+
return result;
201+
}
202+
133203
/**
134204
* Build the parser.
135205
*

libs/x-content/src/test/java/org/elasticsearch/xcontent/ConstructingObjectParserTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.io.ByteArrayOutputStream;
2222
import java.io.IOException;
23+
import java.lang.invoke.MethodHandles;
2324
import java.util.List;
2425
import java.util.function.BiConsumer;
2526

@@ -850,4 +851,19 @@ class DoubleFieldDeclaration {
850851
assertThat(exception, instanceOf(IllegalArgumentException.class));
851852
assertThat(exception.getMessage(), startsWith("Parser already registered for name=[name]"));
852853
}
854+
855+
public void testForRecord() throws IOException {
856+
record TestRecord(int field1, String field2) {}
857+
var PARSER = ConstructingObjectParser.forRecord("record", false, TestRecord.class, MethodHandles.lookup());
858+
PARSER.declareInt(constructorArg(), new ParseField("field1"));
859+
PARSER.declareString(constructorArg(), new ParseField("field2"));
860+
861+
TestRecord actual = PARSER.parse(createParser(JsonXContent.jsonXContent, """
862+
{
863+
"field1": 123,
864+
"field2": "value2"
865+
}
866+
"""), null);
867+
assertEquals(new TestRecord(123, "value2"), actual);
868+
}
853869
}

0 commit comments

Comments
 (0)