Basic CRUD with Tapestry 5
Introduction
This tutorials aims to present how to make a simple Crud cycle in tapestry 5.
Before reading this tutorial, you should read the Howard Lewis Ship's tutorials as we're going to reuse parts of it without re-explaining all the concepts they introduce.
In order to stay focused on designing the CRUD, we're not going to deal with a database or worry about transactions. If you want to work on a tutorial that deals with this question in great details you should read this one carefully.
It's good to understand how one would go about implementing a basic CRUD but there's no reason every Tapestry user would have to rewrite the same code for CRUD functionality. For a pre-packaged open-source Tapestry 5 solution, see tapestry-model from Tynamo project.
Create the project
To create the project and set up all the dependencies we use maven :
mvn archetype:create -DarchetypeGroupId=org.apache.tapestry -DarchetypeArtifactId=quickstart -DarchetypeVersion=5.0.5 -DgroupId=org.apache.tapestry -DartifactId=basicCrud -DpackageName=org.apache.tapestry.tutorial.basiccrud
Now let's move in the project and and make it available in Eclipse
cd basicCrud mvn eclipse:eclipse
Import the project in Eclipse : File -> Import -> General -> Existing Project Into Workspace Browse to the folder basicCrud and click finish.
Now we are ready to work!
Create the model
Create in the package org.apache.tapestry.tutorial.basiccrud.model our model class, as you can see I made it simple :
package org.apache.tapestry.tutorial.basiccrud.model;
public class MyBean {
private Long id;
private String name;
public MyBean(){
//nothing to do
}
public MyBean(String name){
this.name = name;
}
public MyBean(String name, Long id){
this.name = name;
this.id = id;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return id + " : " + name;
}
}- The id is going to identify a bean
- The name is just a property we will manipulate with the CRUD. Of course you can expand this later!
Create the Service
As explained earlier, we're only going to focus on the CRUD process, thus we're going to make a very simple BeanManager that will hold a list of MyBean in memory. This service is going to be injected (more on this later) as a Singleton. Thus, as long a your application is deployed the list is still in memory, but every time you restart the server the list is reinitialized. For this tutorial we're not going to implement persistence between server reboots.
To make a service we need an interface and an implementation. If you wonder why we can't just use an implementation, you should read this. Additionally, this is a good habit when used judiciously.
In org.apache.tapestry.tutorial.basiccrud.services.manager create the interface BeanManager
package org.apache.tapestry.tutorial.basiccrud.services.manager;
import java.util.List;
import org.apache.tapestry.tutorial.basiccrud.model.MyBean;
public interface BeanManager {
public abstract List<MyBean> getMyBeans();
public abstract void setMyBeans(List<MyBean> myBeans);
public void delete(Long id);
public abstract void save(MyBean myBean);
public MyBean getMyBean(Long id);
}In the same package make the implementation
package org.apache.tapestry.tutorial.basiccrud.services.manager;
import java.util.ArrayList;
import java.util.List;
import org.apache.tapestry.tutorial.basiccrud.model.MyBean;
public class BeanManagerImpl implements BeanManager {
private List<MyBean> myBeans = new ArrayList<MyBean>();
public List<MyBean> getMyBeans() {
return myBeans;
}
public void setMyBeans(List<MyBean> myBeans) {
this.myBeans = myBeans;
}
public void delete(Long id){
MyBean myBean = getMyBean(id);
getMyBeans().remove(myBean);
}
public void save(MyBean myBean){
if(myBean.getId()!=null){
//update
myBeans.set(myBeans.indexOf(myBean), myBean);
}else{
myBean.setId(new Long(myBeans.size()));
myBeans.add(myBean);
}
}
public MyBean getMyBean(Long id){
for(MyBean myBean : myBeans){
if (myBean.getId().equals(id)){
return myBean;
}
}
return null;
}
@Override
public String toString() {
return myBeans.toString();
}
}
Inject the service
Injecting a service in Tapestry is really easy, just add a bind method to your AppModule.java
in org.apache.tapestry.tutorial.basiccrud.services.AppModule.java add this method:
public static void bind(ServiceBinder binder)
{
binder.bind(BeanManager.class, BeanManagerImpl.class);
}Now the BeanManager is available in any tapestry component through the @Inject annotation.
More explaination on binding service interfaces to service implementations
Display a list of beans in the Start page
Change Start.java to display a list of beans and handle the delete action.
package org.apache.tapestry.tutorial.basiccrud.pages;
import java.util.List;
import org.apache.tapestry.annotations.Inject;
import org.apache.tapestry.annotations.InjectPage;
import org.apache.tapestry.tutorial.basiccrud.model.MyBean;
import org.apache.tapestry.tutorial.basiccrud.services.manager.BeanManager;
/**
* Start page of application basicCrud.
*/
public class Start{
@Inject
private BeanManager beanManager;
private MyBean myBean;
public MyBean getMyBean() {
return myBean;
}
public void setMyBean(MyBean myBean) {
this.myBean = myBean;
}
public List<MyBean> getMyBeans(){
return beanManager.getMyBeans();
}
public boolean isEmptyList() {
return beanManager.getMyBeans().isEmpty();
}
public void onActionFromDelete(Long id){
beanManager.delete(id);
}
}- We inject the beanManager service through the @Inject annotation. Note that we can only inject into page/component classes, because Tapestry IOC will handle the necessary transformations. Classes outside of these directories will not be transformed, and thus the injected object will be null.
- onActionFromDelete removes the bean from the list but return nothing as we stay on the same page. The list is only refreshed. (You could also return null, which will keep you on the same page)
- onActionFromDelete will be called each time the component actionLink having an id "delete" is clicked. (We're talking about the "id" field of the actionLink, not the Bean Id number that's passed to it) Change Start.html in the WEB-INF directory
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd"> <head> <title>basicCrud Start Page</title> </head> <body> <h1>basicCrud of MyBean</h1> <p><t:pagelink t:page="Save" context="0">Create a new MyBean</t:pagelink></p> <t:if test="emptyList"> <p>There's no MyBeans in the list</p> <t:parameter name="else"> <t:loop source="myBeans" value="myBean"> ${myBean.name} <t:pagelink t:page="Save" context="myBean.id">Edit</t:pagelink> <t:actionlink t:id="delete" context="myBean.id">Delete</t:actionlink> <br/> </t:loop> </t:parameter> </t:if> </body> </html>There's two pageLinks
One actionLink
- Both initialize their context using the id of the bean
Edit or create a bean
To create or edit a bean we'll use Save.java :
package org.apache.tapestry.tutorial.basiccrud.pages;
import org.apache.tapestry.annotations.Inject;
import org.apache.tapestry.annotations.InjectPage;
import org.apache.tapestry.annotations.Persist;
import org.apache.tapestry.tutorial.basiccrud.model.MyBean;
import org.apache.tapestry.tutorial.basiccrud.services.manager.BeanManager;
public class Save {
private MyBean myBean;
private Long id;
@Inject
private BeanManager beanManager;
@InjectPage
private Start start;
public void onActivate(Long id){
if(id.equals(0L)){
myBean = new MyBean();
}else{
myBean = beanManager.getMyBean(id);
}
this.id = id;
}
public Long onPassivate(){
return id;
}
public Object onSubmit(){
beanManager.save(myBean);
return start;
}
public MyBean getMyBean() {
return myBean;
}
public void setMyBean(MyBean myBean) {
this.myBean = myBean;
}
}onActivate and onPassivate are part of the page navigation
- onActivate will use the context to initialize values in the Save page component.
- onPassivate is used when no context is explicitly provided in a pageLink for the Save page
- When we hit Save page the first time using a pageLink :
- onActivate is called first, myBean is initialized.
- onPassivate is called, tapestry use the returned value to persist it
- When we filled the form and submit it
- onActivate is called again but this time tapestry provide the argument obtained from onPassivate before.
- onSubmit is called
And Save.html
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>basicCrud edit Page</title>
</head>
<body>
<t:form>
<t:errors/>
<t:label t:for="name"/>
<input t:type="textfield" t:id="name" t:value="myBean.name"/><br/>
<br/>
<input type="submit"/>
</t:form>
</body>
</html>
Try it
now try it :
mvn clean compile jetty:run
And just navigate to http://localhost:8080 you'll find your way ;-).
Conclusion
I made this tutorial because I needed one and thought that I probably was not the only one. Thanks to the users of the mailing list that point on weakness and help me to make it better.
Credit to Michael Courcy for taking the time to write this tutorial.