| /* |
| * 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); |
| } |
| } |
| } |