TASKPM/doc/src/technical/howto-develop-a-use-case-in-libreplan.rst

2661 lines
98 KiB
ReStructuredText
Raw Normal View History

--------------------------------------
How To Develop A Use Case In LibrePlan
--------------------------------------
.. sectnum::
:Author: Manuel Rego Casasnovas
:Contact: rego@igalia.com
:Date: 15/08/2011
:Copyright:
Some rights reserved. This document is distributed under the Creative
Commons Attribution-ShareAlike 3.0 licence, available in
http://creativecommons.org/licenses/by-sa/3.0/.
:Abstract:
This is a guide about how to develop a use case in LibrePlan_. Following the
different sections of this document you will end up developing a complete
CRUD_ (create, read, update and delete) use case in the project.
Thanks to this tutorial you will know the basic structure, different layers of
application architecture and underlying technology stack. Summarizing, you
will learn how to create a new entity, define an interface to manipulate it,
store it on a database, add some kind of validation and integrate it with web
services.
.. contents:: Table of Contents
Introduction
============
This manual is a kind of practical exercise that will be solved throughout the
different sections. It is required to have basic knowledge about Java software
platform in order to properly follow the document. Moreover, knowledge in
different Java frameworks used in LibrePlan like Hibernate, Spring, ZK, JUnit,
etc. would be a nice addition.
The goal of this document is develop a **complete CRUD use case in LibrePlan**.
The idea consists of create a new entity called ``StretchesFunctionTemplate``
that will be managed from application interface, just like any other entity in
the project.
``StretchesFunctionTemplate`` will be a class to define templates for different
``StretchesFunction`` that are used in advanced allocation window.
A ``StretchesFunction`` is a kind of assignment function which allow users
define different stretches in order to do resource allocations.
.. NOTE::
Let's imagine that you have a task which lasts from May 1st to May 10th. Then
users could define the following stretches:
* Stretch 1:
* Duration: 20%
* Progress: 50%
* Stretch 2:
* Duration: 50%
* Progress: 70%
* Stretch 3:
* Duration: 70%
* Progress: 100%
Where, for example, *Stretch 1* means that half of the work to be done in the
task will be ready by the end of May 2nd (20% of task duration).
Then LibrePlan will be perform different resource allocations according to
function defined by user.
Thanks to the new class ``StretchesFunctionTemplate`` users will have the chance
to store repetitive ``StretchesFunction`` that they usually apply while
scheduling. This will allow users to define some kind of patterns for tasks,
where, for example, they know that always start with a lower load and then it is
increased at the end. They could create a ``StretchesFunctionTemplate`` defining
this behaviour and use it in all the tasks they want.
.. TIP::
If you want to run LibrePlan in development mode you need to follow the next
instructions:
* Create a PostgreSQL database called ``libreplandev`` with permissions for a
user ``libreplan`` with password ``libreplan`` (see ``HACKING`` file for
other databases and more info).
* Compile LibrePlan with the following command from project root folder::
mvn -DskipTests -P-userguide clean install
* Launch Jetty from ``libreplan-webapp`` directory::
cd libreplan-webapp
mvn -P-userguide jetty:run
* Access with a web browser to the following URL and login with default
credentials (user ``admin`` and password ``admin``):
http://localhost:8080/libreplan-webapp/
Domain entities
===============
First of all you need to create the new entity ``StretchesFunctionTemplate`` in
LibrePlan **business layer**.
Domain entities encapsulate application business data and part of their logic.
They are Hibernate_ entities, and therefore are retrieved and stored in a data
warehouse (usually a database). Mapping between Java classes and Hibernate is
done with ``.hbm.xml`` files. For example, file ``ResourceAllocations.hbm.xml``
contains ``StretchesFunction`` class mapping.
All domain entities in the project inherit from ``BaseEntity``. ``BaseEntity``
class has two attributes: ``id`` and ``version``. ``id`` is mandatory in order
to entity could be considered as an Hibernate entity. ``version`` attribute is
used to implement concurrency control method called `Optimistic Locking`_.
.. ADMONITION:: Optimistic Locking
``version`` field in entities is used to implement the concurrency control
method in order to detect concurrency problems during execution.
Let's imagine two users go to edit the same exception day type called
"HOLIDAY" and both want to modify field ``color``. Currently in database you
will have::
name: HOLIDAY
version: 1
color: red
First user changes color and sets "blue" as color and save the entity. When
entity is stored, ``version`` is incremented by 1, and the result number must
be greater than current value in database. In this case 2 is greater than 1 so
it is properly stored on database::
name: HOLIDAY
version: 2
color: blue
Second user started at the same time, but it is going to try to save the same
entity later than the first user. Second users sets color to "green" and try
to store the entity. In this case the value for ``version`` is incremented
from 1 (the original) to 2, but 2 is not greater than current value in
database. Therefore, a concurrency problem has happened and second user will
receive the following message:
.. pull-quote::
Another user has modified the same data, so the operation cannot be safely
completed.
Please try it again.
Entities instantiation
----------------------
In LibrePlan domain entities are never instantiated directly, but entities will
expose a **static method ``create()``** which will be responsible to return a
new instance. The rest of classes must call ``create()`` method of
``BaseEntity`` when they want to create a new instance of any entity. This is
usually implemented with something similar to the following code::
public class MyNewEntity extends BaseEntity {
public static MyNewEntity create() {
return create(new MyNewEntity());
}
/**
* Constructor for Hibernate. Do not use!
*/
protected MyNewEntity() {
}
}
As you can see, it is defining a default constructor without parameters with
``protected`` visibility. Default constructor is mandatory because of Hibernate
need it, but it is marked with reduced visibility in order to avoid other
classes use it.
.. WARNING::
In LibrePlan a lot of entities extends ``IntegrationEntity`` instead of
``BaseEntity``, anyway ``IntegrationEntity`` also extends ``BaseEntity``.
``IntegrationEntity`` is a base class for all domain entities that are going
to be available via web services in the project. These entities have a
``code`` attribute, which unlike ``id`` is unique among the applications to be
integrated (``id`` is only unique inside one LibrePlan instance).
In order to know if an object is new or not you will use method
``isNewObject()`` of ``BaseEntity``, you will never directly check if ``id``
attribute is ``null`` (transient entity).
.. ADMONITION:: State of objects in Hibernate
Transient
An object out of Hibernate session instantiated with ``new()``. Actually, in
LibrePlan the method used is ``create()`` that calls ``new()`` at some
point.
Persistent
A persistent entity, already stored on database, which is inside a
Hibernate session.
Detached
A persistent entity out of Hibernate session.
New entity implementation
-------------------------
The **new entity ``StretchesFunctionTemplate``** will have the following
properties:
* ``name``: A string to identify the template.
* ``stretches``: A list of ``StretchTemplate`` a new class that will just have
two attributes: ``durationProportion`` and ``progressProportion``. These
would be two percentages defined as ``BigDecimal`` and one based, i.e., 20%
will be 0.20.
``StretchTemplate`` will be a value object as every ``StretchTemplate`` will
belong just to one ``StretchesFunctionTemplate`` and would not be modified out
of this relationship. So, in this case ``StretchTemplate`` will not extends
``BaseEntity``.
You will need to create the following files (some excerpts of source code are
shown):
* ``StretchesFunctionTemplate.java``:
::
package org.libreplan.business.planner.entities;
...
/**
* This will store repetitive patterns to be applied in different
* {@link StretchesFunction}
*
* @author Manuel Rego Casasnovas <mrego@igalia.com>
*/
public class StretchesFunctionTemplate extends BaseEntity implements
IHumanIdentifiable {
public static StretchesFunctionTemplate create(String name) {
return create(new StretchesFunctionTemplate(name));
}
private String name;
@Valid
private List<StretchTemplate> stretches = new ArrayList<StretchTemplate>();
/**
* Default constructor for Hibernate. Do not use!
*/
protected StretchesFunctionTemplate() {
}
...
@Override
public String getHumanId() {
return name;
}
...
.. NOTE::
``IHumanIdentifiable`` is an interface that needs a human identifier to show
in application UI. It defines the method ``getHumanId`` that returns a text
identifier of the entity.
As this entity is going to be edited from LibrePlan web interface, it
implements ``IHumanIdentifiable``.
* ``StretchTemplate.java``:
::
package org.libreplan.business.planner.entities;
...
/**
* This class is intended as a Hibernate component. It's formed by two
* components, the duration proportion and the progress proportion. It
* represents the different values of a {@link StretchesFunctionTemplate}.
*
* @author Manuel Rego Casasnovas <mrego@igalia.com>
*/
public class StretchTemplate {
public static StretchTemplate create(BigDecimal durationProportion,
BigDecimal progressProportion) {
return new StretchTemplate(durationProportion, progressProportion);
}
private BigDecimal durationProportion = BigDecimal.ZERO;
private BigDecimal progressProportion = BigDecimal.ZERO;
/**
* Default constructor for Hibernate. Do not use!
*/
protected StretchTemplate() {
}
...
.. IMPORTANT::
You should not forget to add license header in your new files specifying the
license as explained in documentation section at `LibrePlan wiki`_. You can
copy it from other files and modify year and copyright holder accordingly.
Moreover, always remember to add, at least, a general comment explaining the
purpose of your classes.
Model View Controller pattern
=============================
LibrePlan architecture follows MVC_ (Model-view-controller) pattern, which
isolates business logic from user interface allowing separation of different
layers in the application. View and controller will be explained later, now it
is time to explain **model layer** that is in charge of implement application
business or domain logic.
This model layer is formed by different elements. On the one hand, there are
domain entities and DAO_ (Data Access Object) classes which offer methods to
query and store domain objects. On the other hand there are ``XXXModel.java``
files, that are always associated to some controller.
.. ADMONITION:: Domain Driven Design
The project follows approach proposed by DDD_. It tries that business logic
remains encapsulated inside domain classes, as far as possible, otherwise
it will be used a model layer.
The idea is that every domain element will be responsible for itself, which
means that it knows its business logic and exposes it to other objects
through methods. Other operations were, for example, several objects are used
could be written in model layer.
Actually, model classes do not access directly to database but they do it
through a DAO object. DAO classes are responsible for retrieve, query and store
domain entities on database, i.e. they implement the persistence layer only
accessible from model.
However, in the application domain elements can be used directly from view layer
for reading or modifying its content.
Persistence layer communication
-------------------------------
In order to access domain entities it will always exist a **DAO class** for each
entity type. This DAO class inherits from ``GenericDAOHibernate``, which
provides the methods needed to implement common persistence behaviour.
If you want that a model has access to a DAO class, you have to insert an
attribute in your model, for example, a variable called
``tretchesFunctionTemplateDAO`` with type ``IStretchesFunctionTemplateDAO``::
@Autowired
private IStretchesFunctionTemplateDAO stretchesFunctionTemplateDAO;
Take into account that this attribute has an interface as type. This interface,
``IStretchesFunctionTemplateDAO``, will have associated an implementation class
called ``StretchesFunctionTemplateDAO``. Spring_ framework is in charge to
inject this implementation class in the variable. For this to happen, it is
needed to mark the attribute with ``@Autowired`` annotation. This will be also
needed to add some special annotations, interpreted by Spring, at implementation
class.
There is also an interface ``IGenericDAOHibernate`` implemented by
``GenericDAOHibernate``. So, your new interface will extend this one.
Then you will have the following files:
* ``IStretchesFunctionTemplateDAO.java``:
::
package org.libreplan.business.planner.daos;
...
/**
* DAO interface for {@link StretchesFunctionTemplate}
*
* @author Manuel Rego Casasnovas <mrego@igalia.com>
*/
public interface IStretchesFunctionTemplateDAO extends
IGenericDAO<StretchesFunctionTemplate, Long> {
}
* ``StretchesFunctionTemplateDAO.java``:
::
package org.libreplan.business.planner.daos;
...
/**
* DAO for {@link StretchesFunctionTemplate}
*
* @author Manuel Rego Casasnovas <mrego@igalia.com>
*/
@Repository
@Scope(BeanDefinition.SCOPE_SINGLETON)
public class StretchesFunctionTemplateDAO extends
GenericDAOHibernate<StretchesFunctionTemplate, Long> implements
IStretchesFunctionTemplateDAO {
}
.. ADMONITION:: Inversion of control
`Inversion of control`_ pattern, or Dependency Injection, is based on object
oriented programming principle: "develop in terms of interfaces and
functionality instead of concrete implementation details".
In LibrePlan for each DAO class there is an interface class `IXXXDAO`. Models
always use these interface classes. Spring framework instantiates a class for
each interface type and injects it in the corresponding variable.
.. NOTE::
As you can see DAO class is being defined as a singleton with the following
line::
@Scope(BeanDefinition.SCOPE_SINGLETON)
This is because of DAO classes are not going to store any state variable, so
methods only depends on parameters. Thus, just an instance of a DAO class is
enough for any place where it is used.
Summarizing, persistence layer encapsulates all operations related to Hibernate
communication for retrieving, querying and storing entities on database.
Therefore, you will not need to use Hibernate API directly in LibrePlan source
code in order to perform operations like: start transaction, commit
transaction, rollback, etc.
Database schema
---------------
Moreover, you need to define **Hibernate mapping** for the new entity
``StretchesFunctionTemplate``. Like this new entity is related with allocations
you will use ``ResourceAllocations.hbm.xml`` file and, then, add the following
lines (in other cases you should look for the proper ``.hbm.xml`` file or just
create a new one if needed)::
<!-- StretchesFunctionTemplate -->
<class name="StretchesFunctionTemplate" table="stretches_function_template">
<id name="id" access="property" type="long">
<generator class="hilo">
<param name="max_lo">100</param>
</generator>
</id>
<version name="version" access="property" type="long" />
<property name="name" access="property" not-null="true" />
<list name="stretches" table="stretch_template">
<key column="stretches_function_template_id" />
<list-index column="stretch_position" />
<composite-element class="StretchTemplate">
<property name="durationProportion" column="duration_proportion"
not-null="true" />
<property name="progressProportion" column="progress_proportion"
not-null="true" />
</composite-element>
</list>
</class>
However, this is not enough in order to store the new entity on database,
because of tables are not created yet. Usually, tables are automatically created
by Hibernate, but this is disabled in LibrePlan, and Hibernates just validates
that database structure matches with mapping specifications in ``hbm.xml``
files. The reason to disable automatic schema creation is for having a proper
control over `database refactorings`_, this allows that application manage
migrations between databases of different LibrePlan versions. Only testing
database is created automatically in the project.
Liquibase_ is the tool used to manage these **database refactorings**.
Developers have to specify in a changelog file the changes to be applied on
database when they modify any mapping. Then you will need to add the following
lines in the proper ``db.changelog-XXX.xml`` file::
<changeSet author="mrego" id="create-tables-related-to-stretches_function_template">
<comment>Create new new tables and indexes related with StretchesFunctionTemplate entity</comment>
<createTable tableName="stretches_function_template">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="stretches_function_template_pkey"/>
</column>
<column name="version" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="stretch_template">
<column name="stretches_function_template_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="duration_proportion" type="DECIMAL(19,2)">
<constraints nullable="false"/>
</column>
<column name="progress_proportion" type="DECIMAL(19,2)">
<constraints nullable="false"/>
</column>
<column name="stretch_position" type="INTEGER">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey
columnNames="stretches_function_template_id, stretch_position"
constraintName="stretch_template_pkey"
tableName="stretch_template"/>
<addForeignKeyConstraint
baseColumnNames="stretches_function_template_id"
baseTableName="stretch_template"
constraintName="stretch_template_stretches_function_template_id_fkey"
deferrable="false" initiallyDeferred="false"
onDelete="NO ACTION" onUpdate="NO ACTION"
referencedColumnNames="id"
referencedTableName="stretches_function_template"
referencesUniqueColumn="false"/>
</changeSet>
As you can see, this specify the different tables to be created on database and
also some constraints like foreign keys. Usually you can take a look to other
Liquibase changes to know how to create a table or some field. Also a good idea
is to check the result of your changeset against testing database (which is
created automatically), in this way you will be sure that your changes are
right.
Interface
=========
Let's move to **view layer**, now that you already know how is the new entity,
which attributes it has and so on. You are ready to start developing the
interface and start to see something working in the application. LibrePlan uses
ZK_ framework for UI development.
Menu entry
----------
First, the new entity ``StretchesFunctionTemplate`` will be a managed by
application administrator. For that reason, you need to add a new option on
*Administration / Management* menu.
Class ``CustomMenuController`` is in charge to create options menu which appears
in top part of the application. Then you need to modify method
``initializeMenu()`` in ``CustomMenuController`` to add a new ``subItem`` inside
the ``topItem`` *Administration / Management*::
subItem(_("Stretches Function Templates"),
"/planner/stretchesFunctionTemplate.zul",
"")
This option will link to a new ``.zul`` file that will be interface for
application users in order to manage ``StretchesFunctionTemplate`` entity. When
you click the new entry, LibrePlan will the load ``.zul`` file (but, at this
moment, the link is not going to work as ``.zul`` page does not exist yet).
Main ``.zul`` page
------------------
Then you will create the file ``stretchesFunctionTemplate.zul`` inside
``libreplan-webapp/src/main/webapp/planner/`` folder with the following
content:
::
<?page id="exceptionDayTypesList" title="${i18n:_('LibrePlan: Stretches Function Templates')}" ?>
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
<?init class="org.zkoss.zk.ui.util.Composition" arg0="/common/layout/template.zul"?>
<?link rel="shortcut icon" href="/common/img/favicon.ico" type="image/x-icon"?>
<?link rel="stylesheet" type="text/css" href="/common/css/libreplan.css"?>
<?link rel="stylesheet" type="text/css" href="/common/css/libreplan_zk.css"?>
<?component name="list" inline="true" macroURI="_listStretchesFunctionTemplates.zul"?>
<?component name="edit" inline="true" macroURI="_editStretchesFunctionTemplate.zul"?>
<zk>
<window self="@{define(content)}"
apply="org.libreplan.web.planner.allocation.streches.StretchesFunctionTemplateCRUDController">
<vbox id="messagesContainer"/>
<list id="listWindow"/>
<edit id="editWindow"/>
</window>
</zk>
This file contains a ``.zul`` page which contains a window that has another
window to list (``list``) elements and another for editing them (``edit``).
::
<?page id=”” title=”${i18n:_('LibrePlan: Exception Days')}” ?>
This line define that the document is a page.
::
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
It is needed because of you are going to use **bindings** in this page.
.. NOTE::
``<?init ... ?>`` labels are always the first ones to be evaluated inside a
page. And they always receive a class as parameter, they instantiate it and
call its ``init()`` method.
.. ADMONITION:: Data Binding
A binding is the ability to evaluate a data element (for example, a bean) in
execution time from a ``.zul`` page. Evaluation, which finally executes a
method, could be used to get data from the object or modify its properties.
Usually bindings are used in components like ``Listbox``, ``Grid`` and
``Tree``. These components have the possibility to be fed by dynamic data
(*live-data*). Because these components receive dynamic data, it is not
possible to determine how many rows are going to be shown before knowing the
real data. These components allow build a generic row that will be repeated
for each element in the collection. When component is rendered, bindings are
evaluated in order to get concrete value. For example::
<list model="@{controller.elements}" >
<rows each="" value="">
<row>
<label value="@{element.name}" />
</row>
</rows>
</list>
When component is evaluated, ``controller.getElements()`` will be called and
a collection of elements will be returned. For each returned element,
``element.getName()`` method will be executed, and then value of name
attribute will be printed as a label.
Symbols marked with ``@{...}`` are bindings. These expressions will be only
evaluated if the following directive is included in the ``.zul`` page::
<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit" ?>
::
<?init class="org.zkoss.zk.ui.util.Composition"
arg0="/common/layout/template.zul"?>
It is a composition component. ``arg0`` attribute makes reference to a `.zul`
file which is used as layout for current page. In this layout is specified that
a component defined as ``content`` will be inserted. Your page will define a
window marked as ``content``, that will be inserted in ``template.zul`` page.
``apply`` attribute
-------------------
The basis for implementing MVC pattern in ZK is ``apply`` attribute.
Your page defines a component ``Window`` with an ``apply`` attribute assigned::
<window self="@{define(content)}"
apply="org.libreplan.web.planner.allocation.streches.StretchesFunctionTemplateCRUDController">
It links this ``Window`` component with a ``.java`` file, thereby the Java class
will be able to access and manipulate components defined inside ``window`` tag.
This class will play controller role for this ``.zul`` page (view).
Communication between view and controller
-----------------------------------------
If you want that ``.zul`` components will be accessible from controller just use
the same identifier in ``.zul`` and Java. For example:
::
package org.libreplan.web.common;
...
/**
* Abstract class defining common behavior for controllers of CRUD screens. <br />
*
* Those screens must define the following components:
* <ul>
* <li>{@link #messagesContainer}: A {@link Component} to show the different
* messages to users.</li>
* <li>{@link #listWindow}: A {@link Window} where the list of elements is
* shown.</li>
* <li>{@link #editWindow}: A {@link Window} with creation/edition form.</li>
*
* @author Manuel Rego Casasnovas <rego@igalia.com>
*/
@SuppressWarnings("serial")
public abstract class BaseCRUDController<T extends IHumanIdentifiable> extends
GenericForwardComposer {
...
This matching is automatic and is done by ZK. In order that this works it is
needed that your controller inherits from ``GenericForwarComposer`` (which in
turn extends ``GenericAutowireComposer``, that is the class doing this kind of
"magic").
Thanks to this you will be able to access view from controller, but not the
other way around. If you want to do this you need to define a variable inside
``Window`` component that will contain a reference to controller instance. The
steps to do this are the following ones:
* Your controller will override method ``doAfterCompose``.
* This method receives a component which is the window associated to the
controller through ``apply`` attribute.
* In ``Window`` you will use ``setAttribute`` method in order to create a
variable called ``controller`` that will contain a reference to controller.
::
@Override
public void doAfterCompose(Component comp) throws Exception {
super.doAfterCompose(comp);
comp.setAttribute("controller", this);
...
}
After that from ``.zul``, you will make reference to a variable called
``controller`` (either from a binding or in order to execute any method when an
event is dispatched). In this way you could see that view can also access to
controller. For example with the following lines::
<!-- Call method getStretchesFunctionTemplates from view -->
<list model="@{controller.stretchesFunctionTemplates}">
<!-- When a button is clicked call method goToEditForm() -->
<button onClick="controller.goToEditForm()" />
As you can see in last example, when an event is launched is not needed to use
data binding.
``BaseCRUDController`` is a generic class with common behaviour for controllers
of CRUD screens. It defines a set of methods with a common functionality and
delegates on some abstract methods that should be implemented in the subclasses.
For this example you will create a new controller
``StretchesFunctionTemplateCRUDController`` as a subclass of
``BaseCRUDController``.
::
package org.libreplan.web.planner.allocation.streches;
...
/**
* CRUD controller for {@link StretchesFunctionTemplate}.
*
* @author Manuel Rego Casasnovas <mrego@igalia.com>
*/
public class StretchesFunctionTemplateCRUDController extends
BaseCRUDController<StretchesFunctionTemplate> {
...
ZK macro components
-------------------
Your page ``stretchesFunctionTemplate.zul`` defines two macro components:
``list`` and ``edit``. These macro components implement list view and
edit/creation view respectively.
::
<?component name="list" inline="true" macroURI="_listStretchesFunctionTemplates.zul"?>
This line declares a macro component called ``list`` associated to page
``_listStretchesFunctionTemplates.zul``. ``inline`` attribute indicates that the
macro component is on the same scope as the component which contains it, i.e.,
``window`` component could see ``list`` component and the other way around.
Inside the same scope or namespace there can not be repeated identifiers (``id``
attributes). However, ``window`` component creates a new namespace. Inside
different namespaces identifiers could be repeated.
Another consequence is that from the main window, which is associated with
controller, you can not access components defined in ``list`` or ``edit``. For
example, ``list`` contains a ``Grid`` called
``listStretchesFunctionTemplates``::
public class StretchesFunctionTemplateCRUDController extends
BaseCRUDController<StretchesFunctionTemplate> {
...
private Grid listStretchesFunctionTemplates;
@Override
public void doAfterCompose(Component comp) throws Exception {
...
listStretchesFunctionTemplates.getModel();
}
...
Access to ``listStretchesFunctionTemplates`` will cause a
``NullPointerException``, because of ``listStretchesFunctionTemplates`` is not
in main window namespace. But, you could access indirectly to component from
controller through ``list`` component, because this is accessible from
controller. For example::
public class StretchesFunctionTemplateCRUDController extends
BaseCRUDController<StretchesFunctionTemplate> {
...
private Grid listStretchesFunctionTemplates;
@Override
public void doAfterCompose(Component comp) throws Exception {
...
listStretchesFunctionTemplates = (Grid) listWindow
.getFellowIfAny("listStretchesFunctionTemplates");
listStretchesFunctionTemplates.getModel();
}
...
Another important issue when implementing CRUD use cases is that general view
contains both ``list`` and ``edit`` component. These components are rendered
and shown when page is loaded. Class ``OnlyOneVisible`` is used in controller to
manage which one will be visible at a given time. You can find the following
pieces of code in ``BaseCRUDController``::
private OnlyOneVisible visibility;
...
private OnlyOneVisible getVisibility() {
if (visibility == null) {
visibility = new OnlyOneVisible(listWindow, editWindow);
}
return visibility;
}
/**
* Show list window and reload bindings
*/
protected void showListWindow() {
getVisibility().showOnly(listWindow);
Util.reloadBindings(listWindow);
}
/**
* Show edit form with different title depending on controller state and
* reload bindings
*/
protected void showEditWindow() {
getVisibility().showOnly(editWindow);
updateWindowTitle();
Util.reloadBindings(editWindow);
}
And at the end of ``doAfterCompose`` method there is a call to
``showListWindow``, that shows the list view and use ``OnlyOneVisible`` class
to hide edit/creation form.
Messages for users
------------------
::
<vbox id="messagesContainer"/>
Defines a container to show messages to users. These messages usually appear in
the top of current window inside a box. There is a default implementation in a
class called ``MessagesForUser`` which is used in all controllers to show
messages to users in a similar way for the whole application.
Apart from previous line on ``.zul`` file you will see the following lines
inside ``doAfterCompose`` method in ``BaseCRUDController``::
private IMessagesForUser messagesForUser;
private Component messagesContainer;
...
@Override
public void doAfterCompose(Component comp) throws Exception {
...
messagesForUser = new MessagesForUser(messagesContainer);
...
}
These lines instantiate a new object of ``MessagesForUser`` class using the
container defined at ``.zul`` page. Then when you want to notify or show a
message to the users you will use some method defined at ``IMessagesForUser``.
For example::
messagesForUser.showMessage(Level.INFO,
_("Stretches function template saved"));
List view
---------
For the moment you just have the code needed for the main page
``stretchesFunctionTemplate.zul``. At this point you are going to create the
list view interface in a file called ``_listStretchesFunctionTemplates.zul``
(in the same folder than main page file
``libreplan-webapp/src/main/webapp/planner/``). This file will have the
following content:
::
<window id="${arg.id}" title="${i18n:_('Stretches Function Templates List')}">
<grid id="listStretchesFunctionTemplates"
model="@{controller.stretchesFunctionTemplates}"
mold="paging" pageSize="10" fixedLayout="true">
<columns>
<column label="${i18n:_('Name')}" sort="auto(lower(name))" />
<column label="${i18n:_('Operations')}" />
</columns>
<rows>
<row self="@{each='stretchesFunctionTemplate'}"
value="@{stretchesFunctionTemplate}">
<label value="@{stretchesFunctionTemplate.name}" />
<!-- Operations -->
<hbox>
<button sclass="icono" image="/common/img/ico_editar1.png"
hoverImage="/common/img/ico_editar.png"
tooltiptext="${i18n:_('Edit')}"
onClick="controller.goToEditForm(self.parent.parent.value)"/>
<button sclass="icono" image="/common/img/ico_borrar1.png"
hoverImage="/common/img/ico_borrar.png"
tooltiptext="${i18n:_('Delete')}"
onClick="controller.confirmDelete(self.parent.parent.value)"/>
</hbox>
</row>
</rows>
</grid>
<button label="${i18n:_('Create')}" onClick="controller.goToCreateForm()"
sclass="create-button global-action"/>
</window>
In the next paragraphs different parts of the file will be reviewed.
::
<grid id="listStretchesFunctionTemplates"
model="@{controller.stretchesFunctionTemplates}"
``Grid`` is a visual ZK component with some sorting features. As you can see,
``model`` attribute is set, which means that a method called
``getStretchesFunctionTemplates`` in controller will be called. This method
will have the responsibility to communicate with model layer in order to get the
list of ``StretchesFunctionTemplate`` from database.
::
<column label="${i18n:_('Name')}" sort="auto(lower(name))" />
Thanks to this custom component you are able to define that *Name* column will
by sorted by default in ascending order.
::
<row self="@{each='stretchesFunctionTemplate'}"
value="@{stretchesFunctionTemplate}">
With this line you are doing 2 different things:
* Define a variable to represent each instance in the collection defined at
``model`` attribute. It uses ``self`` for this and set the name
``stretchesFunctionTemplate`` that will only be seen by this component and its
children.
* Set value for ``Row`` to current ``StretchesFunctionTemplate`` being iterated.
This will allow to access associated entity for each row in the list.
::
<label value="@{stretchesFunctionTemplate.name}" />
This line will access to ``name`` attribute for entity
``StretchesFunctionTemplate`` and show it as a label.
::
<button sclass="icono" image="/common/img/ico_editar1.png"
hoverImage="/common/img/ico_editar.png"
tooltiptext="${i18n:_('Edit')}"
onClick="controller.goToEditForm(self.parent.parent.value)"/>
An edit button is added for each row, and ``onClick`` event is associated with a
call to some method in the controller. In this case the method called is
``goToEditForm`` and argument is the ``StretchesFunctionTemplate`` associated
with current row. In order to access to the entity go to parent components till
``Row`` and get value there. There is also a delete button with similar
implementation.
::
<button label="${i18n:_('Create')}" onClick="controller.goToCreateForm()"
sclass="create-button global-action"/>
The last part is another button which will call a different method on controller
in order to show create form for a new ``StretchesFunctionTemplate`` entity.
To sum up, this ``.zul`` file will create a very simple list with the name of
each ``StretchesFunctionTemplate`` and buttons to edit or remove items in each
row. And also adds another button which will allow to create new entities.
Edit/Create view
----------------
Now you are going to create a file called
``_editStretchesFunctionTemplate.zul``, this file defines the form to create and
edit ``StretchesFunctionTemplate`` entities. It is used for both creation and
edition process. The file will have the following content:
::
<window id="${arg.id}">
<caption id="caption" sclass="caption-title" />
<tabbox>
<tabs>
<tab label="${i18n:_('Edit')}" />
</tabs>
<tabpanels>
<tabpanel>
<grid fixedLayout="true">
<columns>
<column width="200px" />
<column />
</columns>
<rows>
<row>
<label value="${i18n:_('Name')}" />
<textbox id="tbName"
value="@{controller.stretchesFunctionTemplate.name}"
width="300px"
onBlur="controller.updateWindowTitle()" />
</row>
</rows>
</grid>
<groupbox closable="false">
<caption label="${i18n:_('Stretches')}" />
<vbox>
<hbox align="center">
<label value="${i18n:_('New stretch:')}" />
<label value="${i18n:_('Duration Percentage')}" />
<intbox id="durationPercentage" width="50px"
value="0" onOK="controller.addStretchTemplate();" />
<label value="${i18n:_('Progress Percentage')}" />
<intbox id="progressPercentage" width="50px"
value="0" onOK="controller.addStretchTemplate();" />
<button id="add_new_stretch_template" label="${i18n:_('Add')}"
onClick="controller.addStretchTemplate();" />
</hbox>
</vbox>
<grid id="stretchTemplates"
model="@{controller.stretchTemplates}"
rowRenderer="@{controller.stretchTemplatesRenderer}"
mold="paging" pageSize="10" fixedLayout="true">
<columns>
<column label="${i18n:_('Duration Percentage')}" />
<column label="${i18n:_('Progress Percentage')}" />
<column label="${i18n:_('Operations')}" />
</columns>
</grid>
</groupbox>
</tabpanel>
</tabpanels>
</tabbox>
<!-- Control buttons -->
<button onClick="controller.saveAndExit()"
label="${i18n:_('Save')}"
sclass="save-button global-action" />
<button onClick="controller.saveAndContinue()"
label="${i18n:_('Save and Continue')}"
sclass="save-button global-action" />
<button onClick="controller.cancelForm()"
label="${i18n:_('Cancel')}"
sclass="cancel-button global-action" />
</window>
Now, let's take a look to the most important parts of the file.
::
<label value="${i18n:_('Name')}" />
<textbox id="tbName"
value="@{controller.stretchesFunctionTemplate.name}"
width="300px"
onBlur="controller.updateWindowTitle()" />
This will create a ``Textbox`` field in the form. As you can see, it is using
data bindings, which means that different methods will be automatically called
for get and set ``name`` attribute of entity.
In this case, first method ``getStretchesFunctionTemplate`` in controller will
be called, which will return current entity being edited or created. Then
method ``getName`` or ``setName`` of entity will be called as appropriate.
::
<label value="${i18n:_('New stretch:')}" />
<label value="${i18n:_('Duration Percentage')}" />
<intbox id="durationPercentage" width="50px"
value="0" onOK="controller.addStretchTemplate();" />
<label value="${i18n:_('Progress Percentage')}" />
<intbox id="progressPercentage" width="50px"
value="0" onOK="controller.addStretchTemplate();" />
<button id="add_new_stretch_template" label="${i18n:_('Add')}"
onClick="controller.addStretchTemplate();" />
In order to define new ``StretchTemplate`` for current entity, some fields are
added. Two ``Intbox`` fields and a button, all of them associated to
``addStretchTemplate`` method in controller that will be called to perform the
operation.
::
<grid id="stretchTemplates"
model="@{controller.stretchTemplates}"
rowRenderer="@{controller.stretchTemplatesRenderer}"
mold="paging" pageSize="10" fixedLayout="true">
<columns>
<column label="${i18n:_('Duration Percentage')}" />
<column label="${i18n:_('Progress Percentage')}" />
<column label="${i18n:_('Operations')}" />
</columns>
</grid>
List of ``StretchTemplate`` will be shown inside a ``Grid``. You define
``model`` just like in ``_listStretchesFunctionTemplates.zul`` but for
``StretchTemplate`` entities in this case. However, you are not using ``Row``
elements, instead, you are setting ``rowRenderer`` attribute that will
call to a method in controller. This method will return a ``RowRenderer`` that
will know how to show information about a ``StretchTemplate``.
Conversation model
==================
Model always contains state variables which are being modified by use case. For
example, model for CRUD use case, that is going to allow manage
``StretchesFunctionTemplate`` entities, will have a **conversation state** with
the current entity being created or edited. The series of steps that modify the
entity are called **conversation**.
Every conversation has a starting point and an ending one. Class ``XXXModel`` is
in charge of implement the conversation. Similar to what happens in DAOs case,
models will always implement an interface ``IXXXModel``, which will define
conversation steps. In LibrePlan there are some kind of naming conventions in
order to implement conversations.
.. ADMONITION:: Conversation naming conventions
In order to name the steps of a conversation it is recommended to use the
following conventions:
* If there is only one operation which starts the conversation, then name
``init`` should be used (e.g. ``IXXXModel::init``). If conversation can
start with different operations, names will be prefixed with ``init`` (e.g.
``IXXXModel::initCreate``, ``IXXXModel::initEdit``, etc.).
* If there is only one operation to successfully finish conversation, then
name ``confirm`` should be used (e.g. ``IXXXModel::confirm``). If it is
possible to end conversation successfully with different operations, names
will be prefixed with ``confirm`` (e.g. ``IXXXModel::confirmSave``,
``IXXXModel::confirmRemove``, etc.).
* Operation to cancel changes will be called ``cancel`` (e.g.
``IXXXModel::cancel``).
Usually when defining models you should add documentation about conversation
protocol:
Conversation state
Entity (or entities) being manipulated in the conversation. In some cases
other different objects will be kept in memory if needed.
Non conversational steps (or independent steps)
Specify operations not involved in conversation.
Conversation protocol
* Initial step: Indicates (exclusive) operations which allow start a
conversation.
* Intermediate steps: Specify methods that are invoked once the conversation
is started and before the end step is executed.
* End step: Set of (exclusive) operations which finish the conversation.
The application uses ``session-per-request-with-detached-objects`` pattern, it
is a way to implement the conversation model. This is usually valid for
applications that extract data from a database, user make some operations with
this data (*think-time*), and after that they are stored in database.
In the project when you start an edition conversation, you will retrieve an
entity from database (through DAO) and keep it in memory as state variable in
model (conversation state). This variable will be detached, i.e., a stored
variable that is not inside a Hibernate session). Hibernate allows to modify
detached entity out of a session. But, after that, it should be needed to open a
transaction in order to store entity on database.
You should be careful working with detached objects because of you could easily
get errors like: ``ObjectNotBoundInSession``, ``LazyInitializationException``
(trying to access a entity marked as lazy) or ``DuplicateSessionInObject`` (two
objects of same instance in the same session).
On the contrary, ``session-per-conversation`` pattern always keep Hibernate
session open, so there will be no objects with detached state. This pattern is
suitable for applications with low *think-time*.
Communication between model and controller
------------------------------------------
Following the approach explained before, in this use case you are going to
have a model ``StretchesFunctionTemplateModel`` and its interface called
``IStretchesFunctionTemplateModel``. That means that you will have two new files
inside
``libreplan-webapp/src/main/java/org/libreplan/web/planner/allocation/streches/``
folder:
* ``IStretchesFunctionTemplateModel.java``::
package org.libreplan.web.planner.allocation.streches;
...
public interface IStretchesFunctionTemplateModel {
...
}
* ``StretchesFunctionTemplateModel.java``::
package org.libreplan.web.planner.allocation.streches;
...
@Service
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
@OnConcurrentModification(goToPage = "/planner/stretchesFunctionTemplate.zul")
public class StretchesFunctionTemplateModel implements
IStretchesFunctionTemplateModel {
...
}
As you can see, model is a Spring bean, in order that controller communicates
with model, you need to do two different things:
* Add the following line at ``.zul`` page (this is not really needed because of
this line is already in ``template.zul``)::
<?variable-resolver class="org.zkoss.zkplus.spring.DelegatingVariableResolver"?>
* Add a new ``IStretchesFunctionTemplateModel`` type attribute in controller
class (``StretchesFunctionTemplateCRUDController``). This attribute must be
called ``stretchesFunctionTemplateModel``, because of ``variable-resolver``
will get the object from Spring based on name::
private IStretchesFunctionTemplateModel stretchesFunctionTemplateModel;
This is the way provided by ZK to do something similar to dependency injection,
in order to use model from controller (which is not inside Spring context). This
is why ``@Autowired`` is not needed, but on the other hand you need to use a
specific name for variable.
.. NOTE::
Model classes are defined with prototype scope::
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
The reason is that models are going to keep conversation state in a variable,
so in that case new instance are going to be needed every time model is used.
Developing the conversation
---------------------------
At this point you are going to start to develop controller and model in order to
implement the use case.
Non conversational step
.......................
For example you could start to work in the list view, if you review
``_listStretchesFunctionTemplates.zul`` code you will see that method
``getStretchesFunctionTemplates`` in controller is going to be called.
Implementation for this method is usually simple and similar to the next
example.
* ``StretchesFunctionTemplateCRUDController``::
public List<StretchesFunctionTemplate> getStretchesFunctionTemplates() {
return stretchesFunctionTemplateModel.getStretchesFunctionTemplates();
}
* ``IStretchesFunctionTemplateModel``::
/*
* Non conversational steps
*/
List<StretchesFunctionTemplate> getStretchesFunctionTemplates();
* ``StretchesFunctionTemplateModel``::
@Override
@Transactional(readOnly = true)
public List<StretchesFunctionTemplate> getStretchesFunctionTemplates() {
return stretchesFunctionTemplateDAO.getAll();
}
As you can see, you need to use ``@Transactional`` annotation in
``getStretchesFunctionTemplates`` method. This is needed in order to access DAO
object inside Hibernate session in order to get entities from database. If you
just need to query data, like in this case, you should mark transaction as read
only (``@Transactional(readOnly = true)``). Moreover, method
``getStretchesFunctionTemplates`` in model is not involved in conversation
protocol.
On the other hand, you will also need to implement ``getAll`` method in DAO that
would be quite simple::
@Override
public List<StretchesFunctionTemplate> getAll() {
return list(StretchesFunctionTemplate.class);
}
Conversational steps
....................
Now you are going to implement the form to create a new
``StretchesFunctionTemplate``. As you can see in the ``.zul`` page, the method
called in order to create a new entity is ``goToCreateForm``. This method will
start the conversation between controller and model, and it's already
implemented in ``BaseCRUDController``::
/**
* Show edit form with different title depending on controller state and
* reload bindings
*/
protected void showEditWindow() {
getVisibility().showOnly(editWindow);
updateWindowTitle();
Util.reloadBindings(editWindow);
}
...
/**
* Show create form. Delegate in {@link #initCreate()} that should be
* implemented in subclasses.
*/
public final void goToCreateForm() {
state = CRUDControllerState.CREATE;
initCreate();
showEditWindow();
}
/**
* Performs needed operations to initialize the creation of a new entity.
*/
protected abstract void initCreate();
.. NOTE::
Method ``Util::reloadBindings`` forces reload of bindings used in a component.
For example, this is needed to refresh a list of items when some of them are
added or removed.
This method delegates in ``initCreate`` that should be implemented in
``StretchesFunctionTemplateCRUDController``. Moreover it opens ``editWindow``
and then reload information in the form.
Implementation of ``initCreate`` in ``StretchesFunctionTemplateConverter``::
@Override
protected void initCreate() {
stretchesFunctionTemplateModel.initCreate();
}
Then this method calls to ``initCreate`` in model to start the conversation.
Then you need to add the following lines in model (remember to create method in
interface too)::
/**
* Conversation state
*/
private StretchesFunctionTemplate stretchesFunctionTemplate;
...
/*
* Initial conversation steps
*/
@Override
public void initCreate() {
this.stretchesFunctionTemplate = StretchesFunctionTemplate.create("");
}
Thanks to the first line, model will keep in memory current entity being created
or edited. As you can see in the method, a new instance of the entity is created
and assigned to state variable.
As you are using data bindings for ``StretchesFunctionTemplate`` name, then when
user modify this field, attribute ``name`` in entity will be automatically set.
In order to allow users add new ``StretchTemplate`` you need to implement method
``addStretchTemplate`` in controller. As usual this method delegates in model in
oder to perform the real operation. You need to override ``doAfterCompose`` at
``StretchesFunctionTemplateCRUDController`` in order to be able to access input
elements in the form and create the new method::
private Grid stretchTemplates;
private Intbox durationPercentage;
private Intbox progressPercentage;
...
@Override
public void doAfterCompose(Component comp) throws Exception {
super.doAfterCompose(comp);
stretchTemplates = (Grid) editWindow.getFellow("stretchTemplates");
durationPercentage = (Intbox) stretchTemplates
.getFellow("durationPercentage");
progressPercentage = (Intbox) stretchTemplates
.getFellow("progressPercentage");
}
...
public static BigDecimal HUNDRED = BigDecimal.valueOf(100);
public void addStretchTemplate() {
BigDecimal duration = BigDecimal.valueOf(durationPercentage.getValue())
.divide(HUNDRED);
BigDecimal progress = BigDecimal.valueOf(progressPercentage.getValue())
.divide(HUNDRED);
stretchesFunctionTemplateModel.addStretchTemplate(duration, progress);
clearStrechTemplateFields();
Util.reloadBindings(stretchTemplates);
}
private void clearStrechTemplateFields() {
durationPercentage.setValue(0);
progressPercentage.setValue(0);
}
In model, you will need to create an intermediate conversation step, that will
modify current ``StretchesFunctionTemplate`` entity adding a new instance of
``StretchTemplate``::
@Override
public void addStretchTemplate(BigDecimal durationProportion,
BigDecimal progressProportion) {
stretchesFunctionTemplate.addStretch(StretchTemplate.create(
durationProportion, progressProportion));
}
Then you need to implement a ``RowRenderer`` in controller to be used in order
to show ``StrechTemplate`` information in the window::
public RowRenderer getStretchTemplatesRenderer() {
return new RowRenderer() {
@Override
public void render(Row row, Object data) throws Exception {
final StretchTemplate stretchTemplate = (StretchTemplate) data;
row.appendChild(new Label(toStringPercentage(stretchTemplate
.getDurationProportion())));
row.appendChild(new Label(toStringPercentage(stretchTemplate
.getProgressProportion())));
row.appendChild(Util.createRemoveButton(new EventListener() {
@Override
public void onEvent(Event event) throws Exception {
stretchesFunctionTemplateModel
.removeStretchTemplate(stretchTemplate);
}
}));
}
private String toStringPercentage(BigDecimal value) {
return value.multiply(HUNDRED).toBigInteger().toString() + " %";
}
};
}
You will implement ``RowRenderer`` interface, and add the different components
for each column in ``Row`` element. Moreover, you will create a remove button,
associated with a new method in the model which removes the ``StretchTemplate``.
The last step is to close the conversation successfully or not. For this there
are already several methods in ``BaseCRUDController``::
/**
* Save current form and go to list view. Delegate in {@link #save()} that
* should be implemented in subclasses.
*/
public final void saveAndExit() {
try {
saveCommonActions();
goToList();
} catch (ValidationException e) {
messagesForUser.showInvalidValues(e);
}
}
/**
* Common save actions:<br />
* <ul>
* <li>Delegate in {@link #beforeSaving()} that could be implemented if
* needed in subclasses.</li>
* <li>Use {@link ConstraintChecker} to validate form.</li>
* <li>Delegate in {@link #save()} that should be implemented in subclasses.
* </li>
* <li>Show message to user.</li>
* </ul>
*
* @throws ValidationException
* If form is not valid or save has any validation problem
*/
private void saveCommonActions() throws ValidationException {
beforeSaving();
save();
messagesForUser.showMessage(
Level.INFO,
_("{0} \"{1}\" saved", getEntityType(), getEntityBeingEdited()
.getHumanId()));
}
/**
* Save current form and continue in edition view. Delegate in
* {@link #save()} that should be implemented in subclasses.
*/
public final void saveAndContinue() {
try {
saveCommonActions();
goToEditForm(getEntityBeingEdited());
} catch (ValidationException e) {
messagesForUser.showInvalidValues(e);
}
}
/**
* Performs additional operations before saving (usually do some checks or
* generate codes of related entities).
*
* Default behavior use {@link ConstraintChecker} to see if
* {@link #editWindow} is valid, however it could be overridden if needed.
*/
protected void beforeSaving() throws ValidationException {
ConstraintChecker.isValid(editWindow);
}
/**
* Performs actions to save current form
*
* @throws ValidationException
* If entity is not valid
*/
protected abstract void save() throws ValidationException;
/**
* Close form and go to list view. Delegate in {@link #cancel()} that could
* be implemented in subclasses if needed.
*/
public final void cancelForm() {
cancel();
goToList();
}
/**
* Performs needed actions to cancel edition
*
* Default behavior do nothing, however it could be overridden if needed.
*/
protected void cancel() {
// Do nothing
}
Then you will need to implement the following methods in controller. ``cancel``
is not mandatory but here is used to show an example about it::
@Override
protected void save() throws ValidationException {
stretchesFunctionTemplateModel.confirmSave();
}
@Override
protected void cancel() {
stretchesFunctionTemplateModel.cancel();
}
``save`` method will call ``confirmSave`` in model and return true if the
operation is properly performed. Then depending if user will stay or not in
current window, a different operation is done. ``cancel`` method again will
delegate in model calling ``cancel``. Then two new methods will appear in
model::
@Override
@Transactional
public void confirmSave() throws ValidationException {
stretchesFunctionTemplateDAO.save(stretchesFunctionTemplate);
}
@Override
public void cancel() {
stretchesFunctionTemplate = null;
}
As you can see, now ``@Transactional`` is used in ``confirmSave`` method without
read only attribute as this operation is going to store entity on database.
All these steps will carry out a complete conversation in LibrePlan. In this
case this conversation will allow users to create new
``StretchesFunctionTemplate`` entities and store them on database (if they do
not cancel the operation).
Solving issues with detached objects
------------------------------------
As it was already stated, you need to be careful managing **detached objects**.
For example, if you think in edit an already stored
``StretchesFunctionTemplate``, you will have a very similar method to
``goToCreateForm`` in ``BaseCRUDController``::
/**
* Show edit form for entity passed as parameter. Delegate in
* {@link #initEdit(entity)} that should be implemented in subclasses.
*
* @param entity
* Entity to be edited
*/
public final void goToEditForm(T entity) {
state = CRUDControllerState.EDIT;
initEdit(entity);
showEditWindow();
}
/**
* Performs needed operations to initialize the edition of a new entity.
*
* @param entity
* Entity to be edited
*/
protected abstract void initEdit(T entity);
And the specific implementation in ``StretchesFunctionTemplateCRUDController``::
@Override
protected void initEdit(StretchesFunctionTemplate stretchesFunctionTemplate) {
stretchesFunctionTemplateModel.initEdit(stretchesFunctionTemplate);
}
Then a new method called ``initEdit`` will appear in model as initial
conversation step. First you could think in create this method as follows::
@Override
public void initEdit(StretchesFunctionTemplate stretchesFunctionTemplate) {
this.stretchesFunctionTemplate = stretchesFunctionTemplate;
}
In that case you will get a ``LazyInitializationException`` with the following
message:
.. pull-quote::
Run-time error: failed to lazily initialize a collection of role:
org.libreplan.business.planner.entities.StretchesFunctionTemplate.stretches,
no session or session was closed . Error was registered and it will be fixed as
soon as possible.
This is because of ``editWindow`` is calling ``getStretchTemplates``, that at
some point will end up calling ``getStretches`` on entity. This collection is
a proxy because by default Hibernate relations are lazy. You have two different
approaches to fix this issue:
a) Add ``@Transactional`` annotation to open Hibernate session, reattach entity
(i.e. put on session currently detached entity) and navigate the collection
to avoid proxies::
@Override
@Transactional(readOnly = true)
public void initEdit(StretchesFunctionTemplate stretchesFunctionTemplate) {
this.stretchesFunctionTemplate = stretchesFunctionTemplate;
stretchesFunctionTemplateDAO.reattach(this.stretchesFunctionTemplate);
this.stretchesFunctionTemplate.getStretches().size();
}
b) Or modify entity mapping to avoid lazy relation::
<list name="stretches" table="stretch_template" lazy="false">
The option chosen will depend on each specific case and you should select the
more convenient way. If every time you load the entity you are going to access
to relation then changing the mapping will be the best solution. Otherwise, if
you are just going to retrieve entities and show name information in the listing
(as you are doing till this moment) you could prefer not load the relation and
then select the other option.
Testing
=======
LibrePlan uses JUnit_ testing framework, as a tool to check application
behaviour based on **unit tests**. The main classes tested are: entities, models
and DAOs. These tests are executed automatically when project is compiled, thus
allow developers check that their changes do not break other parts of
application.
It is strongly recommended to create test for entities and models, in order to
ensure that business logic is working properly.
.. ADMONITION:: Test Driven Development
Project developers usually follow, although not strictly, TDD_ while
programming use cases. The main idea behind TDD is:
* First write a test to define a expected feature in a class. At this moment,
this is going to be a failing test.
* After that, modify class to fulfill requirements specified by test.
Producing code to pass the test.
An example of unit test
-----------------------
For example, in order to test that defined mapping is right you can define a
test for your DAO. Simply create the following file called
``StretchesFunctionTemplateDAOTest.java`` in
``libreplan-business/src/test/java/org/libreplan/business/test/planner/daos/``:
::
package org.libreplan.business.test.planner.daos;
...
@Transactional
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { BUSINESS_SPRING_CONFIG_FILE,
BUSINESS_SPRING_CONFIG_TEST_FILE })
public class StretchesFunctionTemplateDAOTest {
@Autowired
private IStretchesFunctionTemplateDAO dao;
private StretchesFunctionTemplate stretchesFunctionTemplate;
private void givenValidStretchesFunctionTemplate() {
stretchesFunctionTemplate = StretchesFunctionTemplate
.create("stretches-function-template-name-"
+ UUID.randomUUID());
stretchesFunctionTemplate.addStretch(StretchTemplate.create(
new BigDecimal(0.25), new BigDecimal(0.1)));
stretchesFunctionTemplate.addStretch(StretchTemplate.create(
new BigDecimal(0.75), new BigDecimal(0.9)));
}
@Test
public void afterSavingAStretchesFunctionTemplateItExists() {
givenValidStretchesFunctionTemplate();
dao.save(stretchesFunctionTemplate);
assertTrue(dao.exists(stretchesFunctionTemplate.getId()));
}
}
As you can see, you need some Spring annotations to run test inside a Spring
context in order to be able to use ``@Autowired`` for different Spring beans, in
that case the DAO class.
Methods annotated with ``@Test`` will be the ones executed in order to check
different things with methods like ``assertTrue``.
Validation
----------
In all applications you usually need to validate different data in several
situations. In order to avoid duplicate validations in different layers,
validation logic should take place in domain model. LibrePlan uses `Hibernate
Validator`_ for this task.
Basic validations
.................
Entities should be in charge to validate themselves, which means that some
validations should be done in entities. For example,
``StretchesFunctionTemplate`` needs to have a name, then you will add following
annotation::
@NotEmpty(message = "name not specified or empty")
public String getName() {
return name;
}
.. NOTE::
The different validation annotations like ``@NotNull``, ``@NotEmpty``,
``@Valid``, etc. should be in ``getXXX`` methods, instead of variables, in
order to avoid proxies when trying to validate entities, because of lazy
initialization in Hibernate.
Then you could add the following test to check that
``StretchesFunctionTemplate`` without name are not stored in database::
@Test(expected = ValidationException.class)
public void tryToSaveStretchesFunctionTemplateWithoutName() {
stretchesFunctionTemplate = StretchesFunctionTemplate.create("");
dao.save(stretchesFunctionTemplate);
}
As you can see here, it is being checked that a ``ValidationException`` is
thrown when it is trying to store an entity with empty name.
Validating related entities
...........................
Let's go a bit further and try to also validate ``StretchTemplate`` entity,
which is used by ``StretchesFunctionTemplate``, in order to check that values
for proportions should be between 0 and 1. Then you could think in define the
following unit test::
@Test(expected = ValidationException.class)
public void tryToSaveStretchesFunctionTemplateWithoutNullStretchTemplate() {
stretchesFunctionTemplate = StretchesFunctionTemplate
.create("stretches-function-template-name-" + UUID.randomUUID());
stretchesFunctionTemplate.addStretch(StretchTemplate.create(
BigDecimal.TEN, BigDecimal.TEN));
dao.save(stretchesFunctionTemplate);
}
If you run this test now it is going to fail as not exception is going to be
thrown. Then you will add ``@Min`` and ``@Max`` annotations to these attributes
in class ``StretchTemplate``::
@Min(value = 0, message = "duration proportion is one based percentage so it "
+ "should be greater than or equal to 0")
@Max(value = 1, message = "duration proportion is one based percentage so it "
+ "should be less than or equal to 1")
public BigDecimal getDurationProportion() {
return durationProportion;
}
@Min(value = 0, message = "progress proportion is one based percentage so it "
+ "should be greater than or equal to 0")
@Max(value = 1, message = "progress proportion is one based percentage so it "
+ "should be less than or equal to 1")
public BigDecimal getProgressProportion() {
return progressProportion;
}
Anyway, test is going to keep failing and you are not getting any
``ValidationException`` yet. This is because of relations are not automatically
navigated during validation, you need to manually specify ``@Valid`` annotation
in order to also validate depending entities. So, you just need to modify
``StretchesFunctionTemplate`` to add the annotation and then test would be
successfully passed::
@Valid
public List<StretchTemplate> getStretches() {
return Collections.unmodifiableList(stretches);
}
Complex validations
...................
Sometimes you need more complex validations than simply check if a field is
empty or it has some value, in this case you will have to use ``@AssertTrue``
annotation. There is a convention in LibrePlan for methods annotated with
``@AssertTrue`` that names should start with ``checkConstraint`` prefix.
For example, maybe you want to check that inside a ``StretchesFunctionTemplate``
different ``StretchTemplate`` are correlative. E.g. if you have a stretch with
duration 20% and progress 50%, next stretch should have a greater or equal
progress; then a new stretch with duration 40% and progress 30% is not valid it
should be at least 50% of progress or a greater value.
Then if you follow TDD, you could add a new test to check if this issue is being
properly validated::
@Test(expected = ValidationException.class)
public void checkStretchesProgressOrder() {
stretchesFunctionTemplate = StretchesFunctionTemplate
.create("stretches-function-template-name-" + UUID.randomUUID());
stretchesFunctionTemplate.addStretch(StretchTemplate.create(
new BigDecimal(0.20), new BigDecimal(0.50)));
stretchesFunctionTemplate.addStretch(StretchTemplate.create(
new BigDecimal(0.40), new BigDecimal(0.30)));
dao.save(stretchesFunctionTemplate);
}
In order to implement this behaviour you will add following method in
``StretchesFunctionTemplate`` entity::
@AssertTrue(message = "Some stretch has less progress value than the "
+ "previous stretch")
public boolean checkConstraintStretchesProgressOrder() {
if (stretches.isEmpty()) {
return true;
}
sortStretchesByDuration();
Iterator<StretchTemplate> iterator = stretches.iterator();
StretchTemplate previous = iterator.next();
while (iterator.hasNext()) {
StretchTemplate current = iterator.next();
if (current.getProgressProportion().compareTo(
previous.getProgressProportion()) <= 0) {
return false;
}
previous = current;
}
return true;
}
private void sortStretchesByDuration() {
Collections.sort(stretches, new Comparator<StretchTemplate>() {
@Override
public int compare(StretchTemplate o1, StretchTemplate o2) {
return o1.getDurationProportion().compareTo(
o2.getDurationProportion());
}
});
}
At this moment, when a ``StretchesFunctionTemplate`` entity is stored on
database, this constraint will be checked in order to avoid save wrong data.
.. WARNING::
The project uses a special approach for validating objects when saving, it is
defined in ``GenericDAOHibernate``::
/**
* It's necessary to save and validate later.
*
* Validate may retrieve the entity from DB and put it into the Session, which can eventually lead to
* a NonUniqueObject exception. Save works here to reattach the object as well as saving.
*/
public void save(E entity) throws ValidationException {
getSession().saveOrUpdate(entity);
entity.validate();
}
As you can see, before validating the entity the application saves it and then
checks that all validations run successfully. This could lead to some
"strange" results while developing test.
For example, if you are testing that a value could not be ``null`` and it is
defined with a ``not-null`` constraint in database mapping. You will add
``@NotNull`` annotation and create a test expecting a ``ValidationException``.
However, as LibrePlan is not able to store in database the entity (because of
a database constraint) you will always get a
``DataIntegrityViolationException``.
Interface validations
---------------------
Even, when it is already stated that validations have to be done in domain
entities, in order to check business logic in proper layer and avoid possible
issues because of wrong data is stored on database. It is also possible to
replicate some of these validations in view layer, in order to show to users
better error messages and prevent them to send invalid data to server.
ZK provides an easy way to add constraints to form fields. For example, in
``StretchesFunctionTemplate`` entity name can not be empty so you could add the
following validation on ``_editStretchesFunctionTemplate.zul`` file::
<label value="${i18n:_('Name')}" />
<textbox id="tbName"
value="@{controller.stretchesFunctionTemplate.name}"
width="300px"
onBlur="controller.updateWindowTitle()"
constraint="no empty:${i18n:_('cannot be empty')}" />
Now, if users set an empty name, they will receive an error in a pop-up. However,
if they click *Save* button, the request to sever will be sent and then they
will get validations errors due to Hibernate constraints.
In order to do not follow with the conversation when user has not filled the
right data ``BaseCRUDController`` uses ``ConstraintChecker`` utility in default
``beforeSaving`` method::
/**
* Performs additional operations before saving (usually do some checks or
* generate codes of related entities).
*
* Default behavior use {@link ConstraintChecker} to see if
* {@link #editWindow} is valid, however it could be overridden if needed.
*/
protected void beforeSaving() throws ValidationException {
ConstraintChecker.isValid(editWindow);
}
Web services
============
LibrePlan provides **web services** as integration tool for third party
applications that want to get/send data from/to application. Project
implementation to perform this task is based in REST_ (Representational State
Transfer) services with the following behaviour:
* All integration entities will have a code that will allow them to be
identified for both LibrePlan and third party application. It is important to
stress that this ``code`` attribute will be different to Hibernate ``id``
attribute, which is an internal identifier for the database and could be
repeated in different instances of LibrePlan.
* When the application receive an entity via web service, it follows the next
steps:
* Check if entity already exists on database. Using ``code`` attribute for
this.
* If entity does not exist, then it is created the new entity and stored on
database.
* If entity already exists, then it is modified and changes are stored on
database.
* Delete operation is not going to be allowed, because of remove some entity
could take side effects in other schedules done in LibrePlan. Anyway, it is
possible that some entities provide an attribute to deactivate them in the
system, this could be changed with a modification operation.
Application entities will be represented as XML files in order to be sent or
received as web service data.
Convert into ``IntegrationEntity``
----------------------------------
A lot of entities in the project can be considered **integration entities**,
i.e. suitable entities to be sent/received to/from other applications. As this
is a common case a new class ``IntegrationEntity`` was defined and all these
entities extends this class instead of ``BaseEntity``. Actually,
``IntegrationEntity`` extends in turn ``BaseEntity``.
For example, as part of this exercise you are going to become
``StretchesFunctionTemplate`` in an integration entity. Even when it could not
have be really needed for the moment, it is useful as a test case in order to
know how to develop a web service in the application.
First of all, you need to make that ``StretchesFunctionTemplate`` inherits from
``IntegrationEntity``::
public class StretchesFunctionTemplate extends IntegrationEntity implements
IHumanIdentifiable {
...
}
This fact means that ``StretchesFunctionTemplate`` entity has a new attribute
called ``code``. Thus, you will need to modify Hibernate mapping in
``ResourceAllocations.hbm.xml`` file in order to add the following line::
<property name="code" access="property" not-null="true" unique="true"/>
And you will need to add a new changeset to Liquibase changelog in
``db.changelog-1.0.xml`` file::
<changeSet id="add-new-column-code-to-stretches_function_template" author="mrego">
<comment>Add new column code in table stretches_function_template with not-null constraint</comment>
<addColumn tableName="stretches_function_template">
<column name="code" type="VARCHAR(255)" />
</addColumn>
<addNotNullConstraint tableName="stretches_function_template"
columnName="code"
defaultNullValue=""
columnDataType="VARCHAR(255)" />
</changeSet>
.. WARNING::
This Liquibase changeset is just an example and should not be used as is in
the real world. The reason is that if there are already
``StretchesFunctionTemplate`` entities stored in database, this changeset is
setting ``code`` attribute to empty, which is wrong as code should be unique.
This should be fixed using some kind of custom refactorization provided by
Liquibase, that would generate random codes for currently stored entities.
``IntegrationEntity`` is an abstract class, thus you need to override abstract
method ``getIntegrationEntityDAO``. This method should return DAO of this
entity, that will be used to check that code is not repeated when entity is
validated.
However, before implementing this method you need to modify entity DAO to extend
``IntegrationEntityDAO``. This provides a standard implementation for several
methods in order to check constraints related with ``code`` field. In order to
do this you will need to modify both interface and DAO implementation::
public interface IStretchesFunctionTemplateDAO extends
IIntegrationEntityDAO<StretchesFunctionTemplate> {
...
}
public class StretchesFunctionTemplateDAO extends
IntegrationEntityDAO<StretchesFunctionTemplate> implements
IStretchesFunctionTemplateDAO {
...
}
It is very convenient to use these common classes as you will have a lot of
functionalities automatically added to your entity. Now, you are ready to
implement ``getIntegrationEntityDAO`` in the entity. Just one more problem, you
need to know how to access DAO from an entity, when entities are not in a Spring
context. For this purpose a class called ``Registry`` exists in LibrePlan, so
before modifying entity you will add the following lines to ``Registry``::
@Autowired
private IStretchesFunctionTemplateDAO stretchesFunctionTemplateDAO;
public static IStretchesFunctionTemplateDAO getStretchesFunctionTemplateDAO() {
return getInstance().stretchesFunctionTemplateDAO;
}
And then you will override ``getIntegrationEntityDAO`` in
``StretchesFunctionTemplate``::
@Override
protected IStretchesFunctionTemplateDAO getIntegrationEntityDAO() {
return Registry.getStretchesFunctionTemplateDAO();
}
At this moment, your entity ``StretchesFunctionTemplate`` is an integration
entity, so it is valid to implement a web service providing import and export
facilities for this entity.
.. NOTE::
Integration entities usually will show ``code`` attribute in the interface, in
order that users could uniquely reference to one entity. Moreover, this code
usually follows some kind of sequence prefixed with entity name, these
sequences are managed in *Configuration* window at LibrePlan.
So, if you want ``StretchesFunctionTemplate`` entity will be a common
integration entity in the application you will need to do something similar to
other entities:
* Add your entity in ``EntityNameEnum``.
* Modify *Configuration* window in order to allow manage the new sequence for
your entity.
* Reuse ``IIntegrationEntityModel`` and ``IntegrationEntityModel`` in your
model. Those will provide standard methods to generate entity sequence.
For the moment, as it is not really necessary for this exercise, this part
will be omitted in this document.
Implement export web service
----------------------------
Now you are going to implement the **export service** for
``StretchesFunctionTemplate`` entity. Thanks to this service, third party
applications could access to the list of ``StretchesFunctionTemplate`` defined
in the system. Web services classes are under
``libreplan-webapp/src/main/java/org/libreplan/ws/`` folder, inside it you
should create a new directory ``stretchesfunctiontemplates`` with two
subdirectories ``api`` and ``impl``.
Again, like in previous point, there are some classes already defined which
provide main functionality needed to implement the web service. You will extends
these classes throughout the sample.
Defining service interface
..........................
First of all, you will create an interface inside ``api`` folder. This interface
will define a method to export all ``StretchesFunctionTemplate`` entities stored
in application database::
package org.libreplan.ws.stretchesfunctiontemplates.api;
...
public interface IStretchesFunctionTemplateService {
public StretchesFunctionTemplateListDTO getStretchesFunctionTemplates();
}
Mapping between entities and XMLs
.................................
As you can see, web service interface uses a DTO_ (Data Transfer Object) class,
as you do not need to export all the business logic managed by entities you will
create lighter classes (DTOs) in order to export and import data associated with
web services.
Then you are going to define all DTOs (inside ``api`` folder), needed for
``StretchesFunctionTemplate`` entity. You will need three DTOs:
* ``StretchesFunctionTemplateListDTO``:
::
package org.libreplan.ws.stretchesfunctiontemplates.api;
...
@XmlRootElement(name = "stretches-function-template-list")
public class StretchesFunctionTemplateListDTO {
@XmlElement(name = "stretches-function-template")
public List<StretchesFunctionTemplateDTO> stretchesFunctionTemplateDTOs = new ArrayList<StretchesFunctionTemplateDTO>();
public StretchesFunctionTemplateListDTO() {
}
public StretchesFunctionTemplateListDTO(
List<StretchesFunctionTemplateDTO> stretchesFunctionTemplateDTOs) {
this.stretchesFunctionTemplateDTOs = stretchesFunctionTemplateDTOs;
}
}
* ``StretchesFunctionTemplateDTO``:
::
package org.libreplan.ws.stretchesfunctiontemplates.api;
...
public class StretchesFunctionTemplateDTO extends IntegrationEntityDTO {
public final static String ENTITY_TYPE = "stretches-function-template";
@XmlAttribute
public String name;
@XmlElementWrapper(name = "stretches-list")
@XmlElement(name = "stretch-template")
public List<StretchTemplateDTO> stretches = new ArrayList<StretchTemplateDTO>();
public StretchesFunctionTemplateDTO() {
}
public StretchesFunctionTemplateDTO(String code, String name,
List<StretchTemplateDTO> stretches) {
super(code);
this.name = name;
this.stretches = stretches;
}
public StretchesFunctionTemplateDTO(String name,
List<StretchTemplateDTO> stretches) {
this(generateCode(), name, stretches);
}
@Override
public String getEntityType() {
return ENTITY_TYPE;
}
}
* ``StretchesFunctionTemplateDTO``:
::
package org.libreplan.ws.stretchesfunctiontemplates.api;
...
public class StretchTemplateDTO {
@XmlAttribute(name = "duration-proportion")
public BigDecimal durationProportion;
@XmlAttribute(name = "progress-proportion")
public BigDecimal progressProportion;
public StretchTemplateDTO() {
}
public StretchTemplateDTO(BigDecimal durationProportion,
BigDecimal progressProportion) {
this.durationProportion = durationProportion;
this.progressProportion = progressProportion;
}
}
In these classes you can see that LibrePlan uses JAXB_ for XML bindings. This
makes really easy mapping between Java classes and XML representations providing
annotations like ``@XmlAttribute``, ``@XmlElement``, etc.
Moreover, you also need a file called ``package-info.java`` in ``api`` folder in
order to define namespace for REST service. This file will have the following
content::
@javax.xml.bind.annotation.XmlSchema(
elementFormDefault=javax.xml.bind.annotation.XmlNsForm.QUALIFIED,
namespace=WSCommonGlobalNames.REST_NAMESPACE
)
package org.libreplan.ws.stretchesfunctiontemplates.api;
import org.libreplan.ws.common.api.WSCommonGlobalNames;
Extending ``GenericRESTService``
................................
Then you are going to implement web service interface with a new class which
will extend ``GenericRESTService``. This class provides generic stuff for
implementing new REST services. So, you are going to create the following class
inside ``impl`` folder this time::
package org.libreplan.ws.stretchesfunctiontemplates.impl;
...
@Path("/stretchesfunctiontemplates/")
@Produces("application/xml")
@Service("stretchesFunctionTemplateServiceREST")
public class StretchesFunctionTemplateServiceREST extends
GenericRESTService<StretchesFunctionTemplate, StretchesFunctionTemplateDTO>
implements IStretchesFunctionTemplateService {
@Autowired
private IStretchesFunctionTemplateDAO dao;
@Override
protected IIntegrationEntityDAO<StretchesFunctionTemplate> getIntegrationEntityDAO() {
return dao;
}
@Override
protected StretchesFunctionTemplateDTO toDTO(
StretchesFunctionTemplate entity) {
return StretchesFunctionTemplateConverter.toDTO(entity);
}
@Override
protected StretchesFunctionTemplate toEntity(
StretchesFunctionTemplateDTO entityDTO) throws ValidationException,
RecoverableErrorException {
// Not needed for export service
return null;
}
@Override
protected void updateEntity(StretchesFunctionTemplate entity,
StretchesFunctionTemplateDTO entityDTO) throws ValidationException,
RecoverableErrorException {
// Not needed for export service
}
@Override
@GET
@Transactional(readOnly = true)
public StretchesFunctionTemplateListDTO getStretchesFunctionTemplates() {
return new StretchesFunctionTemplateListDTO(findAll());
}
}
Let's split this file in small hunks in order to explain different annotations.
Take into account that the project uses JAX-RS_ (Java API for RESTful Web
Services) to create web services.
::
@Path("/stretchesfunctiontemplates/")
It is a JAX-RS annotation to indicates the URI for the web service. In the
application it is the entity name in lowercase and plural usually.
::
@Produces("application/xml")
Another JAX-RS annotation which indicates the media type for a method. In this
case it is used at class level, which means that all methods for this web
service produce XML results. This is true in LibrePlan, as even methods to
import data will return an XML with the list of errors during the operation or
an empty list if it was performed successfully.
::
@Service("stretchesFunctionTemplateServiceREST")
In this case it is a Spring annotation which indicates that the class is a
service. Then you will need to add it in
``libreplan-webapp-spring-config.xml`` file::
<jaxrs:serviceBeans>
...
<ref bean="stretchesFunctionTemplateServiceREST"/>
</jaxrs:serviceBeans>
::
public class StretchesFunctionTemplateServiceREST extends
GenericRESTService<StretchesFunctionTemplate, StretchesFunctionTemplateDTO>
implements IStretchesFunctionTemplateService {
As you can see, new class extends ``GenericRESTService`` an abstract class that
provides common functionality for web services. It also implements web service
interface, where you indicate methods provided by this web service.
::
@Autowired
private IStretchesFunctionTemplateDAO dao;
Like this class is marked as a Spring service, you could use ``@Autowired``
annotation to inject DAO class for this entity.
::
@Override
protected IIntegrationEntityDAO<StretchesFunctionTemplate> getIntegrationEntityDAO() {
return dao;
}
This is an abstract method that you need to implement, it simply returns DAO
class for ``StretchesFunctionTemplate`` entity.
::
@Override
protected StretchesFunctionTemplateDTO toDTO(
StretchesFunctionTemplate entity) {
return StretchesFunctionTemplateConverter.toDTO(entity);
}
Another abstract method overridden, in this case it should create a DTO from an
entity. As you can see it delegates the conversion in a special class
``StretchesFunctionTemplateConverter``.
::
@Override
protected StretchesFunctionTemplate toEntity(
StretchesFunctionTemplateDTO entityDTO) throws ValidationException,
RecoverableErrorException {
// Not needed for export service
return null;
}
Similar to previous method, but on the other way around. This will create an
entity from a DTO. This is used when you are implementing an import service and
you receive new entities. Again, it usually delegates in a converter class.
::
@Override
protected void updateEntity(StretchesFunctionTemplate entity,
StretchesFunctionTemplateDTO entityDTO) throws ValidationException,
RecoverableErrorException {
// Not needed for export service
}
This will be used when you receive an already existent entity in order to update
it from a DTO. Like previous ones, it usually delegates in a converter class.
::
@Override
@GET
@Transactional(readOnly = true)
public StretchesFunctionTemplateListDTO getStretchesFunctionTemplates() {
return new StretchesFunctionTemplateListDTO(findAll());
}
Finally, this is the implementation for the only method provided by web service
interface, which will export all ``StretchesFunctionTemplate`` stored in
the system. Method is marked with ``@GET`` JAX-RS annotation, which indicates
that current method will respond to HTTP ``GET`` requests. Moreover, it is also
needed open a transaction, as it is going to use DAO in ``findAll`` method
implemented by ``GenericRESTService``.
Converting entities to/from DTOs
................................
The last step will be implement the converter class. In this case you will just
need to implement the method to convert from ``StretchesFunctionTemplate``
entity to a DTO.
This is a simply class that will be inside ``impl`` folder and will have the
following content::
package org.libreplan.ws.stretchesfunctiontemplates.impl;
...
public final class StretchesFunctionTemplateConverter {
private StretchesFunctionTemplateConverter() {
}
public final static StretchesFunctionTemplateDTO toDTO(
StretchesFunctionTemplate stretchesFunctionTemplate) {
// Convert stretches
List<StretchTemplateDTO> stretchTemplateDTOs = new ArrayList<StretchTemplateDTO>();
for (StretchTemplate each : stretchesFunctionTemplate.getStretches()) {
stretchTemplateDTOs.add(toDTO(each));
}
return new StretchesFunctionTemplateDTO(stretchesFunctionTemplate
.getCode(), stretchesFunctionTemplate.getName(), stretchTemplateDTOs);
}
private static StretchTemplateDTO toDTO(StretchTemplate stretchTemplate) {
return new StretchTemplateDTO(stretchTemplate.getDurationProportion(),
stretchTemplate.getProgressProportion());
}
}
Now you are ready to test your web service. If you go to this URL
http://localhost:8080/libreplan-webapp/ws/rest/stretchesfunctiontemplates/,
and login with a user that has permission to access web services (e.g. user
``wsreader`` with password ``wsreader``) you will get a XML with the list of
``StretchesFunctionTemplate`` stored in the application.
.. NOTE::
It is recommended to define some JUnit test for each web service in order to
validate if they are working properly. In this case it is even more important
as developers could not take into account web services when they are changing
some entities in the business logic. Thanks to this tests developers will
detect problems at the exact moment in which they are doing the changes.
You can take a look to other already existent services in order to create your
test for the new one that, following the convention, will be called
``StretchesFunctionTemplateServiceTest``.
Web services scripts
--------------------
Export services are easily tested just accessing URL with any web browser.
However, in the case of import services you will need to use HTTP ``POST``
method in order to test them. For this reason some convenient scripts were
created in project repository inside ``scripts/rest-clients`` directory.
.. NOTE::
Currently these scripts recommends Tidy to be installed in your system
for a better output. You could install them in a Debian based distribution
with the following command as root::
apt-get install tidy
Then for this example you will create a script called
``export-stretches-function-templates.sh``, that will be very similar to the
rest of export scripts just changing web service path::
#!/bin/sh
. ./rest-common-env.sh
. ./export.sh stretchesfunctiontemplates $*
Script will request user and password in order to access to web service, so you
could use ``wsreader`` user to check that it works properly.
Conclusion
==========
Proposed exercise, even when not fully resolved, allows navigate through basic
architecture of LibrePlan and know better different elements involved in each
layer.
Project view is implemented with ZK web framework. Views are stored in files
with ``.zul`` extension. It is possible grouping components into each other
and, even, create macro components in order to divide views or reuse components.
Every window is associated with a controller class. Controller contains program
source code needed to interact with user and communicate with business logic
layer. Every ``XXXController`` class will have access to business logic through
a ``XXXModel`` class.
Business logic layer is implemented in ``XXXModel`` classes. Domain entities,
apart from contain data which will be stored, also contain business operations.
The philosophy is that, as far as possible, every domain entity knows himself
and offers operations to other entities through its methods.
Model classes do not access directly to database, but do so through a DAO
(persistence class). There is a DAO for each domain entity. DAO offers different
operations for retrieval, query and store entities. Models are not going to
access to concrete classes, they will use interfaces (dependency injection).
Hibernate is the persistence framework used in the application. There is two
base classes ``GenericDAOHibernate`` and ``BaseEntity`` which encapsulate main
part of Hibernate API. Every DAOs and entities inherit from these two classes
respectively. Every domain entities must to have Hibernate mapping. Mapping is
done in ``.hbm.xml`` files.
LibrePlan uses testing framework JUnit. Moreover, Hibernate Validator is used to
validate business logic. Business logic is always tested and validated in model
layer, where entities are responsible to validate their own data.
Finally, you created an export web service for the new entity. Project
services uses XML APIs provided by Java (like JAXB and JAX-RS) which make easier
developing new web services. For each web service, some test scripts are
provided in order to check implementation.
.. _LibrePlan: http://www.libreplan.com/
.. _CRUD: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
.. _Hibernate: http://www.hibernate.org/
.. _`Optimistic Locking`: http://en.wikipedia.org/wiki/Optimistic_locking
.. _`LibrePlan wiki`: http://wiki.libreplan.org/
.. _MVC: http://en.wikipedia.org/wiki/Model_view_controller
.. _DAO: http://en.wikipedia.org/wiki/Data_Access_Object
.. _DDD: http://en.wikipedia.org/wiki/Domain_driven_design
.. _Spring: http://www.springsource.org/
.. _`Inversion of control`: http://en.wikipedia.org/wiki/Inversion_of_control
.. _`database refactorings`: http://en.wikipedia.org/wiki/Database_refactoring
.. _Liquibase: http://www.liquibase.org/
.. _ZK: http://www.zkoss.org/
.. _JUnit: http://junit.sourceforge.net/
.. _TDD: http://en.wikipedia.org/wiki/Test_driven_development
.. _`Hibernate Validator`: http://www.hibernate.org/subprojects/validator.html
.. _REST: http://en.wikipedia.org/wiki/Representational_State_Transfer
.. _DTO: http://en.wikipedia.org/wiki/Data_Transfer_Object
.. _JAXB: http://en.wikipedia.org/wiki/Java_Architecture_for_XML_Binding
.. _JAX-RS: http://en.wikipedia.org/wiki/Java_API_for_RESTful_Web_Services