Testsuite PoC - Refactor registry and suppliers around annotations, and make lookup of dependency by ref more generic (#32161)

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-08-19 08:10:54 +02:00 committed by GitHub
parent 799201f406
commit 841e2790ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 176 additions and 119 deletions

View file

@ -25,9 +25,6 @@ import java.net.URL;
@KeycloakIntegrationTest @KeycloakIntegrationTest
public class OAuthClientTest { public class OAuthClientTest {
@InjectRealm
ManagedRealm realm; // Need to specify realm as otherwise there's a bug when annotation is not present
@InjectUser(config = UserConfig.class) @InjectUser(config = UserConfig.class)
ManagedUser user; ManagedUser user;

View file

@ -17,7 +17,7 @@ public @interface InjectClient {
LifeCycle lifecycle() default LifeCycle.CLASS; LifeCycle lifecycle() default LifeCycle.CLASS;
String ref() default "default"; String ref() default "";
String realmRef() default "default"; String realmRef() default "";
} }

View file

@ -17,5 +17,5 @@ public @interface InjectRealm {
LifeCycle lifecycle() default LifeCycle.CLASS; LifeCycle lifecycle() default LifeCycle.CLASS;
String ref() default "default"; String ref() default "";
} }

View file

@ -17,7 +17,7 @@ public @interface InjectUser {
LifeCycle lifecycle() default LifeCycle.CLASS; LifeCycle lifecycle() default LifeCycle.CLASS;
String ref() default "default"; String ref() default "";
String realmRef() default "default"; String realmRef() default "";
} }

View file

@ -0,0 +1,11 @@
package org.keycloak.test.framework.injection;
public interface AnnotationFields {
String CONFIG = "config";
String LIFECYCLE = "lifecycle";
String REF = "ref";
String REALM_REF = "realmRef";
}

View file

@ -0,0 +1,28 @@
package org.keycloak.test.framework.injection;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DefaultAnnotationProxy implements InvocationHandler {
private final Class<?> annotationClass;
public <S> DefaultAnnotationProxy(Class<?> annotationClass) {
this.annotationClass = annotationClass;
}
public static <S> S proxy(Class<S> annotationClass) {
return (S) Proxy.newProxyInstance(DefaultAnnotationProxy.class.getClassLoader(), new Class<?>[]{annotationClass}, new DefaultAnnotationProxy(annotationClass));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("annotationType")) {
return annotationClass;
} else {
return annotationClass.getMethod(method.getName()).getDefaultValue();
}
}
}

View file

