|
28 | 28 | import java.lang.reflect.Parameter; |
29 | 29 | import java.lang.reflect.Type; |
30 | 30 | import java.nio.ByteBuffer; |
| 31 | +import java.time.LocalDate; |
| 32 | +import java.time.LocalDateTime; |
| 33 | +import java.time.LocalTime; |
31 | 34 | import java.util.ArrayList; |
32 | 35 | import java.util.Arrays; |
33 | 36 | import java.util.Collection; |
|
38 | 41 | import java.util.Optional; |
39 | 42 | import java.util.Set; |
40 | 43 | import java.util.SortedMap; |
| 44 | +import java.util.UUID; |
41 | 45 | import net.bytebuddy.ByteBuddy; |
42 | 46 | import net.bytebuddy.NamingStrategy; |
43 | 47 | import net.bytebuddy.NamingStrategy.SuffixingRandom.BaseNameResolver; |
|
78 | 82 | import org.apache.beam.sdk.schemas.FieldValueHaver; |
79 | 83 | import org.apache.beam.sdk.schemas.FieldValueSetter; |
80 | 84 | import org.apache.beam.sdk.schemas.FieldValueTypeInformation; |
| 85 | +import org.apache.beam.sdk.schemas.Schema.LogicalType; |
| 86 | +import org.apache.beam.sdk.schemas.logicaltypes.NanosInstant; |
| 87 | +import org.apache.beam.sdk.schemas.logicaltypes.SqlTypes; |
81 | 88 | import org.apache.beam.sdk.util.Preconditions; |
82 | 89 | import org.apache.beam.sdk.util.common.ReflectHelpers; |
83 | 90 | import org.apache.beam.sdk.values.TypeDescriptor; |
@@ -120,6 +127,65 @@ public class ByteBuddyUtils { |
120 | 127 | private static final ForLoadedType ENUM_TYPE = new ForLoadedType(Enum.class); |
121 | 128 | private static final ForLoadedType BYTE_BUDDY_UTILS_TYPE = |
122 | 129 | new ForLoadedType(ByteBuddyUtils.class); |
| 130 | + private static final ForLoadedType LOGICAL_TYPE_TYPE = new ForLoadedType(LogicalType.class); |
| 131 | + |
| 132 | + // Static LogicalType instances used by codegen for JSR-310 and UUID POJO/Bean fields. The |
| 133 | + // generated bytecode loads these via FieldAccess and invokes toBaseType / toInputType, so the |
| 134 | + // fields must be public so generated classes in user packages can access them. |
| 135 | + // See logicalTypeFieldName(...) for the type → field name mapping. |
| 136 | + public static final LogicalType<LocalDate, Long> JAVA_LOCAL_DATE_LOGICAL_TYPE = SqlTypes.DATE; |
| 137 | + public static final LogicalType<LocalTime, Long> JAVA_LOCAL_TIME_LOGICAL_TYPE = SqlTypes.TIME; |
| 138 | + public static final LogicalType<LocalDateTime, org.apache.beam.sdk.values.Row> |
| 139 | + JAVA_LOCAL_DATE_TIME_LOGICAL_TYPE = SqlTypes.DATETIME; |
| 140 | + public static final LogicalType<java.time.Instant, org.apache.beam.sdk.values.Row> |
| 141 | + JAVA_INSTANT_LOGICAL_TYPE = new NanosInstant(); |
| 142 | + public static final LogicalType<UUID, org.apache.beam.sdk.values.Row> JAVA_UUID_LOGICAL_TYPE = |
| 143 | + SqlTypes.UUID; |
| 144 | + |
| 145 | + /** |
| 146 | + * Returns the {@link Schema.LogicalType} that {@link StaticSchemaInference} infers for the given |
| 147 | + * Java raw type, or {@code null} if no JSR-310 / UUID inference applies. |
| 148 | + */ |
| 149 | + static @Nullable LogicalType<?, ?> inferredLogicalTypeFor(Class<?> rawType) { |
| 150 | + if (LocalDate.class.equals(rawType)) { |
| 151 | + return JAVA_LOCAL_DATE_LOGICAL_TYPE; |
| 152 | + } else if (LocalTime.class.equals(rawType)) { |
| 153 | + return JAVA_LOCAL_TIME_LOGICAL_TYPE; |
| 154 | + } else if (LocalDateTime.class.equals(rawType)) { |
| 155 | + return JAVA_LOCAL_DATE_TIME_LOGICAL_TYPE; |
| 156 | + } else if (java.time.Instant.class.equals(rawType)) { |
| 157 | + return JAVA_INSTANT_LOGICAL_TYPE; |
| 158 | + } else if (UUID.class.equals(rawType)) { |
| 159 | + return JAVA_UUID_LOGICAL_TYPE; |
| 160 | + } |
| 161 | + return null; |
| 162 | + } |
| 163 | + |
| 164 | + /** Maps a Java raw type to the static field name in {@link ByteBuddyUtils} that holds it. */ |
| 165 | + private static String logicalTypeFieldName(Class<?> rawType) { |
| 166 | + if (LocalDate.class.equals(rawType)) { |
| 167 | + return "JAVA_LOCAL_DATE_LOGICAL_TYPE"; |
| 168 | + } else if (LocalTime.class.equals(rawType)) { |
| 169 | + return "JAVA_LOCAL_TIME_LOGICAL_TYPE"; |
| 170 | + } else if (LocalDateTime.class.equals(rawType)) { |
| 171 | + return "JAVA_LOCAL_DATE_TIME_LOGICAL_TYPE"; |
| 172 | + } else if (java.time.Instant.class.equals(rawType)) { |
| 173 | + return "JAVA_INSTANT_LOGICAL_TYPE"; |
| 174 | + } else if (UUID.class.equals(rawType)) { |
| 175 | + return "JAVA_UUID_LOGICAL_TYPE"; |
| 176 | + } |
| 177 | + throw new IllegalArgumentException("Not an inferred logical type: " + rawType); |
| 178 | + } |
| 179 | + |
| 180 | + /** Stack manipulation that pushes the static {@link LogicalType} for the given Java type. */ |
| 181 | + private static StackManipulation loadLogicalType(Class<?> rawType) { |
| 182 | + return FieldAccess.forField( |
| 183 | + BYTE_BUDDY_UTILS_TYPE |
| 184 | + .getDeclaredFields() |
| 185 | + .filter(ElementMatchers.named(logicalTypeFieldName(rawType))) |
| 186 | + .getOnly()) |
| 187 | + .read(); |
| 188 | + } |
123 | 189 |
|
124 | 190 | /** |
125 | 191 | * A naming strategy for ByteBuddy classes. |
@@ -286,6 +352,8 @@ public T convert(TypeDescriptor<?> typeDescriptor) { |
286 | 352 | return convertDateTime(typeDescriptor); |
287 | 353 | } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(ReadablePartial.class))) { |
288 | 354 | return convertDateTime(typeDescriptor); |
| 355 | + } else if (inferredLogicalTypeFor(typeDescriptor.getRawType()) != null) { |
| 356 | + return convertLogicalType(typeDescriptor); |
289 | 357 | } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(ByteBuffer.class))) { |
290 | 358 | return convertByteBuffer(typeDescriptor); |
291 | 359 | } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(CharSequence.class))) { |
@@ -324,6 +392,14 @@ protected StackManipulation shortCircuitReturnNull( |
324 | 392 |
|
325 | 393 | protected abstract T convertDateTime(TypeDescriptor<?> type); |
326 | 394 |
|
| 395 | + /** |
| 396 | + * Handles JSR-310 ({@link LocalDate}, {@link LocalTime}, {@link LocalDateTime}, {@link |
| 397 | + * java.time.Instant}) and {@link UUID} fields, which {@link StaticSchemaInference} infers as |
| 398 | + * Beam {@link LogicalType}s. Subclasses emit code that round-trips through the corresponding |
| 399 | + * static {@link LogicalType} instance ({@link #JAVA_LOCAL_DATE_LOGICAL_TYPE} etc.). |
| 400 | + */ |
| 401 | + protected abstract T convertLogicalType(TypeDescriptor<?> type); |
| 402 | + |
327 | 403 | protected abstract T convertByteBuffer(TypeDescriptor<?> type); |
328 | 404 |
|
329 | 405 | protected abstract T convertCharSequence(TypeDescriptor<?> type); |
@@ -401,6 +477,15 @@ protected Type convertDateTime(TypeDescriptor<?> type) { |
401 | 477 | return Instant.class; |
402 | 478 | } |
403 | 479 |
|
| 480 | + @Override |
| 481 | + protected Type convertLogicalType(TypeDescriptor<?> type) { |
| 482 | + // The codegen-generated getter returns the LogicalType's base value (Long for |
| 483 | + // Date/Time, Row for DateTime/NanosInstant/UUID). Object.class is a safe upper bound |
| 484 | + // for the FieldValueGetter signature; the framework's GetLogicalInputType wrapper |
| 485 | + // converts back to the input type before exposing the value to user code. |
| 486 | + return Object.class; |
| 487 | + } |
| 488 | + |
404 | 489 | @Override |
405 | 490 | protected Type convertByteBuffer(TypeDescriptor<?> type) { |
406 | 491 | return byte[].class; |
@@ -915,6 +1000,26 @@ protected StackManipulation convertDateTime(TypeDescriptor<?> type) { |
915 | 1000 | return new ShortCircuitReturnNull(readValue, stackManipulation); |
916 | 1001 | } |
917 | 1002 |
|
| 1003 | + @Override |
| 1004 | + protected StackManipulation convertLogicalType(TypeDescriptor<?> type) { |
| 1005 | + // Equivalent code: return STATIC_LOGICAL_TYPE.toBaseType(value); |
| 1006 | + // where STATIC_LOGICAL_TYPE is one of the JAVA_*_LOGICAL_TYPE static fields on |
| 1007 | + // ByteBuddyUtils. The base type is Long (for LocalDate, LocalTime) or Row (for the |
| 1008 | + // others); both are reference types so no boxing/casting is needed beyond the invoke. |
| 1009 | + StackManipulation stackManipulation = |
| 1010 | + new Compound( |
| 1011 | + loadLogicalType(type.getRawType()), |
| 1012 | + readValue, |
| 1013 | + MethodInvocation.invoke( |
| 1014 | + LOGICAL_TYPE_TYPE |
| 1015 | + .getDeclaredMethods() |
| 1016 | + .filter( |
| 1017 | + ElementMatchers.named("toBaseType") |
| 1018 | + .and(ElementMatchers.takesArguments(1))) |
| 1019 | + .getOnly())); |
| 1020 | + return new ShortCircuitReturnNull(readValue, stackManipulation); |
| 1021 | + } |
| 1022 | + |
918 | 1023 | @Override |
919 | 1024 | protected StackManipulation convertByteBuffer(TypeDescriptor<?> type) { |
920 | 1025 | // Generate the following code: |
@@ -1361,6 +1466,29 @@ protected StackManipulation convertEnum(TypeDescriptor<?> type) { |
1361 | 1466 | return new ShortCircuitReturnNull(readValue, stackManipulation); |
1362 | 1467 | } |
1363 | 1468 |
|
| 1469 | + @Override |
| 1470 | + protected StackManipulation convertLogicalType(TypeDescriptor<?> type) { |
| 1471 | + // Equivalent code: return (JavaType) STATIC_LOGICAL_TYPE.toInputType(value); |
| 1472 | + // FromRowUsingCreator already converted the row's input-type value (e.g. LocalDate) |
| 1473 | + // to the LogicalType's base value (e.g. Long) before invoking the generated creator, |
| 1474 | + // so we receive the base type here and need to project back to the POJO field's |
| 1475 | + // Java type. |
| 1476 | + ForLoadedType loadedType = new ForLoadedType(type.getRawType()); |
| 1477 | + StackManipulation stackManipulation = |
| 1478 | + new Compound( |
| 1479 | + loadLogicalType(type.getRawType()), |
| 1480 | + readValue, |
| 1481 | + MethodInvocation.invoke( |
| 1482 | + LOGICAL_TYPE_TYPE |
| 1483 | + .getDeclaredMethods() |
| 1484 | + .filter( |
| 1485 | + ElementMatchers.named("toInputType") |
| 1486 | + .and(ElementMatchers.takesArguments(1))) |
| 1487 | + .getOnly()), |
| 1488 | + TypeCasting.to(loadedType)); |
| 1489 | + return new ShortCircuitReturnNull(readValue, stackManipulation); |
| 1490 | + } |
| 1491 | + |
1364 | 1492 | @Override |
1365 | 1493 | protected StackManipulation convertDefault(TypeDescriptor<?> type) { |
1366 | 1494 | return readValue; |
|
0 commit comments