Created
November 27, 2021 00:02
-
-
Save hborders/f99855ae3f9efa488837e8e5f334b3ac to your computer and use it in GitHub Desktop.
Generates named parameters for a constructor using a chain of interfaces
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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