Uploaded image for project: 'Jenkins'
  1. Jenkins
  2. JENKINS-28510

DescribableHelper error with @DataBoundSetter on Map<String,String>

    • Icon: Bug Bug
    • Resolution: Unresolved
    • Icon: Minor Minor
    • structs-plugin
    • Jenkins 1.609
      workflow-plugin 1.6

      I'm working on implementing a new step in a plugin, and came across the following error:
      If one argument step is a Map, and it is a field annotated with @DataBoundSetter, Ok, no problems. But if it is a Setter annotated with @DataBoundSetter the following error occurs during instantiation:

      java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl cannot be cast to java.lang.Class
      	at org.jenkinsci.plugins.workflow.structs.DescribableHelper.coerce(DescribableHelper.java:214)
      	at org.jenkinsci.plugins.workflow.structs.DescribableHelper.buildArguments(DescribableHelper.java:186)
      	at org.jenkinsci.plugins.workflow.structs.DescribableHelper.injectSetters(DescribableHelper.java:308)
      	at org.jenkinsci.plugins.workflow.structs.DescribableHelper.instantiate(DescribableHelper.java:93)
      	at org.jenkinsci.plugins.workflow.steps.StepDescriptor.newInstance(StepDescriptor.java:95)
      	at org.jenkinsci.plugins.workflow.cps.DSL.invokeMethod(DSL.java:133)
      	at org.jenkinsci.plugins.workflow.cps.CpsScript.invokeMethod(CpsScript.java:100)
      	at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:45)
      	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:42)
      	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:108)
      	at com.cloudbees.groovy.cps.sandbox.DefaultInvoker.methodCall(DefaultInvoker.java:15)
      	at WorkflowScript.run(WorkflowScript:5)
      ...
      

      Apparently the error is in injectSetters() method of the class DescribableHelper

          [JENKINS-28510] DescribableHelper error with @DataBoundSetter on Map<String,String>

          Jesse Glick added a comment -

          Probably caused by use of wildcards in the type.

          Jesse Glick added a comment - Probably caused by use of wildcards in the type.

          Flávio Augusto Valones added a comment - - edited

          Hi Jesse, wildcards are not used in this case, it is a Map<String, String>.
          In the implementation of the AbstractStepImpl, if the map param is annotated in the field it works correctly, but if it is annotated in setter the error occurs.
          There are other parameters of types String, int and boolean, but these work normally whether annotated in the field or setter.

          Flávio Augusto Valones added a comment - - edited Hi Jesse, wildcards are not used in this case, it is a Map<String, String> . In the implementation of the AbstractStepImpl , if the map param is annotated in the field it works correctly, but if it is annotated in setter the error occurs. There are other parameters of types String , int and boolean , but these work normally whether annotated in the field or setter.

          Jesse Glick added a comment -

          A bug in DescribableHelper that it throws this low-level exception, but I doubt it could work in any case. Jenkins has no databinding support for Map<String,String>. You must choose a type for which an actual /lib/form control exists.

          Jesse Glick added a comment - A bug in DescribableHelper that it throws this low-level exception, but I doubt it could work in any case. Jenkins has no databinding support for Map<String,String> . You must choose a type for which an actual /lib/form control exists.

          Jesse Glick added a comment -

          (You could override StepDescriptor.newInstance and manually parse out content coming in from a Groovy script, but Snippet Generator will not work, so this is not a good idea.)

          Jesse Glick added a comment - (You could override StepDescriptor.newInstance and manually parse out content coming in from a Groovy script, but Snippet Generator will not work, so this is not a good idea.)

          Flávio Augusto Valones added a comment - - edited

          Thanks for the reply, I use the Map<String, String> only as a workflow-step param, the conventional UI Builder uses only supported databinds compatible with /lib/form controls. Here's the code:

          public class SCTMStep extends AbstractStepImpl {
            
            private final int projectId;
            private final List<Integer> execDefIds;
            
            @DataBoundSetter public Map<String, String> params;
            @DataBoundSetter public String buildNumber;
            @DataBoundSetter public int delay;
            @DataBoundSetter public boolean continueOnError;
            @DataBoundSetter public boolean collectResults = true;
            @DataBoundSetter public boolean ignoreSetupCleanup;  
            
            @DataBoundConstructor
            public SCTMStep(final int projectId, final List<Integer> execDefIds) {
              this.projectId = projectId;
              this.execDefIds = execDefIds;    
            }  
          
            //getters for private fields
            
            @Extension(optional = true) 
            public static final class DescriptorImpl extends AbstractStepDescriptorImpl {
          
              public DescriptorImpl() { super(Execution.class); }
          
              @Override public String getFunctionName() { return "sctm"; }
          
              @Override public String getDisplayName() { return Messages.getString("SCTMExecutorDescriptor.plugin.title"); }
          
            }
            
            public static final class Execution extends AbstractSynchronousStepExecution<Boolean> {
              
              private static final long serialVersionUID = 1L;
              
              @Inject private transient SCTMStep step;    
              
              @StepContextParameter private transient TaskListener listener;    
              @StepContextParameter private transient FilePath workspace;
              @StepContextParameter private transient Run<?, ?> run;
          
              @Override
              protected Boolean run() throws Exception {
                SCTMExe sctmExe = new SCTMExe(workspace, listener);
                sctmExe.execute(step.projectId, step.execDefIds, step.params, step.buildNumber, 
                    step.delay, step.collectResults, step.ignoreSetupCleanup, step.continueOnError, false);
                
                if (step.collectResults) {
                  sctmExe.publish(run);
                }
                return true;
              }
              
            }
            
          }
          

          The databind of the Map<String, String> is done correctly and in the workflow Job this works like a charm:

          ...
          sctm projectId: 123, execDefIds: [456, 789], params: [NAME: 'Flávio Augusto Valones', ADDR: 'Some Street']
          ...
          

          The sctm step is listed in the Snippet Generator (I have not yet implemented the jelly form for it), and everything goes as expected.

          The exception only occurs if I do this in the SCTMStep:

          ...
          private Map<String, String> params;
          
          @DataBoundSetter public void setParams(Map<String, String> params) { ... }
          ...
          

          Flávio Augusto Valones added a comment - - edited Thanks for the reply, I use the Map<String, String> only as a workflow-step param, the conventional UI Builder uses only supported databinds compatible with /lib/form controls. Here's the code: public class SCTMStep extends AbstractStepImpl { private final int projectId; private final List< Integer > execDefIds; @DataBoundSetter public Map< String , String > params; @DataBoundSetter public String buildNumber; @DataBoundSetter public int delay; @DataBoundSetter public boolean continueOnError; @DataBoundSetter public boolean collectResults = true ; @DataBoundSetter public boolean ignoreSetupCleanup; @DataBoundConstructor public SCTMStep( final int projectId, final List< Integer > execDefIds) { this .projectId = projectId; this .execDefIds = execDefIds; } //getters for private fields @Extension(optional = true ) public static final class DescriptorImpl extends AbstractStepDescriptorImpl { public DescriptorImpl() { super (Execution.class); } @Override public String getFunctionName() { return "sctm" ; } @Override public String getDisplayName() { return Messages.getString( "SCTMExecutorDescriptor.plugin.title" ); } } public static final class Execution extends AbstractSynchronousStepExecution< Boolean > { private static final long serialVersionUID = 1L; @Inject private transient SCTMStep step; @StepContextParameter private transient TaskListener listener; @StepContextParameter private transient FilePath workspace; @StepContextParameter private transient Run<?, ?> run; @Override protected Boolean run() throws Exception { SCTMExe sctmExe = new SCTMExe(workspace, listener); sctmExe.execute(step.projectId, step.execDefIds, step.params, step.buildNumber, step.delay, step.collectResults, step.ignoreSetupCleanup, step.continueOnError, false ); if (step.collectResults) { sctmExe.publish(run); } return true ; } } } The databind of the Map<String, String> is done correctly and in the workflow Job this works like a charm: ... sctm projectId: 123, execDefIds: [456, 789], params: [NAME: 'Flávio Augusto Valones' , ADDR: 'Some Street' ] ... The sctm step is listed in the Snippet Generator (I have not yet implemented the jelly form for it), and everything goes as expected. The exception only occurs if I do this in the SCTMStep : ... private Map< String , String > params; @DataBoundSetter public void setParams(Map< String , String > params) { ... } ...

          Jesse Glick added a comment -

          Well @DataBoundSetter is supposed to be on the setter, not the getter of course.

          Creating Step implementations with parameter types that cannot be data-bound is possible, but not easy and not recommended if you can possibly avoid it. StepDescriptor.newInstance (and potentially .defineArguments) overrides are needed to bypass DescribableHelper, and config.jelly would need to omit that parameter while documenting its syntax in help.html. BuildTriggerStep is the only example I can think of.

          Jesse Glick added a comment - Well @DataBoundSetter is supposed to be on the setter , not the getter of course. Creating Step implementations with parameter types that cannot be data-bound is possible, but not easy and not recommended if you can possibly avoid it. StepDescriptor.newInstance (and potentially .defineArguments ) overrides are needed to bypass DescribableHelper , and config.jelly would need to omit that parameter while documenting its syntax in help.html . BuildTriggerStep is the only example I can think of.

          Oops, my fault, the error occurs in the setter, I erroneously transcribed manually on getter, fixed previous comment. Thanks for the tips, I'll think of a way to refactor the parameters to make them practical as the Map, but data-bound supported.

          Flávio Augusto Valones added a comment - Oops, my fault, the error occurs in the setter, I erroneously transcribed manually on getter, fixed previous comment. Thanks for the tips, I'll think of a way to refactor the parameters to make them practical as the Map, but data-bound supported.

            jglick Jesse Glick
            valones Flávio Augusto Valones
            Votes:
            1 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: