To show how to use Tapestry CRUD, we'll show how
the
Ars Machina Example Project was built.
We'll focus on its Project entity class.
Tapestry CRUD does not require a DAO layer, just the controller (business rules) layer. On the other hand, it's a very good practice to have a persistence layer, and that's what we'll do here.
The first step is to write the DAO: interface and implementation.
We'll use the DAO interface from
Generic DAO and our first version will be this one:
public interface ProjectDAO extends DAO<Project, Integer> {
}
Now we'll implement ProjectDAO with
Hibernate using
Generic DAO-Hibernate:
public class ProjectDAOImpl extends GenericDAOImpl<Project, Integer>
implements ProjectDAO {
public ProjectDAOImpl(SessionFactory sessionFactory) {
super(sessionFactory);
}
}
In a very similar way, we now write our controller interface and implementation, now using Generic Controller:
public interface ProjectController extends Controller<Project, Integer> {
}
To implement the controller implementation, we could have
extended Generic Controller's ControllerImpl,
but the project uses Spring transaction handling, so
we extend
Generic Controller-Spring's
SpringControllerImpl:
public class ProjectControllerImpl
extends SpringControllerImpl<Project, Integer>
implements ProjectController {
private ProjectDAO projectDAO;
public ProjectControllerImpl(ProjectDAO projectDAO) {
super(projectDAO);
this.projectDAO = projectDAO;
}
}
Note that, in this example, we have an unused
projectDAO field. It will be needed
when we add more methods to ProjectControllerImpl,
something outside the scope of this example.
Now we need to wire our objects (beans, in Spring terminology) using some Inversion of Control (IoC) container like Spring or Tapestry IoC. The example project uses Spring and JavaConfig. The latter uses Java classes, instead of XML, to configure beans. You can view the Example's implementation here.
Encoder
To fill all our application Project encoding
needs, we create an Encoder implementation.
To keep it simple, we'll use the Project's
id property (Integer) so
we can extend HibernateIntegerEncoder.
public class ProjectEncoder extends HibernateIntegerEncoder<Project> {
public ProjectEncoder(SessionFactory sessionFactory, ProjectController controller) {
super(sessionFactory, controller);
}
/**
* @see br.com.arsmachina.tapestrycrud.encoder.LabelEncoder#toLabel(java.lang.Object)
*/
public String toLabel(Project project) {
return project.getName();
}
}
Note that we use Project's name
property as its user-presentable label, but we could use anything
we want.
Now, through Tapestry-IoC, we need to add them to the
EncoderSource service. The first
step is to add the following line to the
bind(ServiceBinder binder) method
in our project's
AppModule. It makes an
ProjectEncoder instance available
as a service in Tapestry-IoC:
binder.bind(ProjectEncoder.class);
Now we need to add it to the
ControllerSource service.
This is also done in AppModule.
If the contributeControllerSource method
was not created yet, create it. Otherwise, just
add a ProjectController projectController
parameter to it:
public void contributeControllerSource(
MappedConfiguration<Class, Controller> contributions,
ProjectController projectController) {
contributions.add(Project.class, projectController);
}
SelectModelFactory
In order to easily use Project instances
in object selection components like
Select and Palette, we
need to configure a
SingleTypeSelectModelFactory.
This is done through the
contributeSelectModelFactory() method in
AppModule
Tapestry CRUD provides a class,
DefaultSingleTypeSelectModelFactory,
that can be used to easily implement
SingleTypeSelectModelFactory
without writing a specific class, just instantiating it.
public void contributeSelectModelFactory(
MappedConfiguration<Class, SingleTypeSelectModelFactory> contributions,
ProjectController projectController, ProjectEncoder projectEncoder) {
DefaultSingleTypeSelectModelFactory projectSMF =
new DefaultSingleTypeSelectModelFactory(
projectController, projectEncoder);
contributions.add(Project.class, projectSMF);
}
Now we can implement the project listing page.
We'll subclass BaseListPage to write
our ListProject page:
public class ListProject
extends BaseListPage<Project, Integer, Integer> {
@OnEvent(component = Constants.REMOVE_COMPONENT_ID, value = EventConstants.ACTION)
public Object remove(Integer id) {
return doRemove(id);
}
}
At first, our listing page shows all projects available. It does it in a paginated fashion, only fetching the projects that will be shown in this request.
The remove(Integer id) method is only needed
for listings that have a link or button for removing objects
and, for any entity class that has an Integer
as its primary key field type, this method can be copied
verbatim.
The corresponding template follows:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <body> <div t:type="Zone" t:id="zone"> <div t:type="crud/Message" t:message="message"> <p>Message here.</p> </div> <table t:type="Grid" t:source="objects" t:row="object" t:model="beanModel" t:inplace="true" t:reorder="id, name, manager, action"> <t:parameter name="actionCell"> <div t:type="crud/ActionLinks" t:object="object" t:editPage="project/edit"/> </t:parameter> <t:parameter name="empty"> <div t:type="crud/EmptyGridMessage"></div> </t:parameter> </table> <a href="#" t:type="PageLink" t:page="project/edit" t:context="null"> Create new project </a> </div> </body> </html>
Note that the BeanModel returned by
BaseListPage.getBeanModel() has an added
an action pseudo-property, so we can have
the edit and remove links or buttons in their own table
column.
Also note the use of the crud/ActionLinks component,
used to generate the edit and remove links.
The edition page is used to create new objects or
edit existing ones. It will extend BaseEditPage:
public class EditProject extends BaseEditPage<Project, Integer, Integer> {
@Mixin
@SuppressWarnings("unused")
private HibernateValidatorMixin hibernateValidatorMixin;
@Override
protected Project createNewObject() {
return new Project();
}
/**
* Loads an user given its activation context value.
*
* @param context an {@link Integer} array.
*/
public void onActivate(Integer context) {
setObjectFromActivationContext(context);
}
@Inject
private UserController userController;
public SelectModel getManagerSM() {
final List<User> users = userController.findByRole(Manager.class);
return getSelectModelFactory().create(User.class, users);
}
}
Note the use of the HibernateValidatorMixin,
from the Tapestry CRUd-Hibernate Validator package. It performs
validations defined by annotations in the entity class
(in this case, Project) automatically.
createNewObject() is an abstract method defined
in BaseEditPage that will create the new object
to be edited. It can have properties pre-filled if needed.
On the other hand, the onActivate(Integer context)
is not abstract, but most be implemented in order of
BaseEditPage to be able to edit existing objects.
This implementation can be copied verbatim for other pages
that edit entities whose activation context has type Integer.
This two methods are the only one needed. Other ones can be added if needed.
In this example, note how simple and short is
the use of SelectModelFactory in the
getManagerSM method. If every
User instance could be a valid project manager,
getManagerSM would simply return
getSelectModelFactory().create(User.class).
Finally, the template. I could have used BeanEditForm
or BeanEditor as well:
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"> <body> <div t:type="Zone" t:id="zone" xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"> <form t:type="Form" t:id="form" t:zone="prop:zone" accept-charset="iso-8859-1"> <div t:type="crud/Message" t:message="message"> Messages here. </div> <div t:type="Errors" t:id="errors" /> <label t:type="crud/ImprovedLabel" for="name">Nombre</label> <input t:type="TextField" t:id="name" t:value="object.name" t:validate="required" /> <br /> <label t:type="crud/ImprovedLabel" for="description">Descripción</label> <input t:type="TextField" t:id="description" t:value="object.description" /> <br /> <label t:type="crud/ImprovedLabel" for="manager">Gerente</label> <select t:type="Select" t:id="manager" t:value="object.manager" t:model="managerSM" t:validate="required" t:blankOption="always"/> <br /> <input type="submit" /> </form> </div> <br/> <a href="#" t:type="crud/NewObjectLink"> Create new project </a> <br/> <a href="#" t:type="PageLink" t:page="project/list"> Back to listing page </a> </body> </html>