4

I have created a Bean Class using Builder Pattern and having issues creating an object from a yaml file.

Here is a sample class (Actual class is quite big, this is just an excerpt incase if you wanted to answer with an example):

public class ClientBuilder {

    private final String firstName;
    private final String lastName;
    private final String displayName;

    private ClientBuilder(Builder builder) { 
        firstName   = builder.firstName; 
        lastName    = builder.lastName; 
        displayName = builder.displayName;
    }

    public static class Builder {

        private final String displayName; // Mandatory Attribute

        public Builder( String displayName ) {
            this.displayName = displayName;
        }

        private String firstName;
        private String lastName;

        public Builder setFirstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        public Builder setLastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

        public ClientBuilder build() {
            return new ClientBuilder(this);
        }
    }

    @Override
    public String toString() {
        StringBuffer sbf = new StringBuffer();
        sbf.append("New Company Object: \n");
        sbf.append("firstName   : " +  this.firstName   + "\n");
        sbf.append("lastName    : " +  this.lastName    + "\n");
        sbf.append("displayName : " +  this.displayName + "\n");
        return sbf.toString();
    }
}

I am using snakeyaml to load the file but any yaml api would work. Since the displayName is a mandatory param, I want to pass that value while creating the instance. The other params can be passed while creating the object but I would like the option to load them through yaml file.

I am able to load the yaml file if I use java bean. Is there a way to instantiate builder objects?

I tried:

InputStream input = new FileInputStream(new File("src/main/resources/client.yaml"));
Yaml yaml = new Yaml();
Builder builder = new Builder("Display Name");
builder = (Builder) yaml.loadAs(input, ClientBuilder.Builder.class);
ClientBuilder client = builder.build();
System.out.println(client.toString());

but I get following error:

Exception in thread "main" Can't construct a java object for tag:yaml.org,2002:com.xxx.xxx.xxx.ClientBuilder$Builder; exception=java.lang.NoSuchMethodException: com.xxx.xxx.xxx.ClientBuilder$Builder.<init>()
 in 'reader', line 2, column 1:
    firstName: "Jaypal"
jaypal singh
  • 74,723
  • 23
  • 102
  • 147

2 Answers2

2

SnakeYaml is a very powerful library & it provides support for creating instance based on constructor injection.

/**
 * create JavaBean
 */
public void testGetBeanAssumeClass() {
    String data = "--- !!org.yaml.snakeyaml.constructor.Person\nfirstName: Andrey\nage: 99";
    Object obj = construct(data);
    assertNotNull(obj);
    assertTrue("Unexpected: " + obj.getClass().toString(), obj instanceof Person);
    Person person = (Person) obj;
    assertEquals("Andrey", person.getFirstName());
    assertNull(person.getLastName());
    assertEquals(99, person.getAge().intValue());
}

/**
 * create instance using constructor arguments
 */
public void testGetConstructorBean() {
    String data = "--- !!org.yaml.snakeyaml.constructor.Person [ Andrey, Somov, 99 ]";
    Object obj = construct(data);
    assertNotNull(obj);
    assertTrue(obj.getClass().toString(), obj instanceof Person);
    Person person = (Person) obj;
    assertEquals("Andrey", person.getFirstName());
    assertEquals("Somov", person.getLastName());
    assertEquals(99, person.getAge().intValue());
}

Junit code sample can be viewed here. So your code still holds good. You may need to change yaml content with proper format. Once done, you are all set.

Sairam Krish
  • 10,158
  • 3
  • 55
  • 67
1

The exception is about there not being a no-arg constructor in Builder, as you probably figured out.

What you could do is to allow no-arg constructor for Builder and add corresponding setter and getter for displayName to it. Then simply throw an exception in build() if displayName is not set (or provide a default value for it). The exception can be a runtime one, or you could make it clear and add an explicit throws.

While it is not the prettiest solution, it should work just fine. The fact that the Builder is created without a mandatory argument should not matter, as it is the ClientBuilder that needs to be constructed properly (as the factory/builder is used to ensure that each instance of whatever it is building is correct).

I have no way to access any yaml parsing tools for Java currently, but if there is any way I could improve my answer, let me know - I will be happy to do so.

Miki
  • 7,052
  • 2
  • 29
  • 39
  • Thanks Sorrow for leaving an answer. Yes, adding a no arg constructor works but it _only_ works if I make all the `setters` of my `Builder` class as `void` instead of `Builder` return type. If I loose the ability of having the fluent interface then I guess I should just go with bean pattern. What are your thoughts? – jaypal singh Mar 26 '15 at 01:41
  • 1
    You probably should not stick too much with the patterns, they should be more _guidelines rather than strict code_ ;) There is nothing preventing you from keeping the setters the way they are, at least I am not seeing it. Yes, there is a risk that `Builder` is in incorrect state (no `displayName` set), but it is there even if your setters return `void`. The trick here is delaying the moment when the incorrect state really matters. Does it matter when constructing `Builder`, or rather `ClientBuilder`? The latter seems more appropriate in this case, at least to me. – Miki Mar 26 '15 at 07:54
  • Some wise words I must say! `:)`. The setters don't conform to bean standard and it appears snakeyaml wants that. If I change it back to my old setters (that returns this, instead of void) it errors out. This is going to be part of a test suite and having recently read Effective Java, I thought Builder pattern would be more appropriate. I am ok with beans too. They are easy to create (thanks to eclipse auto code generation `:P`)! – jaypal singh Mar 26 '15 at 15:06