Ars Machina
Software development isn't just technique, it is an art too

Usage

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.

Creating the DAO (optional step)

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);
	}

}				

Creating the controller

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.

Dependency injection

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.

Creating and configuring the 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);
}				

Configuring the 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);
		
}				

Writing the listing page

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.

Writing the edition page

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>