user2917629
user2917629

Reputation: 942

groovy: change setter properties call order

In my groovy class, I have 3 properties below:

class Story{
    @JsonUnwrapped
    Object collections;

    String dateformat;    

    @JsonUnwrapped
    Object  custom;    

    public String getDateformat(){
        println "getDateformat - ${dateformat}";
        return this.dateformat;
    }
    public void setDateformat(String str){
        this.dateformat = str; 
        println "setDateformat - ${dateformat}";
    }

    public void setCollections(Object obj)
    {
        println "setCollections";
    }
    public void setCustom(Object obj){
        println "setCustom: ${this.dateformat}";
    }
}

On class instanciation, the setters are automatically called is this order:

setCustom: null
setCollections
setDateformat - yyyy/MM/dd HH:mm:ss.SSS Z
getDateformat - yyyy/MM/dd HH:mm:ss.SSS Z

The fields can not be renamed. I would like dateformat property to be available in setCustom setter method. Is there any way I could achieve that without creating a second custom constructor?

Upvotes: 1

Views: 291

Answers (1)

Emmanuel Rosa
Emmanuel Rosa

Reputation: 9885

To my surprise, yes!

And it's super simple. I excluded the JSON annotations since they are irrelevant, but here's an example:

class Story{
    Object collections
    String dateformat
    Object  custom

    public String getDateformat(){
        println "getDateformat - ${dateformat}";
        return this.dateformat;
    }
    public void setDateformat(String str){
        this.dateformat = str; 
        println "setDateformat - ${dateformat}";
    }

    public void setCollections(Object obj)
    {
        println "setCollections";
    }
    public void setCustom(Object obj){
        println "setCustom: ${this.dateformat}";
    }
}

def s = new Story(collections: new Object(), dateformat: 'yyyy/MM/dd HH:mm:ss.SSS Z', custom: new Object())

The output looks like this:

setCollections
setDateformat - yyyy/MM/dd HH:mm:ss.SSS Z
setCustom: yyyy/MM/dd HH:mm:ss.SSS Z

How does it work?

The trick is to call the Map-based constructor with the properties defined in the proper order.

The default Map-based constructor provided by Groovy is actually handled by the runtime. More specifically, MetaClassImpl. It first creates an instance with the no-argument constructor, and then sets each of the properties in the Map. The important thing here is how the properties are set:

for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
    Map.Entry entry = (Map.Entry) iter.next();
    String key = entry.getKey().toString();

    Object value = entry.getValue();
    setProperty(bean, key, value);
}

As you can see, it iterates through the keys in the Map and applies each one. When using the constructor syntax in my example, you're declaring a Groovy Map literal, which ends up being an instance of LinkedHashMap. LinkedHashMap preserves the order in which the keys are defined, allowing you to control the order in which the properties are set.

Should you even do this?

Having said all of that, having such an unintuitive dependency on the order in which the properties are set will make the Story class quite fragile. Instead, you can add a new method, lets call it validate(), which will take into account the interdependencies of the properties and ensure everything is good to go before you use the instance.

class Story{
    Object collections
    String dateformat
    Object custom

    boolean validate() {
        /*
         * Do whatever the hell you want with
         * dateformat and custom.
         * Return whether it's all good or not.
         * Or, throw an Exception.
         */
    }
}

Upvotes: 2

Related Questions