Skip to content

Commit 99b5ecd

Browse files
metacosmcsviri
andauthored
feat: cover more cases when trying to determine generic types (#1276)
* feat: cover more cases when trying to determine generic types * fix: if first attempt failed, recursively check parent hierarchy * remove space * format * fix: throw OperatorException * chore: clean-up Co-authored-by: csviri <[email protected]>
1 parent af239f4 commit 99b5ecd

File tree

9 files changed

+110
-18
lines changed

9 files changed

+110
-18
lines changed

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class AnnotationControllerConfiguration<R extends HasMetadata>
3434
protected final Reconciler<R> reconciler;
3535
private final ControllerConfiguration annotation;
3636
private List<DependentResourceSpec> specs;
37+
private Class<R> resourceClass;
3738

3839
public AnnotationControllerConfiguration(Reconciler<R> reconciler) {
3940
this.reconciler = reconciler;
@@ -81,7 +82,12 @@ public Set<String> getNamespaces() {
8182
@Override
8283
@SuppressWarnings("unchecked")
8384
public Class<R> getResourceClass() {
84-
return (Class<R>) Utils.getFirstTypeArgumentFromInterface(reconciler.getClass());
85+
if (resourceClass == null) {
86+
resourceClass =
87+
(Class<R>) Utils.getFirstTypeArgumentFromSuperClassOrInterface(reconciler.getClass(),
88+
Reconciler.class);
89+
}
90+
return resourceClass;
8591
}
8692

8793
@Override

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,15 @@ default Optional<Duration> reconciliationMaxInterval() {
5555
return Optional.of(Duration.ofHours(10L));
5656
}
5757

58+
@SuppressWarnings("unused")
5859
default ConfigurationService getConfigurationService() {
5960
return ConfigurationServiceProvider.instance();
6061
}
62+
63+
@SuppressWarnings("unchecked")
64+
@Override
65+
default Class<R> getResourceClass() {
66+
return (Class<R>) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(),
67+
ControllerConfiguration.class);
68+
}
6169
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ResourceConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ default String getLabelSelector() {
3131

3232
@SuppressWarnings("unchecked")
3333
default Class<R> getResourceClass() {
34-
return (Class<R>) Utils.getFirstTypeArgumentFromInterface(getClass());
34+
return (Class<R>) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(),
35+
ResourceConfiguration.class);
3536
}
3637

3738
default Set<String> getNamespaces() {

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import java.lang.reflect.Type;
66
import java.text.SimpleDateFormat;
77
import java.time.Instant;
8+
import java.util.Arrays;
89
import java.util.Date;
910
import java.util.Properties;
10-
import java.util.function.Function;
1111

1212
import org.slf4j.Logger;
1313
import org.slf4j.LoggerFactory;
1414

15+
import io.javaoperatorsdk.operator.OperatorException;
16+
1517
public class Utils {
1618

1719
private static final Logger log = LoggerFactory.getLogger(Utils.class);
@@ -59,6 +61,8 @@ public static Version loadFromProperties() {
5961
builtTime);
6062
}
6163

64+
@SuppressWarnings("unused")
65+
// this is used in the Quarkus extension
6266
public static boolean isValidateCustomResourcesEnvVarSet() {
6367
return System.getProperty(CHECK_CRD_ENV_KEY) != null;
6468
}
@@ -89,16 +93,55 @@ static boolean getBooleanFromSystemPropsOrDefault(String propertyName, boolean d
8993
}
9094

9195
public static Class<?> getFirstTypeArgumentFromExtendedClass(Class<?> clazz) {
92-
Type type = clazz.getGenericSuperclass();
93-
return (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
96+
try {
97+
Type type = clazz.getGenericSuperclass();
98+
return (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
99+
} catch (Exception e) {
100+
throw new RuntimeException("Couldn't retrieve generic parameter type from "
101+
+ clazz.getSimpleName()
102+
+ " because it doesn't extend a class that is parameterized with the type we want to retrieve",
103+
e);
104+
}
94105
}
95106

96-
public static Class<?> getFirstTypeArgumentFromInterface(Class<?> clazz) {
97-
ParameterizedType type = (ParameterizedType) clazz.getGenericInterfaces()[0];
98-
return (Class<?>) type.getActualTypeArguments()[0];
107+
public static Class<?> getFirstTypeArgumentFromInterface(Class<?> clazz,
108+
Class<?> expectedImplementedInterface) {
109+
return Arrays.stream(clazz.getGenericInterfaces())
110+
.filter(type -> type.getTypeName().startsWith(expectedImplementedInterface.getName())
111+
&& type instanceof ParameterizedType)
112+
.map(ParameterizedType.class::cast)
113+
.findFirst()
114+
.map(t -> (Class<?>) t.getActualTypeArguments()[0])
115+
.orElseThrow(() -> new RuntimeException(
116+
"Couldn't retrieve generic parameter type from " + clazz.getSimpleName()
117+
+ " because it doesn't implement "
118+
+ expectedImplementedInterface.getSimpleName()
119+
+ " directly"));
99120
}
100121

101-
public static <C, T> T valueOrDefault(C annotation, Function<C, T> mapper, T defaultValue) {
102-
return annotation == null ? defaultValue : mapper.apply(annotation);
122+
public static Class<?> getFirstTypeArgumentFromSuperClassOrInterface(Class<?> clazz,
123+
Class<?> expectedImplementedInterface) {
124+
// first check super class if it exists
125+
try {
126+
final Class<?> superclass = clazz.getSuperclass();
127+
if (!superclass.equals(Object.class)) {
128+
try {
129+
return getFirstTypeArgumentFromExtendedClass(clazz);
130+
} catch (Exception e) {
131+
// try interfaces
132+
try {
133+
return getFirstTypeArgumentFromInterface(clazz, expectedImplementedInterface);
134+
} catch (Exception ex) {
135+
// try on the parent
136+
return getFirstTypeArgumentFromSuperClassOrInterface(superclass,
137+
expectedImplementedInterface);
138+
}
139+
}
140+
}
141+
return getFirstTypeArgumentFromInterface(clazz, expectedImplementedInterface);
142+
} catch (Exception e) {
143+
throw new OperatorException(
144+
"Couldn't retrieve generic parameter type from " + clazz.getSimpleName(), e);
145+
}
103146
}
104147
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.fabric8.kubernetes.api.model.HasMetadata;
77
import io.javaoperatorsdk.operator.api.config.DefaultResourceConfiguration;
88
import io.javaoperatorsdk.operator.api.config.ResourceConfiguration;
9+
import io.javaoperatorsdk.operator.api.config.Utils;
910
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
1011
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
1112
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
@@ -160,4 +161,10 @@ static <R extends HasMetadata> InformerConfigurationBuilder<R> from(
160161
.withNamespacesInheritedFromController(eventSourceContext);
161162
}
162163

164+
@SuppressWarnings("unchecked")
165+
@Override
166+
default Class<R> getResourceClass() {
167+
return (Class<R>) Utils.getFirstTypeArgumentFromSuperClassOrInterface(getClass(),
168+
InformerConfiguration.class);
169+
}
163170
}

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@
44

55
import io.javaoperatorsdk.operator.sample.simple.TestCustomResource;
66

7-
import static org.junit.jupiter.api.Assertions.*;
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
import static org.junit.jupiter.api.Assertions.assertThrows;
89

910
class ControllerConfigurationTest {
1011

1112
@Test
1213
void getCustomResourceClass() {
14+
final ControllerConfiguration<TestCustomResource> lambdasCannotBeUsedToExtractGenericParam =
15+
() -> null;
16+
assertThrows(RuntimeException.class,
17+
lambdasCannotBeUsedToExtractGenericParam::getResourceClass);
18+
1319
final ControllerConfiguration<TestCustomResource> conf = new ControllerConfiguration<>() {
1420
@Override
1521
public String getAssociatedReconcilerClassName() {

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44

55
import org.junit.jupiter.api.Test;
66

7+
import io.fabric8.kubernetes.api.model.ConfigMap;
8+
import io.fabric8.kubernetes.api.model.HasMetadata;
79
import io.fabric8.kubernetes.api.model.apps.Deployment;
810
import io.javaoperatorsdk.operator.api.reconciler.Context;
11+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
12+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
913
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
1014
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
1115
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
@@ -84,10 +88,28 @@ void getsFirstTypeArgumentFromExtendedClass() {
8488

8589
@Test
8690
void getsFirstTypeArgumentFromInterface() {
87-
assertThat(Utils.getFirstTypeArgumentFromInterface(TestDependentResource.class))
91+
assertThat(Utils.getFirstTypeArgumentFromInterface(TestDependentResource.class,
92+
DependentResource.class))
8893
.isEqualTo(Deployment.class);
8994
}
9095

96+
@Test
97+
void getsFirstTypeArgumentFromInterfaceFromParent() {
98+
assertThat(Utils.getFirstTypeArgumentFromSuperClassOrInterface(ConcreteReconciler.class,
99+
Reconciler.class)).isEqualTo(ConfigMap.class);
100+
}
101+
102+
public abstract static class AbstractReconciler<P extends HasMetadata> implements Reconciler<P> {
103+
}
104+
105+
public static class ConcreteReconciler extends AbstractReconciler<ConfigMap> {
106+
@Override
107+
public UpdateControl<ConfigMap> reconcile(ConfigMap resource, Context<ConfigMap> context)
108+
throws Exception {
109+
return null;
110+
}
111+
}
112+
91113
public static class TestDependentResource
92114
implements DependentResource<Deployment, TestCustomResource> {
93115

operator-framework/src/main/java/io/javaoperatorsdk/operator/config/runtime/RuntimeControllerMetadata.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ public class RuntimeControllerMetadata {
1717
RECONCILERS_RESOURCE_PATH, Reconciler.class, HasMetadata.class);
1818
}
1919

20-
static <R extends HasMetadata> Class<R> getResourceClass(
21-
Reconciler<R> reconciler) {
20+
@SuppressWarnings("unchecked")
21+
static <R extends HasMetadata> Class<R> getResourceClass(Reconciler<R> reconciler) {
2222
final Class<? extends HasMetadata> resourceClass =
2323
controllerToCustomResourceMappings.get(reconciler.getClass());
2424
if (resourceClass == null) {

operator-framework/src/test/java/io/javaoperatorsdk/operator/config/runtime/AnnotationControllerConfigurationTest.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,15 @@ void getDependentResources() {
8181

8282
@Test
8383
void missingAnnotationThrowsException() {
84-
Assertions.assertThrows(OperatorException.class, () -> {
85-
new AnnotationControllerConfiguration<>(new MissingAnnotationReconciler());
86-
});
84+
Assertions.assertThrows(OperatorException.class,
85+
() -> new AnnotationControllerConfiguration<>(new MissingAnnotationReconciler()));
8786
}
8887

8988
@SuppressWarnings("rawtypes")
9089
private DependentResourceSpec findByName(
9190
List<DependentResourceSpec> dependentResourceSpecList, String name) {
9291
return dependentResourceSpecList.stream().filter(d -> d.getName().equals(name)).findFirst()
93-
.get();
92+
.orElseThrow();
9493
}
9594

9695
@SuppressWarnings("rawtypes")

0 commit comments

Comments
 (0)