Wednesday, January 7, 2009

Introducing Pojomatic

New year, new project
I'd like to start off the new year by introducing a project I've been working on for a while now along with Ian Robertson, a colleague of mine at Overstock. The project itself is actually not new at all, considering we began working on it around April of last year and it's based on something similar we've been using internally at Overstock since the early days of Java 1.5. The project is called Pojomatic, and what's new is that it's open source (Apache 2.0 license) and there's a release candidate available (1.0-RC1) on Sourceforge or the Maven central repository.

What does it do?
Pojomatic is a Java library which provides automatic and configurable implementations of the hashCode() equals(Object) and toString() methods inherited from java.lang.Object using annotations. POJOs (Plain Old Java Objects) + automatic implementations of common methods = Pojomatic.

This is a useful because it is generally a good idea to override hashCode() equals(Object) and sometimes toString(). One could manually implement these methods, but that is time-consuming and prone to error (e.g. forgetting to check for null everywhere). Instead, one could have an IDE generate implementations of these methods for you. As with a lot of generated code, this can be like slapping your code with an ugly stick. Besides, I'd often add fields and/or methods to the class later and forget to re-generate new implementations, which may have no effect or may lead to very subtle, hard to detect bugs (e.g. two objects are equal when they shouldn't be => two different people mistaken for the same person => money is deposited to the wrong account => one person is happy, while you are not because you have to work late to track down and fix the bug).

How do I use it?
The easiest way to use Pojomatic is to put one annotation (@AutoProperty) on your class and delegate the desired method(s) to the corresponding static methods in Pojomatic. For example:
@AutoProperty
public class Person {
private final String firstName;
private final String lastName;
private final int age;

public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

public String getFirstName() {
return this.firstName;
}

public String getLastName() {
return this.lastName;
}

public int getAge() {
return this.age;
}

@Override public int hashCode() {
return Pojomatic.hashCode(this);
}

@Override public String toString() {
return Pojomatic.toString(this);
}

@Override public boolean equals(Object o) {
return Pojomatic.equals(this, o);
}
}

By default, @AutoProperty tells Pojomatic to automatically detect all of your fields and use them in all of the Pojomatic.* methods. We'll see the different options later, but in the above example, all of the fields will be used for hashCode() equals(Object) and toString() as shown here:
public static void main(String[] args) {
Person johnDoe = new Person("John", "Doe", 32);
System.out.println(johnDoe.hashCode());
System.out.println(johnDoe.equals(new Person("John", "Doe", 32)));
System.out.println(johnDoe.toString());
}

Outputs:
-2068529904
Person{firstName: {John}, lastName: {Doe}, age: {32}}
true

Using all fields for each of hashCode() equals(Object) and toString() is usually not advised, however, so @AutoProperty can be configured to include automatically detected properties in any (valid) combination of these methods (including something in hashCode() without including it in equals(Object) violates the contract of hashCode(), so this is not allowed). Additionally, properties can be configured individually via the @Property annotation. When both annotations are present, @Property is used since it applies only to the one property. Using @Property gives you complete control and makes @AutoProperty optional. A common practice is to have all properties included in equals(Object) and toString(), while only one or two key properties are included in hashCode() like so:
@AutoProperty(policy=DefaultPojomaticPolicy.EQUALS_TO_STRING)
public class Book {

@Property(policy=PojomaticPolicy.ALL)
private final String isbn;

private final String title;

public Book(String isbn, String title) {
super();
this.isbn = isbn;
this.title = title;
}

public String getIsbn() {
return isbn;
}

public String getTitle() {
return title;
}

@Override public int hashCode() {
return Pojomatic.hashCode(this);
}

@Override public String toString() {
return Pojomatic.toString(this);
}

@Override public boolean equals(Object o) {
return Pojomatic.equals(this, o);
}
}
Both @AutoProperty and @Property can use accessor methods (getters) instead of fields in situations where using accessors is more desirable or when a SecurityManager prevents Pojomatic from accessing private fields through reflection.

Briefly, there is also a feature which will let you customize the String representation of each property. For example, this would be useful if one of your properties contains sensitive data such as an account number, credit card number or social security number (see AccountNumberFormatter). Pojomatic provides the ability for you to define your own formatter implementations as well.

Feedback
Pojomatic has been a lot of fun to work on, and I hope you will find it useful. I'm confident that after trying it out, you will not want to go back to handcrafting equals methods or using ugly IDE-generated code instead. Any feedback is appreciated, as well as feature requests, so what do you think?

2 comments:

  1. Hi Chris,
    I have been playing around with pojomatic. Great works and many thanks. When do you think it will released as 1.0

    ReplyDelete
  2. @Eamonn: Glad to hear it.

    I think a 1.0 release will happen within a month or two, but there are a couple of things to be decided. One is whether or not annotations on interfaces should be picked up. Also, I would like it to be easy to add functionality by being able to process properties yourself, but this may not end up being a 1.0 feature. Obviously, 1.0 will come faster the less changes we decide to make.

    The good news is that neither of these possible changes would cause much (or any) change to the existing API (or functionality for that matter), if that is your concern. No guarantees, but I don't see us changing the API from here on out.

    ReplyDelete