| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.beam.sdk.schemas.utils; |
| |
| import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkNotNull; |
| |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.lang.reflect.Parameter; |
| import java.lang.reflect.Type; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import org.apache.avro.generic.GenericFixed; |
| import org.apache.beam.sdk.schemas.FieldValueGetter; |
| import org.apache.beam.sdk.schemas.FieldValueSetter; |
| import org.apache.beam.sdk.schemas.FieldValueTypeInformation; |
| import org.apache.beam.sdk.values.TypeDescriptor; |
| import org.apache.beam.sdk.values.TypeParameter; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.ByteBuddy; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.NamingStrategy; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.NamingStrategy.SuffixingRandom.BaseNameResolver; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.description.method.MethodDescription.ForLoadedConstructor; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.description.method.MethodDescription.ForLoadedMethod; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.description.type.TypeDescription; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.description.type.TypeDescription.ForLoadedType; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.dynamic.DynamicType; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.dynamic.scaffold.InstrumentedType; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.Implementation; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.ByteCodeAppender; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.ByteCodeAppender.Size; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.Duplication; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.StackManipulation; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.StackManipulation.Compound; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.TypeCreation; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.assign.Assigner; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.assign.Assigner.Typing; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.assign.TypeCasting; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.collection.ArrayAccess; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.collection.ArrayFactory; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.constant.IntegerConstant; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.member.FieldAccess; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.member.MethodInvocation; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.member.MethodReturn; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.implementation.bytecode.member.MethodVariableAccess; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.matcher.ElementMatchers; |
| import org.apache.beam.vendor.bytebuddy.v1_9_3.net.bytebuddy.utility.RandomString; |
| import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Lists; |
| import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Maps; |
| import org.apache.commons.lang3.ArrayUtils; |
| import org.apache.commons.lang3.ClassUtils; |
| import org.joda.time.DateTimeZone; |
| import org.joda.time.Instant; |
| import org.joda.time.ReadableInstant; |
| import org.joda.time.ReadablePartial; |
| import org.joda.time.base.BaseLocal; |
| |
| class ByteBuddyUtils { |
| private static final ForLoadedType ARRAYS_TYPE = new ForLoadedType(Arrays.class); |
| private static final ForLoadedType ARRAY_UTILS_TYPE = new ForLoadedType(ArrayUtils.class); |
| private static final ForLoadedType BYTE_ARRAY_TYPE = new ForLoadedType(byte[].class); |
| private static final ForLoadedType BYTE_BUFFER_TYPE = new ForLoadedType(ByteBuffer.class); |
| private static final ForLoadedType CHAR_SEQUENCE_TYPE = new ForLoadedType(CharSequence.class); |
| private static final ForLoadedType INSTANT_TYPE = new ForLoadedType(Instant.class); |
| private static final ForLoadedType DATE_TIME_ZONE_TYPE = new ForLoadedType(DateTimeZone.class); |
| private static final ForLoadedType LIST_TYPE = new ForLoadedType(List.class); |
| private static final ForLoadedType READABLE_INSTANT_TYPE = |
| new ForLoadedType(ReadableInstant.class); |
| private static final ForLoadedType READABLE_PARTIAL_TYPE = |
| new ForLoadedType(ReadablePartial.class); |
| private static final ForLoadedType OBJECT_TYPE = new ForLoadedType(Object.class); |
| |
| /** |
| * A naming strategy for ByteBuddy classes. |
| * |
| * <p>We always inject the generator classes in the same same package as the user's target class. |
| * This way, if the class fields or methods are package private, our generated class can still |
| * access them. |
| */ |
| static class InjectPackageStrategy extends NamingStrategy.AbstractBase { |
| /** A resolver for the base name for naming the unnamed type. */ |
| private static final BaseNameResolver baseNameResolver = |
| BaseNameResolver.ForUnnamedType.INSTANCE; |
| |
| private static final String SUFFIX = "SchemaCodeGen"; |
| |
| private final RandomString randomString; |
| |
| private final String targetPackage; |
| |
| public InjectPackageStrategy(Class<?> baseType) { |
| randomString = new RandomString(); |
| this.targetPackage = baseType.getPackage().getName(); |
| } |
| |
| @Override |
| protected String name(TypeDescription superClass) { |
| String baseName = baseNameResolver.resolve(superClass); |
| int lastDot = baseName.lastIndexOf('.'); |
| String className = baseName.substring(lastDot, baseName.length()); |
| return targetPackage + className + "$" + SUFFIX + "$" + randomString.nextString(); |
| } |
| }; |
| |
| // Create a new FieldValueGetter subclass. |
| @SuppressWarnings("unchecked") |
| static DynamicType.Builder<FieldValueGetter> subclassGetterInterface( |
| ByteBuddy byteBuddy, Type objectType, Type fieldType) { |
| TypeDescription.Generic getterGenericType = |
| TypeDescription.Generic.Builder.parameterizedType( |
| FieldValueGetter.class, objectType, fieldType) |
| .build(); |
| return (DynamicType.Builder<FieldValueGetter>) |
| byteBuddy.with(new InjectPackageStrategy((Class) objectType)).subclass(getterGenericType); |
| } |
| |
| // Create a new FieldValueSetter subclass. |
| @SuppressWarnings("unchecked") |
| static DynamicType.Builder<FieldValueSetter> subclassSetterInterface( |
| ByteBuddy byteBuddy, Type objectType, Type fieldType) { |
| TypeDescription.Generic setterGenericType = |
| TypeDescription.Generic.Builder.parameterizedType( |
| FieldValueSetter.class, objectType, fieldType) |
| .build(); |
| return (DynamicType.Builder<FieldValueSetter>) |
| byteBuddy.with(new InjectPackageStrategy((Class) objectType)).subclass(setterGenericType); |
| } |
| |
| // Base class used below to convert types. |
| @SuppressWarnings("unchecked") |
| abstract static class TypeConversion<T> { |
| public T convert(TypeDescriptor typeDescriptor) { |
| if (typeDescriptor.isArray() |
| && !typeDescriptor.getComponentType().getRawType().equals(byte.class)) { |
| // Byte arrays are special, so leave those alone. |
| return convertArray(typeDescriptor); |
| } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(Collection.class))) { |
| return convertCollection(typeDescriptor); |
| } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(Map.class))) { |
| return convertMap(typeDescriptor); |
| } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(ReadableInstant.class))) { |
| return convertDateTime(typeDescriptor); |
| } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(ReadablePartial.class))) { |
| return convertDateTime(typeDescriptor); |
| } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(ByteBuffer.class))) { |
| return convertByteBuffer(typeDescriptor); |
| } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(GenericFixed.class))) { |
| // TODO: Refactor AVRO-specific check into separate class. |
| return convertGenericFixed(typeDescriptor); |
| } else if (typeDescriptor.isSubtypeOf(TypeDescriptor.of(CharSequence.class))) { |
| return convertCharSequence(typeDescriptor); |
| } else if (typeDescriptor.getRawType().isPrimitive()) { |
| return convertPrimitive(typeDescriptor); |
| } else if (typeDescriptor.getRawType().isEnum()) { |
| return convertEnum(typeDescriptor); |
| } else { |
| return convertDefault(typeDescriptor); |
| } |
| } |
| |
| protected abstract T convertArray(TypeDescriptor<?> type); |
| |
| protected abstract T convertCollection(TypeDescriptor<?> type); |
| |
| protected abstract T convertMap(TypeDescriptor<?> type); |
| |
| protected abstract T convertDateTime(TypeDescriptor<?> type); |
| |
| protected abstract T convertByteBuffer(TypeDescriptor<?> type); |
| |
| protected abstract T convertGenericFixed(TypeDescriptor<?> type); |
| |
| protected abstract T convertCharSequence(TypeDescriptor<?> type); |
| |
| protected abstract T convertPrimitive(TypeDescriptor<?> type); |
| |
| protected abstract T convertEnum(TypeDescriptor<?> type); |
| |
| protected abstract T convertDefault(TypeDescriptor<?> type); |
| } |
| |
| /** |
| * Give a Java type, returns the Java type expected for use with Row. For example, both {@link |
| * StringBuffer} and {@link String} are represented as a {@link String} in Row. This determines |
| * what the return type of the getter will be. For instance, the following POJO class: |
| * |
| * <pre><code> |
| * class POJO { |
| * StringBuffer str; |
| * int[] array; |
| * } |
| * </code></pre> |
| * |
| * Generates the following getters: |
| * |
| * <pre><code>{@literal FieldValueGetter<POJO, String>}</code></pre> |
| * |
| * <pre><code>{@literal FieldValueGetter<POJO, List<Integer>>}</code></pre> |
| */ |
| static class ConvertType extends TypeConversion<Type> { |
| private boolean returnRawTypes; |
| |
| public ConvertType(boolean returnRawTypes) { |
| this.returnRawTypes = returnRawTypes; |
| } |
| |
| @Override |
| protected Type convertArray(TypeDescriptor<?> type) { |
| TypeDescriptor ret = createListType(type); |
| return returnRawTypes ? ret.getRawType() : ret.getType(); |
| } |
| |
| @Override |
| protected Type convertCollection(TypeDescriptor<?> type) { |
| return Collection.class; |
| } |
| |
| @Override |
| protected Type convertMap(TypeDescriptor<?> type) { |
| return Map.class; |
| } |
| |
| @Override |
| protected Type convertDateTime(TypeDescriptor<?> type) { |
| return Instant.class; |
| } |
| |
| @Override |
| protected Type convertByteBuffer(TypeDescriptor<?> type) { |
| return byte[].class; |
| } |
| |
| @Override |
| protected Type convertGenericFixed(TypeDescriptor<?> type) { |
| return byte[].class; |
| } |
| |
| @Override |
| protected Type convertCharSequence(TypeDescriptor<?> type) { |
| return String.class; |
| } |
| |
| @Override |
| protected Type convertPrimitive(TypeDescriptor<?> type) { |
| return ClassUtils.primitiveToWrapper(type.getRawType()); |
| } |
| |
| @Override |
| protected Type convertEnum(TypeDescriptor<?> type) { |
| return String.class; |
| } |
| |
| @Override |
| protected Type convertDefault(TypeDescriptor<?> type) { |
| return returnRawTypes ? type.getRawType() : type.getType(); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private <ElementT> TypeDescriptor<List<ElementT>> createListType(TypeDescriptor<?> type) { |
| TypeDescriptor componentType = |
| TypeDescriptor.of(ClassUtils.primitiveToWrapper(type.getComponentType().getRawType())); |
| return new TypeDescriptor<List<ElementT>>() {}.where( |
| new TypeParameter<ElementT>() {}, componentType); |
| } |
| } |
| |
| /** |
| * Takes a {@link StackManipulation} that returns a value. Prepares this value to be returned by a |
| * getter. {@link org.apache.beam.sdk.values.Row} needs getters to return specific types, but we |
| * allow user objects to contain different but equivalent types. Therefore we must convert some of |
| * these types before returning. These conversions correspond to the ones defined in {@link |
| * ConvertType}. This class generates the code to do these conversion. |
| */ |
| static class ConvertValueForGetter extends TypeConversion<StackManipulation> { |
| // The code that reads the value. |
| private final StackManipulation readValue; |
| |
| ConvertValueForGetter(StackManipulation readValue) { |
| this.readValue = readValue; |
| } |
| |
| @Override |
| protected StackManipulation convertArray(TypeDescriptor<?> type) { |
| // Generate the following code: |
| // return isComponentTypePrimitive ? Arrays.asList(ArrayUtils.toObject(value)) |
| // : Arrays.asList(value); |
| |
| ForLoadedType loadedType = new ForLoadedType(type.getRawType()); |
| StackManipulation stackManipulation = readValue; |
| // Row always expects to get an Iterable back for array types. Wrap this array into a |
| // List using Arrays.asList before returning. |
| if (loadedType.getComponentType().isPrimitive()) { |
| // Arrays.asList doesn't take primitive arrays, so convert first using ArrayUtils.toObject. |
| stackManipulation = |
| new Compound( |
| stackManipulation, |
| MethodInvocation.invoke( |
| ARRAY_UTILS_TYPE |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.isStatic() |
| .and(ElementMatchers.named("toObject")) |
| .and(ElementMatchers.takesArguments(loadedType))) |
| .getOnly())); |
| } |
| return new Compound( |
| stackManipulation, |
| MethodInvocation.invoke( |
| ARRAYS_TYPE |
| .getDeclaredMethods() |
| .filter(ElementMatchers.isStatic().and(ElementMatchers.named("asList"))) |
| .getOnly())); |
| } |
| |
| @Override |
| protected StackManipulation convertCollection(TypeDescriptor<?> type) { |
| return readValue; |
| } |
| |
| @Override |
| protected StackManipulation convertMap(TypeDescriptor<?> type) { |
| return readValue; |
| } |
| |
| @Override |
| protected StackManipulation convertDateTime(TypeDescriptor<?> type) { |
| // If the class type is an Instant, then return it. |
| if (Instant.class.isAssignableFrom(type.getRawType())) { |
| return readValue; |
| } |
| |
| // Otherwise, generate the following code: |
| // |
| // for ReadableInstant: |
| // return new Instant(value.getMillis()); |
| // |
| // for ReadablePartial: |
| // return new Instant((value.toDateTime(Instant.EPOCH)).getMillis()); |
| |
| List<StackManipulation> stackManipulations = new ArrayList<>(); |
| |
| // Create a new instance of the target type. |
| stackManipulations.add(TypeCreation.of(INSTANT_TYPE)); |
| stackManipulations.add(Duplication.SINGLE); |
| |
| // if value is ReadablePartial, convert it to ReadableInstant first |
| if (ReadablePartial.class.isAssignableFrom(type.getRawType())) { |
| // Generate the following code: .toDateTime(Instant.EPOCH) |
| |
| // Load the parameter and cast it to ReadablePartial. |
| stackManipulations.add(readValue); |
| stackManipulations.add(TypeCasting.to(READABLE_PARTIAL_TYPE)); |
| |
| // Get Instant.EPOCH |
| stackManipulations.add( |
| FieldAccess.forField( |
| INSTANT_TYPE |
| .getDeclaredFields() |
| .filter(ElementMatchers.named("EPOCH")) |
| .getOnly()) |
| .read()); |
| |
| // Call ReadablePartial.toDateTime |
| stackManipulations.add( |
| MethodInvocation.invoke( |
| READABLE_PARTIAL_TYPE |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.named("toDateTime") |
| .and(ElementMatchers.takesArguments(READABLE_INSTANT_TYPE))) |
| .getOnly())); |
| } else { |
| // Otherwise, parameter is already ReadableInstant. |
| // Load the parameter and cast it to ReadableInstant. |
| stackManipulations.add(readValue); |
| stackManipulations.add(TypeCasting.to(READABLE_INSTANT_TYPE)); |
| } |
| |
| // Call ReadableInstant.getMillis to extract the millis since the epoch. |
| stackManipulations.add( |
| MethodInvocation.invoke( |
| READABLE_INSTANT_TYPE |
| .getDeclaredMethods() |
| .filter(ElementMatchers.named("getMillis")) |
| .getOnly())); |
| |
| // Construct a Instant object containing the millis. |
| stackManipulations.add( |
| MethodInvocation.invoke( |
| INSTANT_TYPE |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.isConstructor() |
| .and(ElementMatchers.takesArguments(ForLoadedType.of(long.class)))) |
| .getOnly())); |
| |
| return new StackManipulation.Compound(stackManipulations); |
| } |
| |
| @Override |
| protected StackManipulation convertByteBuffer(TypeDescriptor<?> type) { |
| // Generate the following code: |
| // return value.array(); |
| |
| // We must extract the array from the ByteBuffer before returning. |
| // NOTE: we only support array-backed byte buffers in these POJOs. Others (e.g. mmaped |
| // files) are not supported. |
| return new Compound( |
| readValue, |
| MethodInvocation.invoke( |
| BYTE_BUFFER_TYPE |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.named("array").and(ElementMatchers.returns(BYTE_ARRAY_TYPE))) |
| .getOnly())); |
| } |
| |
| @Override |
| protected StackManipulation convertGenericFixed(TypeDescriptor<?> type) { |
| // TODO: Refactor AVRO-specific code into separate class. |
| |
| // Generate the following code: |
| // return value.bytes(); |
| |
| return new Compound( |
| readValue, |
| MethodInvocation.invoke( |
| new ForLoadedType(GenericFixed.class) |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.named("bytes").and(ElementMatchers.returns(BYTE_ARRAY_TYPE))) |
| .getOnly())); |
| } |
| |
| @Override |
| protected StackManipulation convertCharSequence(TypeDescriptor<?> type) { |
| // If the member is a String, then return it. |
| if (type.isSubtypeOf(TypeDescriptor.of(String.class))) { |
| return readValue; |
| } |
| |
| // Otherwise, generate the following code: |
| // return value.toString(); |
| return new Compound( |
| readValue, |
| MethodInvocation.invoke( |
| CHAR_SEQUENCE_TYPE |
| .getDeclaredMethods() |
| .filter(ElementMatchers.named("toString").and(ElementMatchers.takesArguments(0))) |
| .getOnly())); |
| } |
| |
| @Override |
| protected StackManipulation convertPrimitive(TypeDescriptor<?> type) { |
| ForLoadedType loadedType = new ForLoadedType(type.getRawType()); |
| // Box the primitive type. |
| return new Compound( |
| readValue, |
| Assigner.DEFAULT.assign( |
| loadedType.asGenericType(), loadedType.asBoxed().asGenericType(), Typing.STATIC)); |
| } |
| |
| @Override |
| protected StackManipulation convertEnum(TypeDescriptor<?> type) { |
| return new Compound( |
| readValue, |
| MethodInvocation.invoke( |
| OBJECT_TYPE |
| .getDeclaredMethods() |
| .filter(ElementMatchers.named("toString").and(ElementMatchers.takesArguments(0))) |
| .getOnly())); |
| } |
| |
| @Override |
| protected StackManipulation convertDefault(TypeDescriptor<?> type) { |
| return readValue; |
| } |
| } |
| |
| /** |
| * Row is going to call the setter with its internal Java type, however the user object being set |
| * might have a different type internally. For example, Row will be calling set with a {@link |
| * String} type (for string fields), but the user type might have a {@link StringBuffer} member |
| * there. This class generates code to convert between these types. |
| */ |
| static class ConvertValueForSetter extends TypeConversion<StackManipulation> { |
| StackManipulation readValue; |
| |
| ConvertValueForSetter(StackManipulation readValue) { |
| this.readValue = readValue; |
| } |
| |
| @Override |
| protected StackManipulation convertArray(TypeDescriptor<?> type) { |
| // Generate the following code: |
| // T[] toArray = (T[]) value.toArray(new T[0]); |
| // return isPrimitive ? toArray : ArrayUtils.toPrimitive(toArray); |
| |
| ForLoadedType loadedType = new ForLoadedType(type.getRawType()); |
| // The type of the array containing the (possibly) boxed values. |
| TypeDescription arrayType = |
| TypeDescription.Generic.Builder.rawType(loadedType.getComponentType().asBoxed()) |
| .asArray() |
| .build() |
| .asErasure(); |
| |
| // Extract an array from the collection. |
| StackManipulation stackManipulation = |
| new Compound( |
| readValue, |
| TypeCasting.to(LIST_TYPE), |
| // Call Collection.toArray(T[[]) to extract the array. Push new T[0] on the stack |
| // before |
| // calling toArray. |
| ArrayFactory.forType(loadedType.getComponentType().asBoxed().asGenericType()) |
| .withValues(Collections.emptyList()), |
| MethodInvocation.invoke( |
| LIST_TYPE |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.named("toArray").and(ElementMatchers.takesArguments(1))) |
| .getOnly()), |
| // Cast the result to T[]. |
| TypeCasting.to(arrayType)); |
| |
| if (loadedType.getComponentType().isPrimitive()) { |
| // The array we extract will be an array of objects. If the pojo field is an array of |
| // primitive types, we need to then convert to an array of unboxed objects. |
| stackManipulation = |
| new StackManipulation.Compound( |
| stackManipulation, |
| MethodInvocation.invoke( |
| ARRAY_UTILS_TYPE |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.named("toPrimitive") |
| .and(ElementMatchers.takesArguments(arrayType))) |
| .getOnly())); |
| } |
| return stackManipulation; |
| } |
| |
| @Override |
| protected StackManipulation convertCollection(TypeDescriptor<?> type) { |
| return readValue; |
| } |
| |
| @Override |
| protected StackManipulation convertMap(TypeDescriptor<?> type) { |
| return readValue; |
| } |
| |
| @Override |
| protected StackManipulation convertDateTime(TypeDescriptor<?> type) { |
| // The setter might be called with a different subclass of ReadableInstant than the one stored |
| // in this POJO. We must extract the value passed into the setter and copy it into an instance |
| // that the POJO can accept. |
| |
| // Generate the following code: |
| // return new T(value.getMillis()); |
| // Unless T is a sub-class of BaseLocal. Then generate: |
| // return new T(value.getMillis(), DateTimeZone.UTC); |
| |
| ForLoadedType loadedType = new ForLoadedType(type.getRawType()); |
| List<StackManipulation> stackManipulations = new ArrayList<>(); |
| |
| // Create a new instance of the target ype. |
| stackManipulations.add(TypeCreation.of(loadedType)); |
| stackManipulations.add(Duplication.SINGLE); |
| // Load the parameter and cast it to a ReadableInstant. |
| stackManipulations.add(readValue); |
| stackManipulations.add(TypeCasting.to(READABLE_INSTANT_TYPE)); |
| // Call ReadableInstant.getMillis to extract the millis since the epoch. |
| stackManipulations.add( |
| MethodInvocation.invoke( |
| READABLE_INSTANT_TYPE |
| .getDeclaredMethods() |
| .filter(ElementMatchers.named("getMillis")) |
| .getOnly())); |
| if (type.isSubtypeOf(TypeDescriptor.of(BaseLocal.class))) { |
| // Access DateTimeZone.UTC |
| stackManipulations.add( |
| FieldAccess.forField( |
| DATE_TIME_ZONE_TYPE |
| .getDeclaredFields() |
| .filter(ElementMatchers.named("UTC")) |
| .getOnly()) |
| .read()); |
| // All subclasses of BaseLocal contain a ()(long, DateTimeZone) constructor |
| // that takes in a millis and time zone argument. Call that constructor of the field to |
| // initialize it. |
| stackManipulations.add( |
| MethodInvocation.invoke( |
| loadedType |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.isConstructor() |
| .and( |
| ElementMatchers.takesArguments( |
| ForLoadedType.of(long.class), DATE_TIME_ZONE_TYPE))) |
| .getOnly())); |
| } else { |
| // All subclasses of ReadableInstant and ReadablePartial contain a ()(long) constructor |
| // that takes in a millis argument. Call that constructor of the field to initialize it. |
| stackManipulations.add( |
| MethodInvocation.invoke( |
| loadedType |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.isConstructor() |
| .and(ElementMatchers.takesArguments(ForLoadedType.of(long.class)))) |
| .getOnly())); |
| } |
| |
| return new Compound(stackManipulations); |
| } |
| |
| @Override |
| protected StackManipulation convertByteBuffer(TypeDescriptor<?> type) { |
| // Generate the following code: |
| // return ByteBuffer.wrap((byte[]) value); |
| |
| // We currently assume that a byte[] setter will always accept a parameter of type byte[]. |
| return new Compound( |
| readValue, |
| TypeCasting.to(BYTE_ARRAY_TYPE), |
| // Create a new ByteBuffer that wraps this byte[]. |
| MethodInvocation.invoke( |
| BYTE_BUFFER_TYPE |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.named("wrap") |
| .and(ElementMatchers.takesArguments(BYTE_ARRAY_TYPE))) |
| .getOnly())); |
| } |
| |
| @Override |
| protected StackManipulation convertGenericFixed(TypeDescriptor<?> type) { |
| // Generate the following code: |
| // return new T((byte[]) value); |
| |
| // TODO: Refactor AVRO-specific code out of this class. |
| ForLoadedType loadedType = new ForLoadedType(type.getRawType()); |
| return new Compound( |
| TypeCreation.of(loadedType), |
| Duplication.SINGLE, |
| // Load the parameter and cast it to a byte[]. |
| readValue, |
| TypeCasting.to(BYTE_ARRAY_TYPE), |
| // Create a new instance that wraps this byte[]. |
| MethodInvocation.invoke( |
| loadedType |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.isConstructor() |
| .and(ElementMatchers.takesArguments(BYTE_ARRAY_TYPE))) |
| .getOnly())); |
| } |
| |
| @Override |
| protected StackManipulation convertCharSequence(TypeDescriptor<?> type) { |
| // If the type is a String, just return it. |
| if (type.getRawType().isAssignableFrom(String.class)) { |
| return readValue; |
| } |
| |
| // Otherwise, generate the following code: |
| // return new T((CharacterSequence) value). |
| |
| ForLoadedType loadedType = new ForLoadedType(type.getRawType()); |
| return new StackManipulation.Compound( |
| TypeCreation.of(loadedType), |
| Duplication.SINGLE, |
| // Load the parameter and cast it to a CharSequence. |
| readValue, |
| TypeCasting.to(CHAR_SEQUENCE_TYPE), |
| // Create an element of the field type that wraps this one. |
| MethodInvocation.invoke( |
| loadedType |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.isConstructor() |
| .and(ElementMatchers.takesArguments(CHAR_SEQUENCE_TYPE))) |
| .getOnly())); |
| } |
| |
| @Override |
| protected StackManipulation convertPrimitive(TypeDescriptor<?> type) { |
| ForLoadedType valueType = new ForLoadedType(type.getRawType()); |
| // Unbox the type. |
| return new StackManipulation.Compound( |
| readValue, |
| Assigner.DEFAULT.assign( |
| valueType.asBoxed().asGenericType(), |
| valueType.asUnboxed().asGenericType(), |
| Typing.STATIC)); |
| } |
| |
| @Override |
| protected StackManipulation convertEnum(TypeDescriptor<?> type) { |
| ForLoadedType loadedType = new ForLoadedType(type.getRawType()); |
| |
| return new Compound( |
| readValue, |
| MethodInvocation.invoke( |
| loadedType |
| .getDeclaredMethods() |
| .filter( |
| ElementMatchers.named("valueOf") |
| .and( |
| ElementMatchers.isStatic() |
| .and(ElementMatchers.takesArguments(String.class)))) |
| .getOnly())); |
| } |
| |
| @Override |
| protected StackManipulation convertDefault(TypeDescriptor<?> type) { |
| return readValue; |
| } |
| } |
| |
| /** |
| * Invokes a constructor registered using SchemaCreate. As constructor parameters might not be in |
| * the same order as the schema fields, reorders the parameters as necessary before calling the |
| * constructor. |
| */ |
| static class ConstructorCreateInstruction extends InvokeUserCreateInstruction { |
| private final Constructor constructor; |
| |
| ConstructorCreateInstruction( |
| List<FieldValueTypeInformation> fields, Class targetClass, Constructor constructor) { |
| super(fields, targetClass, Lists.newArrayList(constructor.getParameters())); |
| this.constructor = constructor; |
| } |
| |
| @Override |
| public InstrumentedType prepare(InstrumentedType instrumentedType) { |
| return instrumentedType; |
| } |
| |
| @Override |
| protected StackManipulation beforePushingParameters() { |
| // Create the target class. |
| ForLoadedType loadedType = new ForLoadedType(targetClass); |
| return new StackManipulation.Compound(TypeCreation.of(loadedType), Duplication.SINGLE); |
| } |
| |
| @Override |
| protected StackManipulation afterPushingParameters() { |
| return MethodInvocation.invoke(new ForLoadedConstructor(constructor)); |
| } |
| } |
| |
| /** |
| * Invokes a static factory method registered using SchemaCreate. As the method parameters might |
| * not be in the same order as the schema fields, reorders the parameters as necessary before |
| * calling the constructor. |
| */ |
| static class StaticFactoryMethodInstruction extends InvokeUserCreateInstruction { |
| private final Method creator; |
| |
| StaticFactoryMethodInstruction( |
| List<FieldValueTypeInformation> fields, Class targetClass, Method creator) { |
| super(fields, targetClass, Lists.newArrayList(creator.getParameters())); |
| if (!Modifier.isStatic(creator.getModifiers())) { |
| throw new IllegalArgumentException("Method " + creator + " is not static"); |
| } |
| this.creator = creator; |
| } |
| |
| @Override |
| public InstrumentedType prepare(InstrumentedType instrumentedType) { |
| return instrumentedType; |
| } |
| |
| @Override |
| protected StackManipulation afterPushingParameters() { |
| return MethodInvocation.invoke(new ForLoadedMethod(creator)); |
| } |
| } |
| |
| static class InvokeUserCreateInstruction implements Implementation { |
| protected final List<FieldValueTypeInformation> fields; |
| protected final Class targetClass; |
| protected final List<Parameter> parameters; |
| protected final Map<Integer, Integer> fieldMapping; |
| |
| protected InvokeUserCreateInstruction( |
| List<FieldValueTypeInformation> fields, Class targetClass, List<Parameter> parameters) { |
| this.fields = fields; |
| this.targetClass = targetClass; |
| this.parameters = parameters; |
| |
| // Method parameters might not be in the same order as the schema fields, and the input |
| // array to SchemaUserTypeCreator.create is in schema order. Examine the parameter names |
| // and compare against field names to calculate the mapping between the two lists. |
| Map<String, Integer> fieldsByLogicalName = Maps.newHashMap(); |
| Map<String, Integer> fieldsByJavaClassMember = Maps.newHashMap(); |
| for (int i = 0; i < fields.size(); ++i) { |
| // Method parameters are allowed to either correspond to the schema field names or to the |
| // actual Java field or method names. |
| FieldValueTypeInformation fieldValue = checkNotNull(fields.get(i)); |
| fieldsByLogicalName.put(fieldValue.getName(), i); |
| if (fieldValue.getField() != null) { |
| fieldsByJavaClassMember.put(fieldValue.getField().getName(), i); |
| } else if (fieldValue.getMethod() != null) { |
| String name = ReflectUtils.stripPrefix(fieldValue.getMethod().getName(), "set"); |
| fieldsByJavaClassMember.put(name, i); |
| } |
| } |
| |
| fieldMapping = Maps.newHashMap(); |
| for (int i = 0; i < parameters.size(); ++i) { |
| Parameter parameter = parameters.get(i); |
| String paramName = parameter.getName(); |
| Integer index = fieldsByLogicalName.get(paramName); |
| if (index == null) { |
| index = fieldsByJavaClassMember.get(paramName); |
| } |
| if (index == null) { |
| throw new RuntimeException( |
| "Creator parameter " + paramName + " Doesn't correspond to a schema field"); |
| } |
| fieldMapping.put(i, index); |
| } |
| } |
| |
| @Override |
| public InstrumentedType prepare(InstrumentedType instrumentedType) { |
| return instrumentedType; |
| } |
| |
| @Override |
| public ByteCodeAppender appender(final Target implementationTarget) { |
| return (methodVisitor, implementationContext, instrumentedMethod) -> { |
| // this + method parameters. |
| int numLocals = 1 + instrumentedMethod.getParameters().size(); |
| |
| StackManipulation stackManipulation = beforePushingParameters(); |
| |
| // Push all creator parameters on the stack. |
| ConvertType convertType = new ConvertType(true); |
| for (int i = 0; i < parameters.size(); i++) { |
| Parameter parameter = parameters.get(i); |
| ForLoadedType convertedType = |
| new ForLoadedType( |
| (Class) convertType.convert(TypeDescriptor.of(parameter.getType()))); |
| |
| // The instruction to read the parameter. Use the fieldMapping to reorder parameters as |
| // necessary. |
| StackManipulation readParameter = |
| new StackManipulation.Compound( |
| MethodVariableAccess.REFERENCE.loadFrom(1), |
| IntegerConstant.forValue(fieldMapping.get(i)), |
| ArrayAccess.REFERENCE.load(), |
| TypeCasting.to(convertedType)); |
| stackManipulation = |
| new StackManipulation.Compound( |
| stackManipulation, |
| new ConvertValueForSetter(readParameter) |
| .convert(TypeDescriptor.of(parameter.getType()))); |
| } |
| stackManipulation = |
| new StackManipulation.Compound( |
| stackManipulation, afterPushingParameters(), MethodReturn.REFERENCE); |
| |
| StackManipulation.Size size = stackManipulation.apply(methodVisitor, implementationContext); |
| return new Size(size.getMaximalSize(), numLocals); |
| }; |
| } |
| |
| protected StackManipulation beforePushingParameters() { |
| return new StackManipulation.Compound(); |
| } |
| |
| protected StackManipulation afterPushingParameters() { |
| return new StackManipulation.Compound(); |
| } |
| } |
| } |