Dynject is a data normalization, validation and design by contract framework designed for Java 5 and higher, with integrated support for JPA, Swing/AWT, Struts. I wrote it because I was getting RSI due to typing over and over again the same stuff, and existing validation didn't satisfy me enough (NIH syndrome) and were always missing some "obvious" (for me) showstopper functionality.
The purpose of Dynject is twofold : data validation using declarative constructs in the source code and data normalization. I'll illustrate with an example - a Customer bean that has been annotated with both normalization and validation annotations:
public class Customer {
@Normalized(
whitespace=WhitespaceProcessing.COLLAPSE,
trim=TrimType.BOTH,
transform=TransformType.UPPERCASE
)
@Length(
min=3,
max=20,
message="Customer length must be between 3 and 20 characters"
)
public String name;
@IsDate(value="dd/MM/yyyy", messageKey="customer.birthdate")
private String birthdate;
@MinInclusive(value=0,messageKey="customer.low.discount")
@MaxInclusive(value=20,messageKey="customer.high.discount")
private int discount;
}
First, note that we are not only validating here : we're stating that the name must be stored using some specific transformations (trimmed, uppercased, etc.), and there is an @IsDate annotation tht specifies that birthdate must contain a date, but in a specific format.
Also, note that you can attach error messages to failed validations. These messages can either be external resources or literal messages. Of course, Dynject provides default messages for all validations.
So now that you have the annotated bean, how do you use it?
You can perform assignments to your POJO using a variety of external sources, and you can also perform after-the-fact validations of your instances

