blob: 57998a935671e67d81241067fa72e1d62ac34b25 [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.util;
import static org.apache.beam.vendor.guava.v20_0.com.google.common.base.Preconditions.checkArgument;
import static org.apache.beam.vendor.guava.v20_0.com.google.common.base.Preconditions.checkState;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import org.apache.beam.sdk.values.TypeDescriptor;
import org.apache.beam.vendor.guava.v20_0.com.google.common.base.Joiner;
/**
* Utility for creating objects dynamically.
*
* @param <T> type type of object returned by this instance builder
*/
public class InstanceBuilder<T> {
/**
* Create an InstanceBuilder for the given type.
*
* <p>The specified type is the type returned by {@link #build}, which is typically the common
* base type or interface of the instance being constructed.
*/
public static <T> InstanceBuilder<T> ofType(Class<T> type) {
return new InstanceBuilder<>(type);
}
/**
* Create an InstanceBuilder for the given type.
*
* <p>The specified type is the type returned by {@link #build}, which is typically the common
* base type or interface for the instance to be constructed.
*
* <p>The TypeDescriptor argument allows specification of generic types. For example, a {@code
* List<String>} return type can be specified as {@code ofType(new
* TypeDescriptor<List<String>>(){})}.
*/
public static <T> InstanceBuilder<T> ofType(TypeDescriptor<T> token) {
@SuppressWarnings("unchecked")
Class<T> type = (Class<T>) token.getRawType();
return new InstanceBuilder<>(type);
}
/**
* Sets the class name to be constructed.
*
* <p>If the name is a simple name (ie {@link Class#getSimpleName()}), then the package of the
* return type is added as a prefix.
*
* <p>The default class is the return type, specified in {@link #ofType}.
*
* <p>Modifies and returns the {@code InstanceBuilder} for chaining.
*
* @throws ClassNotFoundException if no class can be found by the given name
*/
public InstanceBuilder<T> fromClassName(String name) throws ClassNotFoundException {
checkArgument(factoryClass == null, "Class name may only be specified once");
if (name.indexOf('.') == -1) {
name = type.getPackage().getName() + "." + name;
}
try {
factoryClass = Class.forName(name);
} catch (ClassNotFoundException e) {
throw new ClassNotFoundException(String.format("Could not find class: %s", name), e);
}
return this;
}
/**
* Sets the factory class to use for instance construction.
*
* <p>Modifies and returns the {@code InstanceBuilder} for chaining.
*/
public InstanceBuilder<T> fromClass(Class<?> factoryClass) {
this.factoryClass = factoryClass;
return this;
}
/**
* Sets the name of the factory method used to construct the instance.
*
* <p>The default, if no factory method was specified, is to look for a class constructor.
*
* <p>Modifies and returns the {@code InstanceBuilder} for chaining.
*/
public InstanceBuilder<T> fromFactoryMethod(String methodName) {
checkArgument(this.methodName == null, "Factory method name may only be specified once");
this.methodName = methodName;
return this;
}
/**
* Adds an argument to be passed to the factory method.
*
* <p>The argument type is used to lookup the factory method. This type may be a supertype of the
* argument value's class.
*
* <p>Modifies and returns the {@code InstanceBuilder} for chaining.
*
* @param <ArgT> the argument type
*/
public <ArgT> InstanceBuilder<T> withArg(Class<? super ArgT> argType, ArgT value) {
parameterTypes.add(argType);
arguments.add(value);
return this;
}
/**
* Creates the instance by calling the factory method with the given arguments.
*
* <h3>Defaults</h3>
*
* <ul>
* <li>factory class: defaults to the output type class, overridden via {@link
* #fromClassName(String)}.
* <li>factory method: defaults to using a constructor on the factory class, overridden via
* {@link #fromFactoryMethod(String)}.
* </ul>
*
* @throws RuntimeException if the method does not exist, on type mismatch, or if the method
* cannot be made accessible.
*/
public T build() {
if (factoryClass == null) {
factoryClass = type;
}
Class<?>[] types = parameterTypes.toArray(new Class<?>[parameterTypes.size()]);
// TODO: cache results, to speed repeated type lookups?
if (methodName != null) {
return buildFromMethod(types);
} else {
return buildFromConstructor(types);
}
}
/////////////////////////////////////////////////////////////////////////////
/** Type of object to construct. */
private final Class<T> type;
/**
* Types of parameters for Method lookup.
*
* @see Class#getDeclaredMethod(String, Class[])
*/
private final List<Class<?>> parameterTypes = new ArrayList<>();
/** Arguments to factory method {@link Method#invoke(Object, Object...)}. */
private final List<Object> arguments = new ArrayList<>();
/** Name of factory method, or null to invoke the constructor. */
@Nullable private String methodName;
/** Factory class, or null to instantiate {@code type}. */
@Nullable private Class<?> factoryClass;
private InstanceBuilder(Class<T> type) {
this.type = type;
}
private T buildFromMethod(Class<?>[] types) {
checkState(factoryClass != null);
checkState(methodName != null);
try {
Method method = factoryClass.getDeclaredMethod(methodName, types);
checkState(
Modifier.isStatic(method.getModifiers()),
"Factory method must be a static method for "
+ factoryClass.getName()
+ "#"
+ method.getName());
checkState(
type.isAssignableFrom(method.getReturnType()),
"Return type for "
+ factoryClass.getName()
+ "#"
+ method.getName()
+ " must be assignable to "
+ type.getSimpleName());
if (!method.isAccessible()) {
method.setAccessible(true);
}
Object[] args = arguments.toArray(new Object[arguments.size()]);
return type.cast(method.invoke(null, args));
} catch (NoSuchMethodException e) {
throw new RuntimeException(
String.format(
"Unable to find factory method %s#%s(%s)",
factoryClass.getSimpleName(), methodName, Joiner.on(", ").join(types)));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(
String.format(
"Failed to construct instance from factory method %s#%s(%s)",
factoryClass.getSimpleName(), methodName, Joiner.on(", ").join(types)),
e);
}
}
private T buildFromConstructor(Class<?>[] types) {
checkState(factoryClass != null);
try {
Constructor<?> constructor = factoryClass.getDeclaredConstructor(types);
checkState(
type.isAssignableFrom(factoryClass),
"Instance type "
+ factoryClass.getName()
+ " must be assignable to "
+ type.getSimpleName());
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
Object[] args = arguments.toArray(new Object[arguments.size()]);
return type.cast(constructor.newInstance(args));
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find constructor for " + factoryClass.getName());
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(
"Failed to construct instance from " + "constructor " + factoryClass.getName(), e);
}
}
}