Introduction
Ironically, property models are the most convenient and the most troublesome feature of wicket. Lets use the following code as an example:
new TextField(“zip”, new PropertyModel(person, “addressBook.addresses.0.zip”));
They are convenient because they make it trivial to bind components to domain objects using one line of code.
They are troublesome because they create code that is easily broken and whose breakage is not apparent until run time. For example: if the AddressBook#getAddresses() method used in the example above is renamed to AddressBook#getLocations() it will not cause a compilation error, but rather a runtime error when the page is rendered. This is a big problem unless every page of the webapp is covered by a unit test.
Possible Solutions
There are a couple of ways to fix the problem:
- Do not use property models; instead create models that implement get/setObject methods using plain java code. Advantages: easy, refactor-safe, compile time errors. Disadvantage: extremely verbose, no default null handling in intermediate return values.
- Use proxies to generate dynamic builders. Advantages: refactor-safe, compile time errors. Disadvantages: runtime overhead (negligible), use of proxies limits code features(cannot intercept final methods, etc), most likely cannot be inlined thus requires at least two lines of code instead of one.
- Use a source code analyzer to check the expression string at compile time. Advantages: compile time errors, most visibility into code – can take advantage of generic types that are erased at runtime. Disadvantages: not refactor-safe, requires source for all classes used in the expression, extra compilation step.
- Use
aptto generate metadata about classes and use that metadata instead of the expression string. Advantages: transparent code generation in java 6, compile time errors. Disadvantages: not refactor-safe, only works for classes compiled with apt processor. - Use a bytecode analyzer to check the expression string at compile time or test time. Advantages: compile or test time errors, no extra compilation step if used in test time. Disadvantages: not refactor-safe.
Salve’s solution
The bytecode analyzer is what I chose to implement as part of Salve. It is implemented as a unit test that scans for use of salve.expr.Pe class that represents property expressions in Salve.
A property model usage with Salve’s Pe class can look something like this:
new TextField(“zip”, new PropertyModel(person, new Pe(Person.class, “addressBook.addresses.0.zip”).toString()));
While the unit test can be installed by creating a simple subclass of Salve’s PropertyExpressionTest within the project being checked:
public class ExpressionValidatorTest extends PropertyExpressionTest {}
When executed the unit test will scan for .class files in the project folder, look for instantiations of Pe class and check the string expression. Any error will cause the test to fail.
Using Pe class does make the code somewhat longer. The checker supports definitions of custom classes. For example we can create a more convenient version of the PropertyModel like so:
public class PeModel<T> extends PropertyModel<T> { public PeModel(Object modelObject, Class< ? > clazz, String expression) { // we do not use the clazz param, it is only here for bytecode analyzer super(modelObject, expression); } }
With usage:
new TextField(“zip”, new PeModel(person, Person.class, “addressBook.addresses.0.zip”));
And let the unit test know about it by specifying a matching rule:
public class ExpressionValidatorTest extends PropertyExpressionTest { @Override protected Set<Rule> getRules() { Set<Rule> rules = super.getRules(); rules.add(new Rule("commons/web/util/model/PeModel", Part.TYPE, Part.PATH)); return rules; } }
Rules match on the last x parameters. In the above example the rule will match instantiations of the PeModel class with constructor signature of (*,Class rootType, String path).
Another convenient usage pattern is binding components to fields of the parent via: new PropertyModel(this, “name”);. Salve’s checker supports this pattern like so:
public PeModel(Object modelObject, String expression) { super(modelObject, expression); } rules.add(new Rule("commons/web/util/model/PeModel", Part.THIS, Part.PATH));
Installing Salve:
svn checkout http://salve.googlecode.com/svn/trunk/ salve cd salve mvn install <dependency> <groupId>salve</groupId> <artifactId>salve-expr-validator</artifactId> <version>0.10-SNAPSHOT</version> </dependency>
Conclusion
The code to all this is still brand new, raw, unfinished, undocumented, untested, etc. This is mainly a call out for feedback and ideas. Let me know what you think.
Nice idea, I will give this a try, just one observation: is not possible for the bytecode analyzer to get the Class from modelObject.getClass() instead of specifying the class?
Comment by Daniele — January 27, 2009 @ 5:37 pm
@Daniele: doing so will push the check back into runtime which is highly undesirable. All the analyzer sees is “new Pe(…)” and it checks just that part of the bytecode.
Comment by ivaynberg — January 27, 2009 @ 5:49 pm
Igor,
I like your idea. I would definitely prefer to use such a PeModel over a PropertyModel (although I find the names Pe and PeModel too cryptic). Could it be taken a step further such that this new model would be integrated into the Wicket core, and that testing is happening automatically when running in development mode?
–Antoine
Comment by Antoine — February 21, 2009 @ 7:11 am
@Anotine:
The names are a bit cryptic, but the reason is that I want to keep them as short as possible, I got tired of typing out PropertyModel all the time :)
There is no need to do this in development mode; when your application is running, in any mode, you will already get a runtime exception. The entire point of this is to shift the error from runtime into a lower phase such as compile or test because runtime is the hardest to test.
Comment by ivaynberg — February 21, 2009 @ 8:32 pm