In this third part we'll be hooking up JPA 2 with the static metamodel generator, Bean Validation and Envers
JPA
Let's get persistent. When we're talking persistence, we need a persistence.xml so let's make a folder META-INF src/main/resources and create one there
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="Greetings"> <jta-data-source>java:/DefaultDS</jta-data-source> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" /> <property name="hibernate.hbm2ddl.auto" value="create-drop" /> <property name="hibernate.show-sql" value="true" /> </properties> </persistence-unit> </persistence>
We're using the standard HSQL DefaultDS that comes with JBoss AS 6. If you want to be really hip, google around for @DataSourceDefinition which is a new kid on the block in EE 6 (haven't tried if AS 6 supports it yet, though)
Next, let's expand our model from Strings to a Greeting entity. Create a
package com.acme.greetings; import static javax.persistence.TemporalType.TIMESTAMP; import java.util.Date; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Temporal; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Entity public class Greeting { @Id @GeneratedValue int id; String text; @Temporal(TIMESTAMP) Date created = new Date(); public int getId() { return id; } public void setId(int id) { this.id = id; } public String getText() { return text; } public void setText(String text) { this.text = text; } }
change the GreetingBean to
package com.acme.greetings; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import javax.annotation.PostConstruct; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; import javax.inject.Inject; import javax.inject.Named; import org.icefaces.application.PushRenderer; @ApplicationScoped @Named public class GreetingBean implements Serializable { Greeting greeting = new Greeting(); List<Greeting> greetings = new ArrayList<Greeting>(); @Inject @Added Event<Greeting> greetingAddedEvent; @Inject GreetingArchiver greetingArchiver; @PostConstruct public void init() { greetings = greetingArchiver.loadGreetings(); } public void addGreeting() { greetings.add(greeting); greetingAddedEvent.fire(greeting); greeting = new Greeting(); PushRenderer.render("greetings"); } public Greeting getGreeting() { return greeting; } public void setGreeting(Greeting greeting) { this.greeting = greeting; } public List<Greeting> getGreetings() { PushRenderer.addCurrentSession("greetings"); return greetings; } public void setGreetings(List<Greeting> greetings) { this.greetings = greetings; } }
We have also injected an event that is fired when comments are added:
@Inject @Added Event<Greeting> greetingAddedEvent;
so we need a qualifier called Added:
package com.acme.greetings; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.inject.Qualifier; @Qualifier @Retention(RUNTIME) @Target( { METHOD, FIELD, PARAMETER, TYPE }) public @interface Added { }
and greetings.xhtml to
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:ice="http://www.icesoft.com/icefaces/component" xmlns:ui="http://java.sun.com/jsf/facelets"> <h:head> <title> Greetings </title> </h:head> <h:body> <h:form> <ice:inputText value="#{greetingBean.greeting.text}" effect="#{greetingBean.appear}"/> <h:commandButton value="Add" action="#{greetingBean.addGreeting}" /> <h:dataTable value="#{greetingBean.greetings}" var="greeting"> <h:column> <h:outputText value="#{greeting.text}"/> </h:column> </h:dataTable> </h:form> </h:body> </html>
(we changed the order of the table and the input fields as it was getting annoying to have the field and button move down as we add comments)
Of course since we are firing events it would be nice if someone is actually listening. Let's create a GreetingArchvier:
package com.acme.greetings; import java.util.List; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @ApplicationScoped public class GreetingArchiver { @PersistenceContext EntityManager db; @Inject UserTransaction userTransaction; public void saveGreeting(@Observes @Added Greeting greeting) { try { userTransaction.begin(); db.joinTransaction(); db.persist(greeting); db.flush(); userTransaction.commit(); } catch (Exception e) { e.printStackTrace(); // The recommended way of dealing with exceptions, right? } } public List<Greeting> loadGreetings() { return db.createQuery("from Greeting").getResultList(); } }
That observes @Added Greetings and store them to the database. Notice also the loadGreetings() method that GreetingBean calls in it's @PostConstruct to populate itself with old comments. Well, with create-drop in our persistence.xml there won't be much to load but let's fix that later.
JPA 2
That's all nice but we're trying to be livin' on the edge so let's bring in JPA and typesafe queries and with those we better have some static metamodel generator, otherwise the attributes will quickly become a burden. There is Eclipse integration available (google around) but if you're doing automated maven-based builds, you're going to need this anyway. Since both Eclipse and Maven are involved in building, be prepared for some chicken-egg-project-cleaning-and-refreshing in Eclipse from time to time when adding new entities. Anyways, open up pom.xml and add some plugin repositories:
<pluginRepositories> <pluginRepository> <id>jfrog</id> <url>http://repo.jfrog.org/artifactory/plugins-releases/</url> </pluginRepository> <pluginRepository> <id>maven plugins</id> <url>http://maven-annotation-plugin.googlecode.com/svn/trunk/mavenrepo/</url> </pluginRepository> </pluginRepositories>
The maven-compiler-plugin will need an argument to not process annotations automagically once we slap jpamodelgen on the classpath
<configuration> <source>1.6</source> <target>1.6</target> <compilerArgument>-proc:none</compilerArgument> </configuration>
and the job should be taken over by our new build-plugins:
<plugin> <groupId>org.bsc.maven</groupId> <artifactId>maven-processor-plugin</artifactId> <version>1.3.5</version> <executions> <execution> <id>process</id> <goals> <goal>process</goal> </goals> <phase>generate-sources</phase> <configuration> <outputDirectory>target/metamodel</outputDirectory> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-jpamodelgen</artifactId> <version>1.0.0.Final</version> </dependency> </dependencies> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>1.3</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>target/metamodel</source> </sources> </configuration> </execution> </executions> </plugin>
Dan Allen thinks this is a lot of configuration for this task, I'll have to remember to ask if he ever got his simple, elegant solution to place the artifacts in usable places ;-)
Run mvn eclipse:eclipse to have the target/metamodel added to eclipse and do the project level Maven, refresh project configuration.
Run the maven build and you should see the Greeting_ class appear in target/metamodel and in the WAR structure. Now let's bring it into use:
First we add EntityManger/EntityManagerFactory producers (the recommeded CDI way of wrapping them)
package com.acme.greetings; import javax.enterprise.inject.Produces; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.PersistenceContext; import javax.persistence.PersistenceUnit; public class DBFactory { @Produces @GreetingDB @PersistenceContext EntityManager entityManager; @Produces @GreetingDB @PersistenceUnit EntityManagerFactory entityManagerFactory; }
We also need a qualifier for that
package com.acme.greetings; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.inject.Qualifier; @Qualifier @Retention(RUNTIME) @Target( { METHOD, FIELD, PARAMETER, TYPE }) public @interface GreetingDB { }
Finally, let's modify or GreetingArchiver:
package com.acme.greetings; import java.util.Date; import java.util.List; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.ParameterExpression; import javax.persistence.criteria.Root; import javax.transaction.UserTransaction; @ApplicationScoped public class GreetingArchiver { @Inject @GreetingDB EntityManager db; @Inject UserTransaction userTransaction; CriteriaQuery<Greeting> loadQuery; ParameterExpression<Date> timestampParam; @Inject public void initQuery(@GreetingDB EntityManagerFactory emf) { CriteriaBuilder cb = emf.getCriteriaBuilder(); timestampParam = cb.parameter(Date.class); loadQuery = cb.createQuery(Greeting.class); Root<Greeting> greeting = loadQuery.from(Greeting.class); loadQuery.select(greeting); loadQuery.where(cb.greaterThan(greeting.get(Greeting_.created), timestampParam)); } public void saveGreeting(@Observes @Added Greeting greeting) { try { userTransaction.begin(); db.joinTransaction(); db.persist(greeting); db.flush(); userTransaction.commit(); } catch (Exception e) { e.printStackTrace(); // The recommended way of dealing with exceptions, right? } } public List<Greeting> loadGreetings() { Date tenMinutesAgo = new Date(); tenMinutesAgo.setTime(tenMinutesAgo.getTime() - 10 * 60 * 1000); return db.createQuery(loadQuery).setParameter(timestampParam, tenMinutesAgo).getResultList(); } }
Bean Validation
Adding Bean Validation is a breeze, just stick the annotations on the entity fields in Greeting:
@Size(min = 1, max = 50) String text;
and attach a message to the input field in greetings.xhtml:
<ice:inputText id="feedback" value="#{greetingBean.greeting.text}" effect="#{greetingBean.appear}"/> <h:message for="feedback" />
I tried placing a @NotNull on text but it still failed on submit because the values came in as empty string (might be this is the designed behavior) so I used min = 1 instead.
Envers
If you would like to have auditing on your entities, you need to add the Envers dep to pom.xml
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> <version>3.5.1-Final</version> <scope>provided</scope> </dependency>
I cheated a little and marked it provided
as it was pulling in a lot of deps. I downloaded the envers.jar and dropped it in the server
common lib with it's other hibernate buddy-jar:s. After that we can stick an annotation on the entity
@Entity @Audited public class Greeting
Last but not least to enjoy automatic data auditing we need to add the Envers listeners to persistence.xml
<property name="hibernate.ejb.event.post-insert" value="org.hibernate.ejb.event.EJB3PostInsertEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-update" value="org.hibernate.ejb.event.EJB3PostUpdateEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-delete" value="org.hibernate.ejb.event.EJB3PostDeleteEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-update" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-remove" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-collection-recreate" value="org.hibernate.envers.event.AuditEventListener" />
This concludes part II, next time we'll be looking at EJB:s and MDB:s in more details. And perhaps abandoning our WAR-only utopia for now. Guess I'll have to learn about the maven EJB plugin. Hints for good tutorials accepted.
PS. Have you checked the size of the WAR file after you've taken all these technologies into use? Around 320k. And of those ~85% are the ICEfaces libs (the only external deps)