In this blog post I'll show how we can implement a simple "Todo" management application using SAPUI5 front-end which communicates with a JAX-RS enabled resource (Jersey implementation) via JSON requests. The todos are persisted to a table from the local dictionary using JPA technology. Here is how the final application looks like.
The application consists of three modules: EAR, EJB, Web. We need the EAR for packaging and deployment to the AS, the EJB module contains the session bean responsible for data-access operations via JPA, the Web module contains the UI and the Jersey's resource.
Each todo has a text attribute describing what needs to be done, a boolean property for checking if the task is completed, and the property for the date and time when the task was created. We also need a unique identifier for each todo, as well as, the username of the user who created the task. So this is how we can represent a todo via a POJO with some JPA annotations.
@Entity
@Table(name="ZTMP_TODO")
public class Todo {
@Id
@GeneratedValue(strategy=GenerationType.TABLE, generator="idGen")
@TableGenerator(name="idGen", table="ZTMP_ID_GEN")
private long id;
private String userId;
private String text;
private short done;
@Transient
private boolean completed;
private Timestamp createdAt;
public Todo() {
// needed for JPA, JAXB
}
public Todo(String userId, String text) {
this.userId = userId;
this.text = text;
this.done = 0;
this.createdAt = new Timestamp(System.currentTimeMillis());
}
// Getters
public long getId() {
return id;
}
public String getText() {
return text;
}
public boolean getCompleted() {
return this.done == 1;
}
public Timestamp getCreatedAt() {
return createdAt;
}
// Setters
public void setDone(short done) {
this.done = done;
}
}
And here is how the table "ZTMP_TODO" is defined in the local dictionary.
The ID column values will be generated automatically, hence we need to explicitly specify an id generator for which we should define a special table "ZTMP_ID_GEN". We also define a transient property "completed" which is just a boolean equivalent of the short "done" attribute. It should not be persisted, but it will be present in the JSON representation of the todo, serialized and returned to the front-end by the Jersey resource.
We let the AS to manage the injection of the persistence context into our session bean. This is how the DAO EJB looks like.
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class TodoDao implements TodoDaoLocal {
@PersistenceContext
private EntityManager em;
/**
* Default constructor.
*/
public TodoDao() {
// needed for JPA
}
@SuppressWarnings("unchecked")
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public List<Todo> findAllUserTodos(String userId) {
Query query = em
.createQuery("SELECT t from Todo t where t.userId = ?1");
query.setParameter(1, userId);
return query.getResultList();
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public long save(Todo todo) {
em.persist(todo);
return todo.getId();
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void change(long id, short done) {
Todo t = em.find(Todo.class, id);
t.setDone(done);
em.persist(t);
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void delete(long id) {
em.remove(em.find(Todo.class, id));
}
}
Notice that we specify a container-level transaction strategy for the access to Todo table. In order for our Jersey resource to be able to access the session bean, we create an InjectableProvider for the singleton instance of the EJB to be injected as a field into our resource.
See this link for a relevant discussion of the methods to inject an EJB into a Jersey resource, the code below is based on the examples found there.
@Provider
public class LocalEjbInjectableProvider implements
InjectableProvider<LocalEjb, Type>, Injectable<Object> {
private static final Logger logger = Logger
.getLogger(LocalEjbInjectableProvider.class);
private LocalEjb annot;
private Type type;
@Override
public Injectable<Object> getInjectable(ComponentContext compCtx,
LocalEjb annot, Type type) {
this.annot = annot;
this.type = type;
return this;
}
@Override
public ComponentScope getScope() {
return ComponentScope.Singleton;
}
@Override
public Object getValue() {
try {
String name = annot.name();
if (name.equals("")) {
name = ((Class<?>) type).getSimpleName();
}
Object ejb = new InitialContext().lookup("java:comp/env/" + name);
logger.debug("Looked up session EJB " + ejb);
return ejb;
} catch (NamingException e) {
throw new RuntimeException(e);
}
}
}
Notice, that we specify ComponentScope.Singleton for the scope of the injected bean. Also, we use a relative JNDI lookup path ("java:comp/env") when looking up the session bean using the name which should be declared in web.xml application descriptor file.
<ejb-local-ref>
<ejb-ref-name>TodoDaoLocal</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local>ch.unil.sapui5.jersey.ejb.TodoDaoLocal</local>
</ejb-local-ref>
Jersey (JAX-RS) resource receives REST requests and responds with JSON data to be processed by the SAPUI5 front-end. We need to configure Jersey to automatically convert any returned POJO into its JSON equivalent using Jackson mapper. For this we set POJOMappingFeature initial parameter to true when configuring the Jersey's ServletContainer in the web.xml.
<servlet>
<servlet-name>jersey-rest-ws</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>ch.unil.sapui5.jersey.rest</param-value>
</init-param>
<init-param>
<param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>jersey-rest-ws</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
The Jersey resource is pretty straight forward. Notice the injection of the session bean.
@Path("/todo")
public class TodoResource {
private static final Logger logger = Logger.getLogger(TodoResource.class);
@LocalEjb
private TodoDaoLocal dao;
@Path("/init")
@GET
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public List<Todo> init(@LoggedInUser String user) {
logger.debug("Retrieving todos for user " + user);
return dao.findAllUserTodos(user);
}
@Path("/add")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Todo add(Map<String, Object> data, @LoggedInUser String user) {
String text = (String) data.get("text");
Todo t = new Todo(user, text);
dao.save(t);
logger.debug("Added a new todo " + t);
return t;
}
@Path("/change/{id}/{completed}")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public void change(@PathParam("id") long id,
@PathParam("completed") boolean completed) {
logger.debug("Changing completed of the todo with id " + id + " to "
+ completed);
dao.change(id, (short) (completed ? 1 : 0));
}
@Path("/delete/{id}")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public void delete(@PathParam("id") long id) {
logger.debug("Deleting the todo with id " + id);
dao.delete(id);
}
}
I'm using a custom implementation of Log4j Appender which redirects all logging messages to the SAP's Location API. Also, there is a PerRequest InjectableProvider to inject the unique name of the logged-in user (IUser) as a method parameter to be used in the calls to the DAO bean. The user can be obtained with the call to com.sap.security.api.UMFactory.getAuthenticator() method. Do not forget to add the relevant security-role and security-constraint sections to web.xml file and security-role-map section to web-j2ee-engine.xml file in order to protect the REST end-points referenced by the Jersey resource.
Here is the controller for the UI.
sap.ui.controller("todo.main", {
todoModel : null,
deleteTodo : function(id) {
this.doAjax("/delete/" + id).done(function(index) {
var t = this.getTodo(id);
this.todoModel.getProperty("/").splice(t.index, 1);
this.todoModel.updateBindings();
this.getView().rerender();
});
},
changeTodo : function(id, completed) {
this.doAjax("/change/" + id + "/" + completed);
this.getView().rerender();
},
addTodo : function(text) {
this.doAjax("/add", {
text : text
}).done(function(todo) {
this.todoModel.getProperty("/").push(todo);
this.todoModel.updateBindings();
this.getView().rerender();
});
},
initTodoModel : function() {
var model = this.todoModel = new sap.ui.model.json.JSONModel(
"/tmp~todo~web/rest/todo/init");
return model;
},
doAjax : function(path, content, type, async) {
var params = {
url : "/tmp~todo~web/rest/todo" + path,
dataType : "json",
contentType : "application/json",
context : this,
cache : false
};
params["type"] = type || "POST";
if (async === false) {
params["async"] = async;
}
if (content) {
params["data"] = JSON.stringify(content);
}
return jQuery.ajax(params);
},
getTodo : function(id) {
var index, todo;
jQuery.each(this.todoModel.getProperty("/"), function(i, t) {
if (t.id === id) {
index = i;
todo = t;
}
});
return {
index : index,
todo : todo
};
}
});
It uses jQuery.ajax() helper to communicate with the Jersey resource deployed at "/tmp~todo~web/rest" end-point. The controller initializes a JSONModel object with the array of user todos. Here is the view.
sap.ui.jsview("todo.main", {
getControllerName : function() {
return "todo.main";
},
createContent : function(oController) {
// extend a TextView control for displaying the text of todos
// with strike-through decoration if the todo is done
sap.ui.commons.TextView.extend("TodoTextView", {
renderer : {
render : function(rm, tv) {
rm.write("<span");
rm.writeControlData(tv);
rm.writeStyles();
if (tv.getBindingContext()
&& tv.getBindingContext().getProperty("completed")) {
rm.addClass("todoDone");
}
rm.writeClasses();
rm.write(">");
rm.writeEscaped(tv.getText());
rm.write("</span>");
}
}
});
// create the JSON model for the table of todos
var model = oController.initTodoModel();
// create a table for todos
var table = new sap.ui.table.Table( {
title : "Todos",
visibleRowCount : 5,
firstVisibleRow : 1,
selectionMode : sap.ui.table.SelectionMode.Single
});
// first column: checkbox bound to done model property
table.addColumn(new sap.ui.table.Column( {
label : new sap.ui.commons.Label( {
text : "Done"
}),
template : new sap.ui.commons.CheckBox( {
checked : "{completed}"
}).attachChange(function(event) {
oController.changeTodo(this.getBindingContext().getProperty("id"),
this.getBindingContext().getProperty("completed"));
}),
width : "25px"
}));
// second colum: custom text view bound to the text model property
table.addColumn(new sap.ui.table.Column( {
label : new sap.ui.commons.Label( {
text : "Text"
}),
template : new TodoTextView( {
text : "{text}"
}).addStyleClass("totoText"),
width : "100px"
}));
// third column: text view with custom formatter for the createdAt
// property
// shows timestamp as a sap.ui.model.type.DateTime object
table.addColumn(new sap.ui.table.Column( {
label : new sap.ui.commons.Label( {
text : "Created At"
}),
template : new sap.ui.commons.TextView( {
text : {
path : "createdAt",
formatter : function(timestamp) {
return new Date(timestamp);
},
type : new sap.ui.model.type.DateTime( {
pattern : "yyyy/MM/dd HH:mm:ss"
})
}
}),
width : "100px"
}));
// fourth column: clickable image for removing a todo
table.addColumn(new sap.ui.table.Column( {
template : new sap.ui.commons.Image( {
src : "resources/images/erase.png",
width : "16px",
height : "16px",
tooltip : {
path : "id",
formatter : function(id) {
return "Remove todo # " + id;
}
}
}).attachPress(function(event) {
oController.deleteTodo(this.getBindingContext().getProperty("id"));
}),
width : "24px"
}));
// set the model for the table and bind the rows
table.setModel(model);
table.bindRows("/");
// store the table as a member variable of the view for convenience
this.table = table;
// controls for adding new todo
var text = new sap.ui.commons.TextField( {
width : "100%"
});
var btnAdd = new sap.ui.commons.Button( {
text : "Add",
icon : "resources/images/add.png"
});
btnAdd.attachPress(function(event) {
oController.addTodo(text.getValue());
});
// arrange controls on the page with a matrix layout
var ml = new sap.ui.commons.layout.MatrixLayout( {
columns : 2,
layoutFixed : true,
width : "500px"
});
ml.createRow(new sap.ui.commons.layout.MatrixLayoutCell( {
content : [ table ],
colSpan : 2
}));
ml.addRow(new sap.ui.commons.layout.MatrixLayoutRow( {
cells : [
new sap.ui.commons.layout.MatrixLayoutCell( {
content : [ text ]
}),
new sap.ui.commons.layout.MatrixLayoutCell( {
content : [ btnAdd ],
hAlign : sap.ui.commons.layout.HAlign.Left
}) ]
}));
return [ ml ];
}
});
When user checks a todo in the table as completed, it should be displayed with a different style (grayed out and with strike-through effect). To achieve this, we can extend TextView and to implement our own renderer, which adds the specific style class depending on the value of "completed" property of the binding context.
For the reference, here are the libraries I've used in my Web project. There are libraries from SAPUI5 distribution, version 1.8.4, and the Jersey libraries, version 1.9.1, with dependencies.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
10 | |
9 | |
5 | |
4 | |
4 | |
3 | |
3 | |
3 | |
3 | |
3 |