Skip to content

Commit 41f1011

Browse files
Resolve Descriptor generic type to derive natural names in DescriptorConfigurator (#2834)
1 parent b6053ae commit 41f1011

2 files changed

Lines changed: 167 additions & 5 deletions

File tree

plugin/src/main/java/io/jenkins/plugins/casc/impl/configurators/DescriptorConfigurator.java

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import io.jenkins.plugins.casc.ConfigurationContext;
99
import io.jenkins.plugins.casc.RootElementConfigurator;
1010
import io.jenkins.plugins.casc.model.Mapping;
11+
import java.lang.reflect.ParameterizedType;
12+
import java.lang.reflect.Type;
1113
import java.util.Arrays;
1214
import java.util.List;
1315
import java.util.Optional;
@@ -64,17 +66,63 @@ private List<String> resolvePossibleNames(Descriptor descriptor) {
6466
return Optional.ofNullable(descriptor.getClass().getAnnotation(Symbol.class))
6567
.map(s -> Arrays.asList(s.value()))
6668
.orElseGet(() -> {
67-
/* TODO: extract Descriptor parameter type such that DescriptorImpl extends Descriptor<XX> returns XX.
68-
* Then, if `baseClass == fooXX` we get natural name `foo`.
69-
*/
70-
return singletonList(fromPascalCaseToCamelCase(
71-
descriptor.getKlass().toJavaClass().getSimpleName()));
69+
String fallbackName = fromPascalCaseToCamelCase(
70+
unwrapAnonymous(descriptor.getKlass().toJavaClass()).getSimpleName());
71+
72+
Class<?> typeParam = extractDescriptorTypeParameter(descriptor.getClass());
73+
String derivedName = null;
74+
75+
if (typeParam != null) {
76+
Class<?> targetClass = unwrapAnonymous(typeParam);
77+
derivedName = fromPascalCaseToCamelCase(targetClass.getSimpleName());
78+
}
79+
80+
if (derivedName != null && !derivedName.equals(fallbackName)) {
81+
return Arrays.asList(fallbackName, derivedName);
82+
}
83+
84+
return singletonList(fallbackName);
7285
});
7386
}
7487

88+
private Class<?> extractDescriptorTypeParameter(Class<?> clazz) {
89+
while (clazz != null && clazz != Object.class) {
90+
Type genericSuperclass = clazz.getGenericSuperclass();
91+
92+
if (genericSuperclass instanceof ParameterizedType pt
93+
&& pt.getRawType() instanceof Class<?> rawClass
94+
&& Descriptor.class.isAssignableFrom(rawClass)) {
95+
96+
Type[] args = pt.getActualTypeArguments();
97+
98+
if (args.length > 0) {
99+
Type typeArg = args[0];
100+
101+
if (typeArg instanceof Class<?> clazzArg) {
102+
return clazzArg;
103+
}
104+
105+
if (typeArg instanceof ParameterizedType nestedPt
106+
&& nestedPt.getRawType() instanceof Class<?> nestedClass) {
107+
return nestedClass;
108+
}
109+
}
110+
}
111+
clazz = clazz.getSuperclass();
112+
}
113+
return null;
114+
}
115+
75116
private static String fromPascalCaseToCamelCase(String s) {
76117
StringBuilder sb = new StringBuilder(s);
77118
sb.setCharAt(0, Character.toLowerCase(s.charAt(0)));
78119
return sb.toString();
79120
}
121+
122+
private Class<?> unwrapAnonymous(Class<?> clazz) {
123+
while (clazz.isAnonymousClass()) {
124+
clazz = clazz.getSuperclass();
125+
}
126+
return clazz;
127+
}
80128
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package io.jenkins.plugins.casc.impl.configurators;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import hudson.model.Describable;
6+
import hudson.model.Descriptor;
7+
import java.util.Arrays;
8+
import java.util.Collections;
9+
import org.jenkinsci.Symbol;
10+
import org.junit.Test;
11+
12+
public class DescriptorConfiguratorTest {
13+
14+
public static class DummyTask implements Describable<DummyTask> {
15+
@Override
16+
public Descriptor<DummyTask> getDescriptor() {
17+
throw new UnsupportedOperationException("Not required for name extraction tests");
18+
}
19+
}
20+
21+
public static class ParameterizedTask<T> implements Describable<ParameterizedTask<T>> {
22+
@Override
23+
public Descriptor<ParameterizedTask<T>> getDescriptor() {
24+
throw new UnsupportedOperationException();
25+
}
26+
}
27+
28+
public static class DummyTaskDescriptor extends Descriptor<DummyTask> {
29+
public DummyTaskDescriptor() {
30+
super(DummyTask.class);
31+
}
32+
}
33+
34+
@Symbol({"primary", "alias"})
35+
public static class MultiSymbolDescriptor extends Descriptor<DummyTask> {
36+
public MultiSymbolDescriptor() {
37+
super(DummyTask.class);
38+
}
39+
}
40+
41+
public abstract static class SimulatedBuildStepDescriptor<T extends Describable<T>> extends Descriptor<T> {
42+
protected SimulatedBuildStepDescriptor(Class<? extends T> clazz) {
43+
super(clazz);
44+
}
45+
}
46+
47+
public static class DeepTaskDescriptor extends SimulatedBuildStepDescriptor<DummyTask> {
48+
public DeepTaskDescriptor() {
49+
super(DummyTask.class);
50+
}
51+
}
52+
53+
@SuppressWarnings({"rawtypes", "unchecked"})
54+
public static class ParameterizedDescriptor extends Descriptor<ParameterizedTask<String>> {
55+
public ParameterizedDescriptor() {
56+
super((Class) ParameterizedTask.class);
57+
}
58+
}
59+
60+
@SuppressWarnings("rawtypes")
61+
public static class RawDescriptor extends Descriptor {
62+
@SuppressWarnings("unchecked")
63+
public RawDescriptor(Class clazz) {
64+
super(clazz);
65+
}
66+
}
67+
68+
@Test
69+
public void testMultipleSymbols() {
70+
DescriptorConfigurator configurator = new DescriptorConfigurator(new MultiSymbolDescriptor());
71+
72+
assertEquals("Should use the first symbol as primary name", "primary", configurator.getName());
73+
assertEquals(
74+
"Should return all symbols as aliases", Arrays.asList("primary", "alias"), configurator.getNames());
75+
}
76+
77+
@Test
78+
public void testNameResolvedFromGenericExtraction() {
79+
DescriptorConfigurator configurator = new DescriptorConfigurator(new DummyTaskDescriptor());
80+
81+
assertEquals("Should extract 'DummyTask' and convert to camelCase", "dummyTask", configurator.getName());
82+
assertEquals(Collections.singletonList("dummyTask"), configurator.getNames());
83+
}
84+
85+
@Test
86+
public void testNameResolvedFromParameterizedType() {
87+
DescriptorConfigurator configurator = new DescriptorConfigurator(new ParameterizedDescriptor());
88+
89+
assertEquals(
90+
"Should unwrap ParameterizedType to its raw class name", "parameterizedTask", configurator.getName());
91+
}
92+
93+
@Test
94+
public void testNameResolvedFromDeepInheritance() {
95+
DescriptorConfigurator configurator = new DescriptorConfigurator(new DeepTaskDescriptor());
96+
97+
assertEquals(
98+
"Should bypass type erasure and extract from superclass hierarchy",
99+
"dummyTask",
100+
configurator.getName());
101+
}
102+
103+
@Test
104+
@SuppressWarnings("rawtypes")
105+
public void testFallbackWithAnonymousTargetClass() {
106+
107+
Class<?> anonymousTarget = new DummyTask() {}.getClass();
108+
Descriptor rawDescriptor = new RawDescriptor(anonymousTarget);
109+
DescriptorConfigurator configurator = new DescriptorConfigurator(rawDescriptor);
110+
111+
assertEquals(
112+
"Should unwrap anonymous target class via the fallback logic", "dummyTask", configurator.getName());
113+
}
114+
}

0 commit comments

Comments
 (0)