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)
Hey Nicklas thankyou very much for the great tutorial, realy enjoying it!
I was looking at the criteria part, and I think it is just too much work to build a simple select with jpa criteria, ok, we got type safe goodies but people will be really scared of changing their sqls...
keep up with the next part
thanks!
Fortunately you can still use JPA 1 style string-based queries if you like.
My general rule of thumb for queries:
In this tutorial Nicklas could have used a named query with the meta model but then that wouldn't have illustrated how to get started using the CriteriaBuilder api.
Good Series. Someone should put together a full Java EE 6 index which references these as a general tutorial, have links to tutorials and guides for all of the major pieces of Java EE 6: JPA 2, JSF 2, CDI, Bean validator, etc. As well as references to recommended Maven archetypes for Java EE 6 projects.
For whom will run into...
Container org.jboss.web.tomcat.service.TomcatInjectionContainer failed to resolve persistence unit null
the persistence.xml should be located in src/main/resources/META-INF/
Btw, thumbs up, Nicklass!
Ah yes good catch on the META-INF. Corrected it.
Since it's not there in Seam3 yet (and this is EE6, not Seam3) couldn't the transaction begin, join and commit be done using a CDI interceptor or possibly decorator? If so, it should probably be added at some point.
Yep. Most people would probably use EJB CMT or some Seam-like transaction interceptor for the job
"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."
Looks like this is by design and can be configured, as per the JSF2 spec (section 11.1.3) via a couple of context-params in your web.xml, e.g.:
<context-param>
<param-name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
<param-value>true</param-value>
</context-param>
A related parameter is javax.faces.VALIDATE_EMPTY_FIELDS