/* * 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 com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import com.google.common.base.Joiner; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.LinkedList; import java.util.List; import javax.annotation.Nullable; import org.apache.beam.sdk.values.TypeDescriptor; /** * 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 LinkedList<>(); /** * Arguments to factory method {@link Method#invoke(Object, Object...)}. */ private final List<Object> arguments = new LinkedList<>(); /** * 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); } } }