11package io .jenkins .plugins .casc ;
22
3+ import static java .lang .reflect .Array .newInstance ;
4+ import static java .lang .reflect .Array .set ;
5+
36import edu .umd .cs .findbugs .annotations .NonNull ;
47import hudson .BulkChange ;
58import hudson .model .Describable ;
2225import java .lang .reflect .TypeVariable ;
2326import java .lang .reflect .WildcardType ;
2427import java .util .ArrayList ;
25- import java .util .Arrays ;
2628import java .util .Collection ;
2729import java .util .Collections ;
2830import java .util .HashMap ;
3739import org .kohsuke .accmod .AccessRestriction ;
3840import org .kohsuke .accmod .Restricted ;
3941import org .kohsuke .accmod .restrictions .Beta ;
42+ import org .kohsuke .accmod .restrictions .NoExternalUse ;
4043import org .kohsuke .accmod .restrictions .None ;
4144
4245/**
@@ -76,57 +79,116 @@ public abstract class BaseConfigurator<T> implements Configurator<T> {
7679 }
7780
7881 final Class <T > target = getTarget ();
82+ Map <String , List <Method >> methodsByProperty = new HashMap <>();
7983 // Resolve the methods and merging overrides to more concretized signatures
8084 // because the methods can to have been overridden with concretized type
81- // TODO: Overloaded setters with different types can corrupt this logic
8285 for (Method method : target .getMethods ()) {
8386 final String methodName = method .getName ();
84- TypePair type ;
8587 if (method .getParameterCount () == 0
8688 && methodName .startsWith ("get" )
8789 && PersistedList .class .isAssignableFrom (method .getReturnType ())) {
88- type = TypePair .ofReturnType (method );
89- } else if (method .getParameterCount () != 1 || !methodName .startsWith ("set" )) {
90- // Not an accessor, ignore
90+ String name = StringUtils .uncapitalize (methodName .substring (3 ));
91+ if (exclusions .contains (name )) {
92+ continue ;
93+ }
94+
95+ TypePair type = TypePair .ofReturnType (method );
96+ @ SuppressWarnings ("unchecked" )
97+ Attribute <T , ?> attribute = (Attribute <T , ?>) createAttribute (name , type );
98+
99+ if (attribute != null ) {
100+ attribute .deprecated (method .getAnnotation (Deprecated .class ) != null );
101+ final Restricted r = method .getAnnotation (Restricted .class );
102+ if (r != null ) {
103+ attribute .restrictions (r .value ());
104+ }
105+ attributes .putIfAbsent (name , attribute );
106+ }
91107 continue ;
92- } else {
93- type = TypePair .ofParameter (method , 0 );
94108 }
95109
96- final String s = methodName .substring (3 );
97- final String name = StringUtils .uncapitalize (s );
98- if (exclusions .contains (name )) {
99- continue ;
110+ if (method .getParameterCount () == 1 && methodName .startsWith ("set" )) {
111+ String propertySuffix = methodName .substring (3 );
112+ final String name = StringUtils .uncapitalize (propertySuffix );
113+
114+ if (exclusions .contains (name )) {
115+ continue ;
116+ }
117+ methodsByProperty
118+ .computeIfAbsent (propertySuffix , k -> new ArrayList <>())
119+ .add (method );
100120 }
121+ }
101122
102- if (!hasGetter (target , s )) {
103- // Looks like a property but no actual getter method we can use to read value
123+ for (Map .Entry <String , List <Method >> entry : methodsByProperty .entrySet ()) {
124+ final String propertySuffix = entry .getKey ();
125+ final String name = StringUtils .uncapitalize (propertySuffix );
126+
127+ Method g = findGetter (target , propertySuffix );
128+
129+ if (g == null ) {
104130 continue ;
105131 }
106132
107- LOGGER .log (Level .FINER , "Processing {0} property" , name );
133+ Class <?> getterRawType = g .getReturnType ();
134+
135+ List <Method > candidateSetters = entry .getValue ().stream ()
136+ .filter (m -> {
137+ Class <?> paramType = m .getParameterTypes ()[0 ];
138+ return isSameType (paramType , getterRawType )
139+ || getterRawType .isAssignableFrom (paramType )
140+ || paramType .isAssignableFrom (getterRawType );
141+ })
142+ .collect (Collectors .toList ());
143+
144+ if (candidateSetters .isEmpty ()) {
145+ candidateSetters = entry .getValue ();
146+ }
147+
148+ Method bestMethod = resolveBestSetter (candidateSetters , getterRawType );
149+ TypePair type = TypePair .ofParameter (bestMethod , 0 );
150+
151+ TypePair getterType = TypePair .ofReturnType (g );
152+ if (type .rawType .isAssignableFrom (getterType .rawType )) {
153+ type = getterType ;
154+ }
108155
109156 if (Map .class .isAssignableFrom (type .rawType )) {
110157 // yaml has support for Maps, but as nobody seem to like them we agreed not to support them
111158 LOGGER .log (Level .FINER , "{0} is a Map<?,?>. We decided not to support Maps." , name );
112159 continue ;
113160 }
114161
115- Attribute attribute = createAttribute (name , type );
116- if (attribute == null ) {
162+ final TypePair finalType = type ;
163+
164+ @ SuppressWarnings ("unchecked" )
165+ Attribute <T , Object > rawAttribute = (Attribute <T , Object >) createAttribute (name , type );
166+ if (rawAttribute == null ) {
117167 continue ;
118168 }
169+ rawAttribute .setter ((targetInstance , value ) -> {
170+ Object finalValue = value ;
171+
172+ if (value instanceof Collection <?> collection && finalType .rawType .isArray ()) {
173+ Object array = newInstance (rawAttribute .getType (), collection .size ());
174+ int i = 0 ;
175+ for (Object item : collection ) {
176+ set (array , i ++, item );
177+ }
178+ finalValue = array ;
179+ }
180+ bestMethod .invoke (targetInstance , finalValue );
181+ });
119182
120- attribute .deprecated (method .getAnnotation (Deprecated .class ) != null );
121- final Restricted r = method .getAnnotation (Restricted .class );
183+ rawAttribute .deprecated (bestMethod .getAnnotation (Deprecated .class ) != null );
184+ final Restricted r = bestMethod .getAnnotation (Restricted .class );
122185 if (r != null ) {
123- attribute .restrictions (r .value ());
186+ rawAttribute .restrictions (r .value ());
124187 }
125188
126- Attribute prevAttribute = attributes .get (name );
127- // Replace the method if it have more concretized type
128- if (prevAttribute == null || prevAttribute .type .isAssignableFrom (attribute .type )) {
129- attributes .put (name , attribute );
189+ Attribute <T , ?> prevAttribute = attributes .get (name );
190+ if (prevAttribute == null || ((Class <?>) prevAttribute .type ).isAssignableFrom (rawAttribute .type )) {
191+ attributes .put (name , rawAttribute );
130192 }
131193 }
132194
@@ -136,14 +198,25 @@ public abstract class BaseConfigurator<T> implements Configurator<T> {
136198 /**
137199 * Check if target class has a Getter method for property s
138200 */
139- private boolean hasGetter (Class <T > c , String s ) {
140- List <String > candidates = Arrays .asList ("get" + s , "is" + s );
201+ private Method findGetter (Class <T > c , String s ) {
202+ String getMethod = "get" + s ;
203+ String isMethod = "is" + s ;
204+
141205 for (Method m : c .getMethods ()) {
142- if (m .getParameterCount () == 0 && candidates .contains (m .getName ())) {
143- return true ;
206+ if (m .getParameterCount () == 0 ) {
207+ if (m .getName ().equals (getMethod )) {
208+ return m ;
209+ }
210+
211+ if (m .getName ().equals (isMethod )) {
212+ Class <?> returnType = m .getReturnType ();
213+ if (returnType == boolean .class || returnType == Boolean .class ) {
214+ return m ;
215+ }
216+ }
144217 }
145218 }
146- return false ;
219+ return null ;
147220 }
148221
149222 /**
@@ -439,9 +512,9 @@ public static final class TypePair {
439512 /**
440513 * Erasure of {@link #type}
441514 */
442- final Class rawType ;
515+ final Class <?> rawType ;
443516
444- public TypePair (Type type , Class rawType ) {
517+ public TypePair (Type type , Class <?> rawType ) {
445518 this .rawType = rawType ;
446519 this .type = type ;
447520 }
@@ -476,4 +549,101 @@ public boolean equals(Object obj) {
476549 public int hashCode () {
477550 return getTarget ().hashCode ();
478551 }
552+
553+ @ Restricted (NoExternalUse .class )
554+ Method resolveBestSetter (List <Method > methods , Class <?> getterRawType ) {
555+ List <Method > realMethods =
556+ methods .stream ().filter (m -> !m .isBridge () && !m .isSynthetic ()).collect (Collectors .toList ());
557+ if (!realMethods .isEmpty ()) {
558+ methods = realMethods ;
559+ }
560+ if (methods .size () == 1 ) {
561+ return methods .get (0 );
562+ }
563+
564+ if (getterRawType != null ) {
565+ for (Method m : methods ) {
566+ if (isSameType (m .getParameterTypes ()[0 ], getterRawType )) {
567+ return m ;
568+ }
569+ }
570+ }
571+ List <Method > arrayMethods =
572+ methods .stream ().filter (m -> m .getParameterTypes ()[0 ].isArray ()).collect (Collectors .toList ());
573+ if (!arrayMethods .isEmpty ()) {
574+ methods = arrayMethods ;
575+ }
576+
577+ Method best = null ;
578+ Class <?> bestType = null ;
579+
580+ for (Method m : methods ) {
581+ Class <?> currentType = m .getParameterTypes ()[0 ];
582+
583+ if (best == null ) {
584+ best = m ;
585+ bestType = currentType ;
586+ continue ;
587+ }
588+
589+ if (bestType .isAssignableFrom (currentType )) {
590+ best = m ;
591+ bestType = currentType ;
592+ } else if (!currentType .isAssignableFrom (bestType )) {
593+ boolean currentMatch = (getterRawType != null && getterRawType .isAssignableFrom (currentType ));
594+ boolean bestMatch = (getterRawType != null && getterRawType .isAssignableFrom (bestType ));
595+
596+ if (currentMatch == bestMatch
597+ && ((!currentType .isInterface () && bestType .isInterface ())
598+ || (currentType .isInterface () == bestType .isInterface ()
599+ && currentType .getName ().compareTo (bestType .getName ()) < 0 ))) {
600+
601+ best = m ;
602+ bestType = currentType ;
603+ }
604+ }
605+ }
606+ return best ;
607+ }
608+
609+ private boolean isSameType (Class <?> a , Class <?> b ) {
610+ if (a == b ) {
611+ return true ;
612+ }
613+ if (a .isPrimitive () && !b .isPrimitive ()) {
614+ return isWrapper (b , a );
615+ }
616+ if (b .isPrimitive () && !a .isPrimitive ()) {
617+ return isWrapper (a , b );
618+ }
619+ return false ;
620+ }
621+
622+ private boolean isWrapper (Class <?> wrapper , Class <?> primitive ) {
623+ if (primitive == int .class ) {
624+ return wrapper == Integer .class ;
625+ }
626+ if (primitive == boolean .class ) {
627+ return wrapper == Boolean .class ;
628+ }
629+ if (primitive == long .class ) {
630+ return wrapper == Long .class ;
631+ }
632+ if (primitive == double .class ) {
633+ return wrapper == Double .class ;
634+ }
635+ if (primitive == float .class ) {
636+ return wrapper == Float .class ;
637+ }
638+ if (primitive == byte .class ) {
639+ return wrapper == Byte .class ;
640+ }
641+ if (primitive == char .class ) {
642+ return wrapper == Character .class ;
643+ }
644+ if (primitive == short .class ) {
645+ return wrapper == Short .class ;
646+ }
647+ return false ;
648+ }
479649}
0 commit comments