blob: 8540b3d683bb59356793fd746c8beb01487f6157 [file] [log] [blame]
/*
* 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;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.beam.sdk.annotations.Experimental;
import org.apache.beam.sdk.annotations.Experimental.Kind;
import org.apache.beam.sdk.schemas.annotations.SchemaFieldName;
import org.apache.beam.sdk.schemas.annotations.SchemaIgnore;
import org.apache.beam.sdk.schemas.utils.FieldValueTypeSupplier;
import org.apache.beam.sdk.schemas.utils.JavaBeanUtils;
import org.apache.beam.sdk.schemas.utils.ReflectUtils;
import org.apache.beam.sdk.values.TypeDescriptor;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.annotations.VisibleForTesting;
/**
* A {@link SchemaProvider} for Java Bean objects.
*
* <p>This provider finds (recursively) all public getters and setters in a Java object, and creates
* schemas and rows that bind to those fields. The field order in the schema is not guaranteed to
* match the method order in the class. The Java object is expected to have implemented a correct
* .equals() and .hashCode methods The equals method must be completely determined by the schema
* fields. i.e. if the object has hidden fields that are not reflected in the schema but are
* compared in equals, then results will be incorrect.
*
* <p>TODO: Validate equals() method is provided, and if not generate a "slow" equals method based
* on the schema.
*/
@Experimental(Kind.SCHEMAS)
public class JavaBeanSchema extends GetterBasedSchemaProvider {
/** {@link FieldValueTypeSupplier} that's based on getter methods. */
@VisibleForTesting
public static class GetterTypeSupplier implements FieldValueTypeSupplier {
public static final GetterTypeSupplier INSTANCE = new GetterTypeSupplier();
@Override
public List<FieldValueTypeInformation> get(Class<?> clazz) {
return ReflectUtils.getMethods(clazz).stream()
.filter(ReflectUtils::isGetter)
.filter(m -> !m.isAnnotationPresent(SchemaIgnore.class))
.map(FieldValueTypeInformation::forGetter)
.map(
t -> {
SchemaFieldName fieldName = t.getMethod().getAnnotation(SchemaFieldName.class);
return (fieldName != null) ? t.withName(fieldName.value()) : t;
})
.collect(Collectors.toList());
}
}
/** {@link FieldValueTypeSupplier} that's based on setter methods. */
@VisibleForTesting
public static class SetterTypeSupplier implements FieldValueTypeSupplier {
private static final SetterTypeSupplier INSTANCE = new SetterTypeSupplier();
@Override
public List<FieldValueTypeInformation> get(Class<?> clazz) {
return ReflectUtils.getMethods(clazz).stream()
.filter(ReflectUtils::isSetter)
.filter(m -> !m.isAnnotationPresent(SchemaIgnore.class))
.map(FieldValueTypeInformation::forSetter)
.collect(Collectors.toList());
}
}
@Override
public <T> Schema schemaFor(TypeDescriptor<T> typeDescriptor) {
Schema schema =
JavaBeanUtils.schemaFromJavaBeanClass(
typeDescriptor.getRawType(), GetterTypeSupplier.INSTANCE);
// If there are no creator methods, then validate that we have setters for every field.
// Otherwise, we will have not way of creating the class.
if (ReflectUtils.getAnnotatedCreateMethod(typeDescriptor.getRawType()) == null
&& ReflectUtils.getAnnotatedConstructor(typeDescriptor.getRawType()) == null) {
JavaBeanUtils.validateJavaBean(
GetterTypeSupplier.INSTANCE.get(typeDescriptor.getRawType(), schema),
SetterTypeSupplier.INSTANCE.get(typeDescriptor.getRawType(), schema));
}
return schema;
}
@Override
public FieldValueGetterFactory fieldValueGetterFactory() {
return (Class<?> targetClass, Schema schema) ->
JavaBeanUtils.getGetters(targetClass, schema, GetterTypeSupplier.INSTANCE);
}
@Override
UserTypeCreatorFactory schemaTypeCreatorFactory() {
UserTypeCreatorFactory setterBasedFactory =
new SetterBasedCreatorFactory(new JavaBeanSetterFactory());
return (Class<?> targetClass, Schema schema) -> {
// If a static method is marked with @SchemaCreate, use that.
Method annotated = ReflectUtils.getAnnotatedCreateMethod(targetClass);
if (annotated != null) {
return JavaBeanUtils.getStaticCreator(
targetClass, annotated, schema, GetterTypeSupplier.INSTANCE);
}
// If a Constructor was tagged with @SchemaCreate, invoke that constructor.
Constructor<?> constructor = ReflectUtils.getAnnotatedConstructor(targetClass);
if (constructor != null) {
return JavaBeanUtils.getConstructorCreator(
targetClass, constructor, schema, GetterTypeSupplier.INSTANCE);
}
return setterBasedFactory.create(targetClass, schema);
};
}
@Override
public FieldValueTypeInformationFactory fieldValueTypeInformationFactory() {
return (Class<?> targetClass, Schema schema) ->
JavaBeanUtils.getFieldTypes(targetClass, schema, GetterTypeSupplier.INSTANCE);
}
/** A factory for creating {@link FieldValueSetter} objects for a JavaBean object. */
public static class JavaBeanSetterFactory implements FieldValueSetterFactory {
@Override
public List<FieldValueSetter> create(Class<?> targetClass, Schema schema) {
return JavaBeanUtils.getSetters(targetClass, schema, SetterTypeSupplier.INSTANCE);
}
}
}