Skip to content

Instantly share code, notes, and snippets.

@hborders
Created November 27, 2021 00:02
Show Gist options
  • Save hborders/f99855ae3f9efa488837e8e5f334b3ac to your computer and use it in GitHub Desktop.
Save hborders/f99855ae3f9efa488837e8e5f334b3ac to your computer and use it in GitHub Desktop.
Generates named parameters for a constructor using a chain of interfaces
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public final class NamedParams {
static final Method OBJECT_EQUALS_METHOD;
static final Method OBJECT_HASH_CODE_METHOD;
static final Method OBJECT_TO_STRING_METHOD;
static {
try {
OBJECT_EQUALS_METHOD = Object.class.getMethod("equals", Object.class);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(
"Missing Object.equals(Object) method",
e
);
}
try {
OBJECT_HASH_CODE_METHOD = Object.class.getMethod("hashCode");
} catch (NoSuchMethodException e) {
throw new IllegalStateException(
"Missing Object.hashCode() method",
e
);
}
try {
OBJECT_TO_STRING_METHOD = Object.class.getMethod("toString");
} catch (NoSuchMethodException e) {
throw new IllegalStateException(
"Missing Object.toString() method",
e
);
}
}
@Nonnull
public static <
ConstructedType,
NamedParamStartingType
> NamedParamStartingType namedParams(
@Nonnull Class<ConstructedType> constructedClass,
@Nonnull Class<NamedParamStartingType> namedParamStartingClass
) {
@Nonnull final ArrayList<
Class<?>
> namedParamClasses = new ArrayList<>();
@Nonnull final ArrayList<Method> namedParamMethods = new ArrayList<>();
@Nonnull final ArrayList<
Class<?>
> constructorParameterClasses = new ArrayList<>();
int size = 0;
@Nonnull Class<?> namedParamClass = namedParamStartingClass;
@Nullable Constructor<ConstructedType> constructor = null;
while (true) {
if (size > 255) {
throw new IllegalArgumentException(
"The Java Language Specification says methods are limited to " +
"255 parameters: https://stackoverflow.com/a/30581726/9636"
);
}
namedParamClasses.add(namedParamClass);
@Nonnull final Method namedParamMethod = findOnlyMethod(namedParamClass);
namedParamMethods.add(namedParamMethod);
@Nonnull final Class<?> parameterClass = findFirstParameterClass(namedParamMethod);
constructorParameterClasses.add(parameterClass);
size++;
@Nonnull final Class<?> returnTypeClass = namedParamMethod.getReturnType();
if (constructedClass.equals(returnTypeClass)) {
constructor = findConstructor(
constructedClass,
constructorParameterClasses.toArray(new Class<?>[size])
);
break;
} else if (!returnTypeClass.isInterface()) {
throw new IllegalArgumentException(
"NamedParam methods must return another NamedParam interface or a" +
" constructed type, but " + namedParamMethod + " returns " +
returnTypeClass
);
} else {
namedParamClass = returnTypeClass;
}
}
@Nonnull final Object[] params = new Object[size];
@Nonnull final Object proxy = namedParamsIteration(
constructedClass,
constructor,
namedParamClasses,
namedParamMethods,
params,
0,
size - 1
);
return Objects.requireNonNull(
namedParamStartingClass.cast(
proxy
)
);
}
@Nonnull
private static <
ConstructedType
> Object namedParamsIteration(
@Nonnull Class<ConstructedType> constructedClass,
@Nonnull Constructor<ConstructedType> constructor,
@Nonnull List<
Class<?>
> namedParamClasses,
@Nonnull List<Method> namedParamMethods,
@Nonnull Object[] params,
int index,
int lastNamedParameterIndex
) {
@Nonnull final Class<?> namedParamClass = Objects.requireNonNull(
namedParamClasses.get(index)
);
@Nonnull final Method namedParamMethod = Objects.requireNonNull(
namedParamMethods.get(index)
);
return proxiedNamedParam(
namedParamClass,
namedParamMethod,
new NamedParamMethodInvoker() {
@Nonnull
@Override
public Object invoke(
@Nonnull Method method,
@Nonnull Object param
) throws Throwable {
if (index < lastNamedParameterIndex) {
params[index] = param;
return namedParamsIteration(
constructedClass,
constructor,
namedParamClasses,
namedParamMethods,
params,
index + 1,
lastNamedParameterIndex
);
} else {
params[index] = param;
return constructor.newInstance(params);
}
}
}
);
}
@Nonnull
private static Method findOnlyMethod(@Nonnull Class<?> clazz) {
@Nonnull final Method[] declaredMethods = Objects.requireNonNull(
clazz.getDeclaredMethods()
);
if (declaredMethods.length == 1) {
@Nonnull final Method declaredMethod = Objects.requireNonNull(
declaredMethods[0]
);
if (
!Void.TYPE.equals(declaredMethod.getReturnType()) &&
declaredMethod.getParameterCount() == 1
) {
return declaredMethod;
} else {
// fallthrough
}
} else {
// fallthrough
}
throw new IllegalStateException(
"Expected a single-parameter method returning non-void accepting 1 parameter, " +
"but only found " + Arrays.deepToString(declaredMethods)
);
}
@Nonnull
private static Class<?> findFirstParameterClass(
@Nonnull Method method
) {
@Nonnull final Class<?>[] parameterTypes = Objects.requireNonNull(
method.getParameterTypes()
);
if (parameterTypes.length == 1) {
return Objects.requireNonNull(
parameterTypes[0]
);
} else {
throw new IllegalStateException(
"Expected method with 1 parameter, but " +
method +
" has: " +
Arrays.deepToString(parameterTypes)
);
}
}
@Nonnull
private static <ConstructedType> Constructor<ConstructedType> findConstructor(
Class<ConstructedType> constructedClass,
Class<?>... parameterClasses
) {
try {
return Objects.requireNonNull(
constructedClass.getDeclaredConstructor(
parameterClasses
)
);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(
"Couldn't find a constructor on " + constructedClass +
" with parameters: " + Arrays.deepToString(parameterClasses)
);
}
}
@Nonnull
private static <NamedParamType> Object proxiedNamedParam(
@Nonnull Class<NamedParamType> namedParamClass,
@Nonnull Method namedParamMethod,
@Nonnull NamedParamMethodInvoker namedParamMethodInvoker
) {
return Objects.requireNonNull(
Proxy.newProxyInstance(
namedParamClass.getClassLoader(),
new Class<?>[]{
namedParamClass
},
new NamedParameterInvocationHandler(
namedParamClass,
namedParamMethod,
namedParamMethodInvoker
)
)
);
}
static final class NamedParameterInvocationHandler implements InvocationHandler {
@Nonnull
private final Class<?> namedParamClass;
@Nonnull
private final Method namedParameterMethod;
@Nonnull
private final NamedParamMethodInvoker namedParamMethodInvoker;
NamedParameterInvocationHandler(
@Nonnull Class<?> namedParamClass,
@Nonnull Method namedParameterMethod,
@Nonnull NamedParamMethodInvoker namedParamMethodInvoker
) {
this.namedParamClass = namedParamClass;
this.namedParameterMethod = namedParameterMethod;
this.namedParamMethodInvoker = namedParamMethodInvoker;
}
// InvocationHandler
@Nonnull
@Override
public Object invoke(
@Nonnull Object proxy,
@Nonnull Method method,
@Nullable Object[] args
) throws Throwable {
// if-check is ordered by most-to-least-likelihood of method call
if (method.equals(namedParameterMethod)) {
return namedParamMethodInvoker.invoke(
method,
Objects.requireNonNull(
Objects.requireNonNull(
args
)[0]
)
);
} else if (method.equals(OBJECT_TO_STRING_METHOD)) {
return namedParamClass.getName() + "@" + System.identityHashCode(proxy);
} else if (method.equals(OBJECT_EQUALS_METHOD)) {
return proxy == Objects.requireNonNull(args)[0];
} else if (method.equals(OBJECT_HASH_CODE_METHOD)) {
return System.identityHashCode(proxy);
} else {
throw new UnsupportedOperationException(method.toString());
}
}
}
interface NamedParamMethodInvoker {
@Nonnull
Object invoke(
@Nonnull Method method,
@Nonnull Object param
) throws Throwable;
}
private NamedParams() {
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment