Reputation: 5317
There is a strange behavior in Digester that I cannot wrap my head around.
I have the following code that call the "Role" object's constructor whenever it encounter the "roles/role" node in the input xml:
AbstractRulesModule loader = (new AbstractRulesModule() {
protected void configure() {
forPattern("roles/role").createObject().ofType(Role.class)
.usingConstructor(String.class, String.class).then()
.callParam().fromAttribute("machine").ofIndex(0);
forPattern("roles/role").callParam().fromAttribute("name")
.ofIndex(1);
forPattern("roles/role").setNext("add");
}
});
Digester digester = DigesterLoader.newLoader(loader).newDigester();
List<Role> roles = new ArrayList<>();
digester.push(roles);
digester.parse(new File("c:/RoleMapping.xml"));
System.out.println(roles);
System.out.println(Role.count);
Every time the Role's constructor is called, Role.count is incremented. Strangely, after running the above code against the following xml, Role.count is 2 instead of 1. When I debug the code, it seems that Digester tried to create 2 extra object with "null" as the constructor parameters.
<roles>
<role name="m1" machine="mymachine" />
</roles>
This would lead to all sort of problem if I have code checking if the constructor's arguments are null.
The definition of my Role class is:
public class Role {
private String machine;
private String name;
static int count = 0;
public Role(String machine, String name) {
this.machine = machine;
this.name = name;
count++;
}
}
Upvotes: 4
Views: 526
Reputation: 2836
I see the question is 3 years old, but I came across this same thing recently and the answer is still valid...
The reason the constructor is called twice is the way Digester 3 handles constructors with parameters. The problem for Digester is a chicken and egg one... it cannot call the constructor until it has all of the required parameters, but because callParam
rules can get their data from child elements, it does not have all the child elements until it has completely processed the element.
In your case, all the parameters are available in the attributes, but consider if you changed your XML to:
<roles>
<role>
<name>m1</name>
<machine>mymachine</machine>
</role>
</roles>
Or even:
<roles>
<role>
<name>m1</name>
<machine>mymachine</machine>
<another>
<tag>which</tag>
<does>morestuff</does>
...
</another>
</role>
</roles>
The digester effectively has to remember everything that happens between <role>
and </role>
, as the call param rules could be called anywhere in the child data, and it has to do all of this before creating the object.
To do this, the digester creates a proxy wrapper around the class to be constructed (Role), creates a dummy instance passing null for all constructor arguments, then calls all other methods triggered for children of the main element. The proxy class intercepts these method calls, records them (including parameters), and passes them on to the dummy instance. Once the end element tag is reached, the dummy object is discarded, a new one is created with the real constructor parameters, and all recorded method calls are 'replayed' back to the new object.
As you have noticed, this not only creates the object twice, but calls all methods triggered by the digester rules twice: once during the 'recording' phase, and once during the 'playback' phase.
This all works fine for simple data objects, but can have strange consequences when constructing more complex ones. See this digester ticket for an example.
To just avoid null pointer exceptions, you can tell the digester which values to use for default constructor parameters using the usingDefaultConstructorArguments
rule:
forPattern("roles/role").createObject().ofType(Role.class)
.usingConstructor(String.class, String.class).then()
.usingDefaultConstructorArguments("one", "two").then()
.callParam().fromAttribute("machine").ofIndex(0);
For more complicated cases, or just if you prefer the approach, you can use a builder class and a custom rule. The basic idea is when you get to the element you push an builder class on to the stack along with a custom rule triggered on the element end tag. While processing the element body, the digester calls all rules as normal passing data to the builder class. At the end tag, the custom rule is triggered which invokes the builder to build the object, then replaces the builder object on the digester stack with the built object. This does require a custom builder class, but is a lot simpler than it sounds. See this digester ticket for a working example.
Hope this clears up the mystery!
Upvotes: 0