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

2

9

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?

Blithesome answered 23/10, 2012 at 6:48 Comment(0)
A
16

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.

Arvo answered 26/10, 2012 at 17:17 Comment(1)
Its important to mention that MyClass and EnableTextBlock require a Descriptor, so that can correctly display previous data.Rockyrococo
S
12

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.

Sharie answered 31/7, 2014 at 22:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.