@ -14,10 +14,8 @@ public class InstanceContext<T, A extends Annotation> {
private final Set<InstanceContext<T, A>> dependencies = new HashSet<>(); private final Set<InstanceContext<T, A>> dependencies = new HashSet<>();
private T value; private T value;
private Class<? extends T> requestedValueType; private Class<? extends T> requestedValueType;
private final Class<?> config;
private LifeCycle lifeCycle; private LifeCycle lifeCycle;
private final String ref; private final String ref;
private final String realmRef;
private final Map<String, Object> notes = new HashMap<>(); private final Map<String, Object> notes = new HashMap<>();
public InstanceContext(Registry registry, Supplier<T, A> supplier, A annotation, Class<? extends T> requestedValueType) { public InstanceContext(Registry registry, Supplier<T, A> supplier, A annotation, Class<? extends T> requestedValueType) {
@ -25,25 +23,16 @@ public class InstanceContext<T, A extends Annotation> {
this.supplier = supplier; this.supplier = supplier;
this.annotation = annotation; this.annotation = annotation;
this.requestedValueType = requestedValueType; this.requestedValueType = requestedValueType;
this.config = (Class<?>) supplier.getAnnotationElementValue(annotation, SupplierHelpers.CONFIG); this.lifeCycle = supplier.getLifeCycle(annotation);
this.lifeCycle = (LifeCycle) supplier.getAnnotationElementValue(annotation, SupplierHelpers.LIFECYCLE); this.ref = StringUtil.convertEmptyToNull(supplier.getRef(annotation));
this.ref = (String) supplier.getAnnotationElementValue(annotation, SupplierHelpers.REF);
this.realmRef = (String) supplier.getAnnotationElementValue(annotation, SupplierHelpers.REALM_REF);
}
public InstanceContext(Registry registry, Supplier<T, A> supplier, Class<? extends T> requestedValueType, String ref, Class<?> config) {
this.registry = registry;
this.supplier = supplier;
this.annotation = null;
this.requestedValueType = requestedValueType;
this.config = config;
this.lifeCycle = supplier.getDefaultLifecycle();
this.ref = ref;
this.realmRef = "";
} }
public <D> D getDependency(Class<D> typeClazz) { public <D> D getDependency(Class<D> typeClazz) {
return registry.getDependency(typeClazz, this); return getDependency(typeClazz, null);
}
public <D> D getDependency(Class<D> typeClazz, String ref) {
return registry.getDependency(typeClazz, ref, this);
} }
public Registry getRegistry() { public Registry getRegistry() {
@ -66,10 +55,6 @@ public class InstanceContext<T, A extends Annotation> {
return requestedValueType; return requestedValueType;
} }
public Class<?> getConfig() {
return config;
}
public LifeCycle getLifeCycle() { public LifeCycle getLifeCycle() {
return lifeCycle; return lifeCycle;
} }
@ -78,10 +63,6 @@ public class InstanceContext<T, A extends Annotation> {
return ref; return ref;
} }
public String getRealmRef() {
return realmRef;
}
public A getAnnotation() { public A getAnnotation() {
return annotation; return annotation;
} }

View file

@ -12,6 +12,7 @@ import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.Set; import java.util.Set;
@ -39,17 +40,18 @@ public class Registry {
this.currentContext = currentContext; this.currentContext = currentContext;
} }
public <T> T getDependency(Class<T> typeClass, InstanceContext dependent) { public <T> T getDependency(Class<T> typeClass, String ref, InstanceContext dependent) {
ref = StringUtil.convertEmptyToNull(ref);
T dependency; T dependency;
dependency = getDeployedDependency(typeClass, dependent); dependency = getDeployedDependency(typeClass, ref, dependent);
if (dependency != null) { if (dependency != null) {
return dependency; return dependency;
} else { } else {
dependency = getRequestedDependency(typeClass, dependent); dependency = getRequestedDependency(typeClass, ref, dependent);
if(dependency != null) { if(dependency != null) {
return dependency; return dependency;
} else { } else {
dependency = getUnConfiguredDependency(typeClass, dependent); dependency = getUnConfiguredDependency(typeClass, ref, dependent);
if(dependency != null) { if(dependency != null) {
return dependency; return dependency;
} }
@ -59,14 +61,8 @@ public class Registry {
throw new RuntimeException("Dependency not found: " + typeClass); throw new RuntimeException("Dependency not found: " + typeClass);
} }
private <T> T getDeployedDependency(Class<T> typeClass, InstanceContext dependent) { private <T> T getDeployedDependency(Class<T> typeClass, String ref, InstanceContext dependent) {
InstanceContext dependency; InstanceContext dependency = getDeployedInstance(typeClass, ref);
if(!dependent.getRealmRef().equals("")) {
dependency = getDeployedInstance(typeClass, dependent.getRealmRef());
} else {
dependency = getDeployedInstance(typeClass);
}
if (dependency != null) { if (dependency != null) {
dependency.registerDependency(dependent); dependency.registerDependency(dependent);
@ -81,16 +77,10 @@ public class Registry {
return null; return null;
} }
private <T> T getRequestedDependency(Class<T> typeClass, InstanceContext dependent) { private <T> T getRequestedDependency(Class<T> typeClass, String ref, InstanceContext dependent) {
InstanceContext dependency; RequestedInstance requestedDependency = getRequestedInstance(typeClass, ref);
RequestedInstance requestedDependency;
if(!dependent.getRealmRef().equals("")) {
requestedDependency = getRequestedInstance(typeClass, dependent.getRealmRef());
} else {
requestedDependency = getRequestedInstance(typeClass);
}
if (requestedDependency != null) { if (requestedDependency != null) {
dependency = new InstanceContext<Object, Annotation>(this, requestedDependency.getSupplier(), requestedDependency.getAnnotation(), requestedDependency.getValueType()); InstanceContext dependency = new InstanceContext<Object, Annotation>(this, requestedDependency.getSupplier(), requestedDependency.getAnnotation(), requestedDependency.getValueType());
dependency.setValue(requestedDependency.getSupplier().getValue(dependency)); dependency.setValue(requestedDependency.getSupplier().getValue(dependency));
dependency.registerDependency(dependent); dependency.registerDependency(dependent);
deployedInstances.add(dependency); deployedInstances.add(dependency);
@ -108,16 +98,13 @@ public class Registry {
return null; return null;
} }
private <T> T getUnConfiguredDependency(Class<T> typeClass, InstanceContext dependent) { private <T> T getUnConfiguredDependency(Class<T> typeClass, String ref, InstanceContext dependent) {
InstanceContext dependency; InstanceContext dependency;
Optional<Supplier<?, ?>> supplied = suppliers.stream().filter(s -> s.getValueType().equals(typeClass)).findFirst(); Optional<Supplier<?, ?>> supplied = suppliers.stream().filter(s -> s.getValueType().equals(typeClass)).findFirst();
if (supplied.isPresent()) { if (supplied.isPresent()) {
Supplier<T, ?> supplier = (Supplier<T, ?>) supplied.get(); Supplier<T, ?> supplier = (Supplier<T, ?>) supplied.get();
if(!dependent.getRealmRef().equals("")) { Annotation defaultAnnotation = DefaultAnnotationProxy.proxy(supplier.getAnnotationClass());
dependency = new InstanceContext(this, supplier, typeClass, dependent.getRealmRef(), DefaultRealmConfig.class); dependency = new InstanceContext(this, supplier, defaultAnnotation, typeClass);
} else {
dependency = new InstanceContext(this, supplier, null, typeClass);
}
dependency.registerDependency(dependent); dependency.registerDependency(dependent);
dependency.setValue(supplier.getValue(dependency)); dependency.setValue(supplier.getValue(dependency));
@ -258,7 +245,7 @@ public class Registry {
Supplier supplier = i.getSupplier(); Supplier supplier = i.getSupplier();
if (supplier.getAnnotationClass().equals(a.annotationType()) if (supplier.getAnnotationClass().equals(a.annotationType())
&& valueType.isAssignableFrom(i.getValue().getClass()) && valueType.isAssignableFrom(i.getValue().getClass())
&& supplier.getAnnotationElementValue(a, SupplierHelpers.REF).equals(i.getRef()) ) { && Objects.equals(supplier.getRef(a), i.getRef()) ) {
return i; return i;
} }
} }
@ -284,7 +271,7 @@ public class Registry {
String requestedRef = requestedInstance.getRef(); String requestedRef = requestedInstance.getRef();
Class requestedValueType = requestedInstance.getValueType(); Class requestedValueType = requestedInstance.getValueType();
for (InstanceContext<?, ?> i : deployedInstances) { for (InstanceContext<?, ?> i : deployedInstances) {
if(!i.getRef().equals(requestedRef)) { if(!Objects.equals(i.getRef(), requestedRef)) {
continue; continue;
} }
@ -345,20 +332,16 @@ public class Registry {
} }
} }
private InstanceContext getDeployedInstance(Class typeClass) { private InstanceContext getDeployedInstance(Class typeClass, String ref) {
return deployedInstances.stream().filter(i -> i.getSupplier().getValueType().equals(typeClass)).findFirst().orElse(null); return deployedInstances.stream()
.filter(i -> i.getSupplier().getValueType().equals(typeClass) && Objects.equals(i.getRef(), ref))
.findFirst().orElse(null);
} }
private InstanceContext getDeployedInstance(Class typeClass, String realmRef) { private RequestedInstance getRequestedInstance(Class typeClass, String ref) {
return deployedInstances.stream().filter(i -> i.getSupplier().getValueType().equals(typeClass)).filter(j -> j.getRef().equals(realmRef)).findFirst().orElse(null); return requestedInstances.stream()
} .filter(i -> i.getSupplier().getValueType().equals(typeClass) && Objects.equals(i.getRef(), ref))
.findFirst().orElse(null);
private RequestedInstance getRequestedInstance(Class typeClass) {
return requestedInstances.stream().filter(i -> i.getSupplier().getValueType().equals(typeClass)).findFirst().orElse(null);
}
private RequestedInstance getRequestedInstance(Class typeClass, String realmRef) {
return requestedInstances.stream().filter(i -> i.getSupplier().getValueType().equals(typeClass)).filter(j -> j.getRef().equals(realmRef)).findFirst().orElse(null);
} }
} }

View file

@ -14,8 +14,8 @@ public class RequestedInstance<T, A extends Annotation> {
this.supplier = supplier; this.supplier = supplier;
this.annotation = annotation; this.annotation = annotation;
this.valueType = valueType; this.valueType = valueType;
this.lifeCycle = (LifeCycle) supplier.getAnnotationElementValue(annotation, SupplierHelpers.LIFECYCLE); this.lifeCycle = supplier.getLifeCycle(annotation);
this.ref = (String) supplier.getAnnotationElementValue(annotation, SupplierHelpers.REF); this.ref = StringUtil.convertEmptyToNull(supplier.getRef(annotation));
} }
public Supplier<T, A> getSupplier() { public Supplier<T, A> getSupplier() {

View file

@ -0,0 +1,9 @@
package org.keycloak.test.framework.injection;
public class StringUtil {
public static String convertEmptyToNull(String s) {
return s != null && s.isEmpty() ? null : s;
}
}

View file

@ -1,9 +1,6 @@
package org.keycloak.test.framework.injection; package org.keycloak.test.framework.injection;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
public interface Supplier<T, S extends Annotation> { public interface Supplier<T, S extends Annotation> {
@ -13,32 +10,12 @@ public interface Supplier<T, S extends Annotation> {
T getValue(InstanceContext<T, S> instanceContext); T getValue(InstanceContext<T, S> instanceContext);
default Object getAnnotationElementValue(S annotation, String annotationAttribute) { default String getRef(S annotation) {
if (annotation != null) { return StringUtil.convertEmptyToNull(SupplierHelpers.getAnnotationField(annotation, AnnotationFields.REF));
Optional<Method> annotationMethod = Arrays.stream(annotation.annotationType().getMethods()).filter(m -> m.getName().equals(annotationAttribute)).findFirst();
if (annotationMethod.isPresent()) {
try {
return annotationMethod.get().invoke(annotation);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
return getAnnotationElementValue(annotationAttribute);
} }
default Object getAnnotationElementValue(String annotationAttribute) { default LifeCycle getLifeCycle(S annotation) {
switch (annotationAttribute) { return SupplierHelpers.getAnnotationField(annotation, AnnotationFields.LIFECYCLE, getDefaultLifecycle());
case SupplierHelpers.LIFECYCLE -> {
return this.getDefaultLifecycle();
}
case SupplierHelpers.REF, SupplierHelpers.REALM_REF -> {
return "";
}
default -> {
return null;
}
}
} }
default LifeCycle getDefaultLifecycle() { default LifeCycle getDefaultLifecycle() {

View file

@ -1,14 +1,11 @@
package org.keycloak.test.framework.injection; package org.keycloak.test.framework.injection;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class SupplierHelpers { public class SupplierHelpers {
public static final String CONFIG = "config";
public static final String LIFECYCLE = "lifecycle";
public static final String REF = "ref";
public static final String REALM_REF = "realmRef";
public static <T> T getInstance(Class<T> clazz) { public static <T> T getInstance(Class<T> clazz) {
try { try {
Constructor<T> declaredConstructor = clazz.getDeclaredConstructor(); Constructor<T> declaredConstructor = clazz.getDeclaredConstructor();
@ -18,4 +15,29 @@ public class SupplierHelpers {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public static <T> T getAnnotationField(Annotation annotation, String name, T defaultValue) {
T value = getAnnotationField(annotation, name);
return value != null ? value : defaultValue;
}
public static <T> T getAnnotationField(Annotation annotation, String name) {
if (annotation != null) {
for (Method m : annotation.annotationType().getMethods()) {
if (m.getName().equals(name)) {
try {
return (T) m.invoke(annotation);
} catch (Exception e){
throw new RuntimeException(e);
}
}
}
}
return null;
}
public static String createName(InstanceContext<?, ?> instanceContext) {
return instanceContext.getRef() != null ? instanceContext.getRef() : "default";
}
} }

View file

@ -23,13 +23,13 @@ public class ClientSupplier implements Supplier<ManagedClient, InjectClient> {
@Override @Override
public ManagedClient getValue(InstanceContext<ManagedClient, InjectClient> instanceContext) { public ManagedClient getValue(InstanceContext<ManagedClient, InjectClient> instanceContext) {
ManagedRealm realm = instanceContext.getDependency(ManagedRealm.class); ManagedRealm realm = instanceContext.getDependency(ManagedRealm.class, instanceContext.getAnnotation().realmRef());
ClientConfig config = SupplierHelpers.getInstance(instanceContext.getAnnotation().config()); ClientConfig config = SupplierHelpers.getInstance(instanceContext.getAnnotation().config());
ClientRepresentation clientRepresentation = config.getRepresentation(); ClientRepresentation clientRepresentation = config.getRepresentation();
if (clientRepresentation.getClientId() == null) { if (clientRepresentation.getClientId() == null) {
String clientId = instanceContext.getRef(); String clientId = SupplierHelpers.createName(instanceContext);
clientRepresentation.setClientId(clientId); clientRepresentation.setClientId(clientId);
} }

View file

@ -29,11 +29,11 @@ public class RealmSupplier implements Supplier<ManagedRealm, InjectRealm> {
KeycloakTestServer server = instanceContext.getDependency(KeycloakTestServer.class); KeycloakTestServer server = instanceContext.getDependency(KeycloakTestServer.class);
Keycloak adminClient = instanceContext.getDependency(Keycloak.class); Keycloak adminClient = instanceContext.getDependency(Keycloak.class);
RealmConfig config = (RealmConfig) SupplierHelpers.getInstance(instanceContext.getConfig()); RealmConfig config = SupplierHelpers.getInstance(instanceContext.getAnnotation().config());
RealmRepresentation realmRepresentation = config.getRepresentation(); RealmRepresentation realmRepresentation = config.getRepresentation();
if (realmRepresentation.getRealm() == null) { if (realmRepresentation.getRealm() == null) {
String realmName = instanceContext.getRef(); String realmName = SupplierHelpers.createName(instanceContext);
realmRepresentation.setRealm(realmName); realmRepresentation.setRealm(realmName);
} }

View file

@ -25,13 +25,13 @@ public class UserSupplier implements Supplier<ManagedUser, InjectUser> {
@Override @Override
public ManagedUser getValue(InstanceContext<ManagedUser, InjectUser> instanceContext) { public ManagedUser getValue(InstanceContext<ManagedUser, InjectUser> instanceContext) {
ManagedRealm realm = instanceContext.getDependency(ManagedRealm.class); ManagedRealm realm = instanceContext.getDependency(ManagedRealm.class, instanceContext.getAnnotation().realmRef());
UserConfig config = SupplierHelpers.getInstance(instanceContext.getAnnotation().config()); UserConfig config = SupplierHelpers.getInstance(instanceContext.getAnnotation().config());
UserRepresentation userRepresentation = config.getRepresentation(); UserRepresentation userRepresentation = config.getRepresentation();
if (userRepresentation.getUsername() == null) { if (userRepresentation.getUsername() == null) {
String username = instanceContext.getRef(); String username = SupplierHelpers.createName(instanceContext);
userRepresentation.setUsername(username); userRepresentation.setUsername(username);
} }

View file

@ -0,0 +1,49 @@
package org.keycloak.test.framework.injection;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.test.framework.annotations.InjectRealm;
import org.keycloak.test.framework.realm.DefaultRealmConfig;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.LinkedList;
import java.util.List;
public class DefaultAnnotationProxyTest {
@Test
public void testGetField() {
MockAnnotation proxy = DefaultAnnotationProxy.proxy(MockAnnotation.class);
Assertions.assertEquals(LifeCycle.CLASS, proxy.lifecycle());
Assertions.assertEquals(LinkedList.class, proxy.config());
Assertions.assertEquals("", proxy.ref());
Assertions.assertEquals("else", proxy.something());
}
@Test
public void testAnnotationReflection() {
MockAnnotation proxy = DefaultAnnotationProxy.proxy(MockAnnotation.class);
Assertions.assertEquals(LifeCycle.CLASS, SupplierHelpers.getAnnotationField(proxy, "lifecycle"));
Assertions.assertEquals(LinkedList.class, SupplierHelpers.getAnnotationField(proxy, "config"));
Assertions.assertEquals("", SupplierHelpers.getAnnotationField(proxy, "ref"));
Assertions.assertEquals("else", SupplierHelpers.getAnnotationField(proxy, "something"));
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MockAnnotation {
Class<? extends List> config() default LinkedList.class;
LifeCycle lifecycle() default LifeCycle.CLASS;
String ref() default "";
String something() default "else";
}
}

View file

@ -302,10 +302,10 @@ public class RegistryTest {
@MockParentAnnotation(ref = "123") @MockParentAnnotation(ref = "123")
MockParentValue parent123; MockParentValue parent123;
@MockChildAnnotation(ref = "123", realmRef = "123") @MockChildAnnotation(ref = "123", parentRef = "123")
MockChildValue child123; MockChildValue child123;
@MockChildAnnotation(ref = "ABC", realmRef = "ABC") @MockChildAnnotation(ref = "ABC", parentRef = "ABC")
MockChildValue childABC; MockChildValue childABC;
} }
} }

View file

@ -10,5 +10,5 @@ import java.lang.annotation.Target;
public @interface MockChildAnnotation { public @interface MockChildAnnotation {
String ref() default ""; String ref() default "";
String realmRef() default ""; String parentRef() default "";
} }

View file

@ -25,7 +25,7 @@ public class MockChildSupplier implements Supplier<MockChildValue, MockChildAnno
@Override @Override
public MockChildValue getValue(InstanceContext<MockChildValue, MockChildAnnotation> instanceContext) { public MockChildValue getValue(InstanceContext<MockChildValue, MockChildAnnotation> instanceContext) {
MockParentValue mockParentValue = instanceContext.getDependency(MockParentValue.class); MockParentValue mockParentValue = instanceContext.getDependency(MockParentValue.class, instanceContext.getAnnotation().parentRef());
return new MockChildValue(mockParentValue); return new MockChildValue(mockParentValue);
} }