Asserting generic implementations in Java

A while back I ended up creating a simple interface like Predicate<T> as follows:

public interface Predicate<T> {
  boolean evaluate(T state);
}

Now when you create a bean that requires a predicate on some object, you’d code a setter for it:

package org.example.foos;

public class Foo {
  private Predicate<Bar> guardPredicate;

  public void operate(Bar bar) {
    if (!guardPredicate.evaluate(bar)) {
      throw new IllegalStateException();
    }

    // do stuff with bar
  }

  public void setGuardPredicate(Predicate<Bar> guardPredicate) {
    this.guardPredicate = guardPredicate;
  }
}

And embed this bean in your Spring ApplicationContext like:

<beans ...>
	<bean class="org.example.foos.Foo">
		<property name="guardPredicate">
			<bean class="org.example.foos.support.SomeBarPredicate" />
		</property>
	</bean>
</beans>

From the name of “SomeBarPredicate” you’d expect it to implement Predicate<org.example.foos.Bar>, but there’s no guarantee on that; an implementation of Predicate<java.lang.String> could be passed in as well. There’s no automatic runtime checking because of type erasure.

Class metadata to the rescue!

Whenever your class implements or extends a generic interface or class, this data is recoverable through Class.getGenericInterfaces() or Class.getGenericSuperclass().

Getting the information out is rather painful tough, and I’m not going to re-iterate the examples out there for this. For one, please see Reflecting generics by Ian Robertson.

Luckily Spring 3.0 has a utility class for this: GenericTypeResolver.

For our example, the setter should be rewritten as:

  public void setGuardPredicate(Predicate<Bar> guardPredicate) {

	Class<?> param = GenericTypeResolver.resolveTypeArgument(guardPredicate.getClass(), 
		Predicate.class);
	if (param != null // if null, it was unrecovarable, treat it as Class<Object>
		  && !param.isAssignableFrom(Bar.class)) {
		throw new IllegalArgumentException();
	}
	this.guardPredicate = guardPredicate;
  }

So lets test this with JUnit 4 and Spring testing utitilities:

package org.example.foos;

import static org.junit.Assert.fail;

import org.junit.Test;
import org.springframework.test.util.ReflectionTestUtils;

public class FooTest {

	private Foo foo = new Foo();
	
	@Test
	public void testSetGuardPredicate() {
		setGuardPredicateScenario(new Predicate<Bar>() {
			public boolean evaluate(Bar state) {
				return state.toString().contains("Bar");
			}
		});
	}
	
	@Test(expected=IllegalArgumentException.class)
	public void testSetGuardPredicateWithWrongTyped() {
		setGuardPredicateScenario(new Predicate<String>() {
			public boolean evaluate(String state) {
				return state != null && !state.isEmpty();
			}
		});
	}
	
	@SuppressWarnings("rawtypes")
	@Test
	public void testSetGuardPredicateWithUntyped() {
		setGuardPredicateScenario(new Predicate() {
			public boolean evaluate(Object state) {
				return state != null;
			}
		});
	}
	
	private void setGuardPredicateScenario(Object predicate) {
		ReflectionTestUtils.invokeSetterMethod(foo, "guardPredicate", 
				predicate, Predicate.class);
		foo.operate(new Bar());
	}
}

Voila, tests pass. Be sure to test it without our assertions as well; look at “testSetGuardPredicateWithWrongTyped” result to see a not so nice ClassCastException:

java.lang.Exception: Unexpected exception, expected<java.lang.IllegalArgumentException> but was<java.lang.ClassCastException>
Caused by: java.lang.ClassCastException: org.example.foos.Bar cannot be cast to java.lang.String
	at org.example.foos.FooTest$2.evaluate(FooTest.java:1)
	at org.example.foos.Foo.operate(Foo.java:11)
	at org.example.foos.FooTest.setGuardPredicateScenario(FooTest.java:49)
	at org.example.foos.FooTest.testSetGuardPredicateWithWrongTyped(FooTest.java:27)

With assertions we catch that configuration problem in earlier refresh stage.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: