Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member

Introduction

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.

Model: JPA entity

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.

Session bean, EntityManager and InjectableProvider.

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 resource

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.

SAPUI5 controller and view

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.

5 Comments
Labels in this area