a) To assign a value to a single field, simply call
ErrorMap errors = Shell.assign(customer,"name"," John Doe" ); // "JOHN DOE" will be stored
b) To assign a value to all fields simultaneously, simply call
ErrorMap errors = Shell.assign(customer, someValueSource);
where "someValueSource" is some of the classes that provide values for properties.
c) To validate an instance
ErrorMap errors = Shell.validate(customer);
In all scenarios, an ErrorMap will be returned, which is a multimap that maps property names to lists of validation errors.
Dynject performs many automatic type conversions "intelligently". This is useful if the data you want to assign to the field doesn't come in a "pefect" data type and format. For example, data coming from an HTTP request are always Strings
In Dynject, it's perfectly valid to do:
Shell.assign(customer,"discount","3");
Shell.assign(customer,"birthdate", new GregorianCalendar(1973,10,3));
// "03/11/1973" will be stored
Shell.assign(customer,"birthdate", System.currentTimeMillis()-1000L*60*60*24*365*32); // Now - 32 years
Shell.assign(customer,"discount", 3.0);
// After all, 3.0 is a mathematical integer, so no problem storing it in an int.
Shell.assign(customer,"discount", 3.1);
// This one fails, though.
Shell.assign(customer,"discount", new BigInteger("4"));
Note that, even though the discount property is an integer, you can assign to it any kind of data type, as long as the "value" is an integer.
Dynject supports JPA / Hibernate and understands annotations like @Basic, @Column, @Id, @OneToOne and the relevant attributes( optional, precision, scale, length ) and validates those fields accordingly.
Click here for a detailed description of the provided JPA Support.
Dynject supports Struts 1.x and can validate annotated ActionForms with a single line. Create an ActionForm (say CustomerActionForm), place whatever validating or normalization annotations you want and simply call:
ActionErrors errors = StrutsUtil.validate(customer);
Click here for a full example
Most annotations can be attached to a class or to a package. This is very useful for data normalization annotations. For example:
@Normalized(whitespace=WhitespaceProcessing.COLLAPSE,trim=TrimType.BOTH)
public class MyActionForm extends ActionForm {
...
}
would trim leading and trailing spaces from all properties.
If you are using Servlets or a different Web framework, you can still take advantage of Dynject. Annotate your class, then simply do:
Customer customer = new Customer(); ErrorMap errorMap = Shell.assign(customer, new HttpRequestSource(request));
Click here for a complete example of the HTTP Support in Dynject
Dynject can also be used in desktop/thick client applications. In a typical scenario, the user is presented with a frame that allows him to inspect or edit the contents of a particular bean, using a series of components (text fields, spinners, scrollbars, combos, whatever). Once the user has finished editing, the usual procedure is to validate the data, then to move it to the appropriate bean. Well, Dynject takes care of this automatically. If you name your components appropriately, all you'd need to do is:
Customer customer = new Customer(); ErrorMap errorMap= Shell.assign(customer, new SwingSource(swingForm));
Dynject automaticall picks the appopriate property for each component : for TextFields it calls getText(), for spinners and scrollbars it calls getValue(), and so forth.
Click here for a detailed description and a complete example about Swing/AWT support in Dynject.
All annotations can be defined at interface level, and are "inherited" automatically by concrete classes. This applies both to validating annotations and to design by contract annotations. So, for example, the following works as expected:
public interface IConstrained {
@Require("__this.y == 8")
public int sum(@MaxInclusive(200) @MinInclusive(50) int a, @MinInclusive(3) int b);
}
public class Foo implements IConstrained {
public int sum(int a, int b) { .. }
}
The sum method in Foo automatically inherits all constraining annotations specified by the IConstrained interface.
Constraints can be "overridden" locally just like any ordinary overriding works. Click here for a detailed description about how dynject handles inheritance and interfaces.
One of the most interesting features of Dynject are db-aware (database aware) annotations, that transform or validate data on the fly using a backing database. Database access can be performed using JDBC connections, data sources or JPA EntityManagers or EntityManagerFactories.
Again, an example is helpful. Imagine that, in a web application that uses an ORM engine, the user is presented a list of customers and requested to select one. The usual approach would be to transmit the Primary Key of the selected customer to the server-side (Servlet, Action or whatever), then validate this parameter and look up the corresponding entity using the ORM engine, raising an error if no entity was found. Well, this can be completely automatec with something as simple as:
public class SomeClass {
@IsEntity(ifNotFound=NotFoundPolicy.ADD_ERROR)
private Customer customer;
}
Now if you do the previous Shell.assign(...) assignment, the system will automatically fetch the value of the "customer" HTTP parameter, consider it as a primary key, use whatever means of obtaining an EntityManager you have configured, locate the entity and store it in the field, or if not found, do whatever you instruct it to do (in the example - add an error message). Dynject can take advantage of the @PersistenceUnit and @PersistenceContext annotations if you are running under a JPA compliant container, or it can be configured to fetch EntityManager(Factory) from fields, or using a JNDI lookup.
Another interesting db-aware annotation is @DBLookup, which performs value translation based on an underlying table or entity. For example, if you use the following annotation:
@DBLookup(entityName="country",searchIn="id", replaceWith="name") private String country;
Now when you assign the value "us" to country, "United States" will be stored instead.
Click here for a detailed description of db-aware annotations.
If you use AspectJ in your projects, Dynject comes with an extensible aspect that allows all of the above to happen automatically. So using our customer example, you wouldn't even need to call Shell.assign. Rather, you use your fields/methods normally:
customer.name = " John Doe "; (or, customer.setName(" John Doe "); )
System.out.println(customer.name); // prints "JOHN DOE"
When using desing-by-contract you can apply normalization and validation annotation to parameters, and you can use the @Require and @Ensure annotations to specify method pre and postconditions (Dynject follows Bertrand Meyer's and Eiffel's terminology)
@Require("__this.y < 8")
@Ensure("__this.x == 8")
public String foo(@Required String bar, @MinInclusive(3) @MaxInclusive(200) int b) {
...
}
(the above examples use JavaScript for validation, but you can use any JSR 223 scripting engine)
Dynject comes with over 40 built-in annotations, ranging from simple validations like @Length (which validates the length of a string) to ones like @ScriptValidation, which allows validation to be performed using any Java-compliant scripting engine. Of course, you have also business validations like @ISBN, @ISSN, @IBAN, @EAN, @CreditNumber, @Luhn, etc.
To make them easy to remember, many of the primtitive validation annotations are named after the restricive facets of XML Schema, so if you have been doing schemas you'll feel at home. If not, the names are anyway easy enough : @MinInclusive, @MaxFractionDigits, @MinDigits, @Length, @Pattern, and so forth.
Writing custom data normalizers, converters and validators is extremely easy (IMHO).
I've tried to test Dynject as much as possible. There are over 400 unit tests that achieve 100% class and method coverage and about 97% code coverage, with the remaining bits being impossible to test because they correspond to exceptions that "can't happen" or typical ( if foo != null try close catch ) idioms.
Dynject is being released under LGPL 3 and has no dependancies on other libraries. Get it now, give it a spin and share your impressions. Suggestions, bug reports, etc. as always are very welcome. cp insults /dev/nul