How to use optionalBlock in build step's config.jelly

5k Views Asked by At

I have problem with creating constructor, which Jenkins can call for some JSON data originating from a Jelly form,. For testing, I created a minimal Jenkins plugin with mvn hpi:create and following two custom files:

src/main/resources/foo/hyde/jenkins/plugins/OptionalBlockSampleBuilder/config.jelly

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:block>
    <table>
        <f:optionalBlock name="enableText" title="Enable optional text" checked="${instance.enableText}">
            <f:entry title="Optional text" field="text">
                <f:textbox />
            </f:entry>
        </f:optionalBlock>
    </table>
</f:block>

src/main/java/foo/hyde/jenkins/plugins/OptionalBlockSampleBuilder.java

package foo.hyde.jenkins.plugins;

public class OptionalBlockSampleBuilder extends hudson.tasks.Builder {

    public final String text;
    public final boolean enableText;

    @org.kohsuke.stapler.DataBoundConstructor
    public OptionalBlockSampleBuilder(String text, Boolean enableText) {
        this.text = text;
        this.enableText = (enableText != null) && enableText;
    }

    @Override
    public boolean perform(hudson.model.AbstractBuild build, hudson.Launcher launcher, hudson.model.BuildListener listener) {
        listener.getLogger().println("OptionalBlockSampleBuilder " + enableText + "/" + text);
        return true;
    }

    @hudson.Extension
    public static final class DescriptorImpl extends hudson.tasks.BuildStepDescriptor<hudson.tasks.Builder> {
        public boolean isApplicable(Class<? extends hudson.model.AbstractProject> aClass) {
            return true;
        }
        public String getDisplayName() {
            return "Optional Block Sample";
        }
    }
}

I'm building against pom.xml parent <groupId>org.jenkins-ci.plugins</groupId><artifactId>plugin</artifactId><version>1.454</version>, and everything builds, Netbeans 6.9.1 launches Debug Jenkins and I get to create a job with this build step. Everything works if I don't check that checkbox, and I get expected OptionalBlockSampleBuilder false/null to job's console output.

But if I do check the checkbox and add text, then saving/applying the job config gives this exception from the depths of Jenkins code, when it tries to call my constructor:

java.lang.RuntimeException:
  Failed to instantiate class
    foo.hyde.jenkins.plugins.OptionalBlockSampleBuilder 
  from {
    "enableText":{"text":"xx"},
    "kind":"foo.hyde.jenkins.plugins.OptionalBlockSampleBuilder",
    "stapler-class":"foo.hyde.jenkins.plugins.OptionalBlockSampleBuilder"
    }

There has to be a simple fix. I have tried many different changes, and also tried to see how other plugins use it, and finally created this minimal test plugin. How to fix it to make optionalBlock work?

2

There are 2 best solutions below

1
On BEST ANSWER

The hint comes from the JSON data:

{
"enableText":{"text":"xx"},
"kind":"foo.hyde.jenkins.plugins.OptionalBlockSampleBuilder",
"stapler-class":"foo.hyde.jenkins.plugins.OptionalBlockSampleBuilder"
}

You can see here that enableText contains a child property, text. That means that the f:optionalBlock is actually expecting an encapsulation of all the fields contained within the block -- when the block is checked, you will receive an instance of the encapsulation field class; when it is unchecked, that field will be null. To use the optionalBlock properly, you would need the @DataBoundConstructor to take in a single nullable class instance that encapsulates the entire optionalBlock. For example:

private String text;

@DataBoundConstructor
public MyClass(EnableTextBlock enableText)
{
    if (enableText != null)
    {
        this.text = enableText.text;
    }
}

public static class EnableTextBlock
{
    private String text;

    @DataBoundConstructor
    public EnableTextBlock(String text)
    {
        this.text = text;
    }
}

Notice that the enableText field in this case is actually an instance of EnableTextBlock class, which contains a child property, text. That will satisfy the JSON object that is being sent in the form.


Instead, if all you need is a single field that has a checkbox to enable entry of that field, you might want to consider instead using the f:optionalProperty tag, which will take care of that single-field encapsulation for you. However, in many cases, the optionalBlock is actually needed to configure multiple fields, in which case the encapsulation class--as exampled above--is usually the correct way to go.

The encapsulation class does not have to be a static inner class; it could be a separate class within your package, but the important part is that the DataBoundConstructor should take in an argument that matches the JSON structure being passed from the form.

0
On

Or you can add inline tag to optionalBlock like this:

<f:optionalBlock inline="true">

if inline is present, the foldable section will not be grouped into a separate JSON object upon submission.