miércoles, 29 de junio de 2011

How-to develop a ZKoss, Spring, Hibernate portlet for Liferay from scratch (I)

In this tutorial I will try to explain how to develop a portlet using the RIA ZKoss framework integrated with Spring and Hibernate. This portlet will hold also the needed configuration to be deployed in Liferay Portal.

This example will use a MySQL database but this could be easily changed in datasource property. This kind of portlet is checked and works with ZK5 and Liferay 6.0.x. It should work with Liferay 6+ version. In fact, it doesn't contain any specific Liferay library, it just contains the configuration files to be deployed as a Liferay Portlet. If you need to use the Liferay libraries to access the portal from your portlet, you should only need to put the Liferay libraries in the WEB-INF folder of your portlet.

This is the first part of the tutorial. The next one will contain a Maven archetype which will be useful to develop portlets easily.

1.- First of all, we have the following prerequisites:

-Install the Eclipse IDE. [1]

-Install ZKStudio from Eclipse Marketplace or using Eclipse Update Manager. [2]

-Download Spring libraries. [3]

-Download Hibernate libraries. [4]

-Download Apache Commons libraries. [5]

-Donwload and install Liferay 6.0.5 bundled with Tomcat (just if it's needed, if you already have a Liferay installation working, you could use that because this portlet works with hot-deploy). [6]

2.- We create a database in MySQL (or another Database Server) with one table:

DROP DATABASE IF EXISTS zkossportletsample;
CREATE DATABASE zkossportletsample;
CREATE TABLE famous_quote(
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
quote_text VARCHAR(255) NOT NULL
);


3.- In Eclipse IDE, we must create a ZK Project using the wizard. For this example we give the name: ZKPortletSample.

Now we have a web project with ZK dependencies in WEB-INF/lib folder. Also the files WEB-INF/web.xml and zk.xml were created.

In WebContent folder we have also the files index.zul and timeout.zul.

4.- We add the Spring, Hibernate and MySQL libraries to WEB-INF/lib folder.

5.- We must create now a package es.comtecsf.zkossportletsample.model where we put our business classes. We will use a class called FamousQuote.java with the following code (just properties and getters and setters):

package es.comtecsf.zkportletsample.model;
public class FamousQuote {

private Long id;
private String quoteText;

public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getQuoteText() {
return quoteText;
}
public void setQuoteText(String quoteText) {
this.quoteText = quoteText;
}
}
6.- Now, in the same package as the class of the previous step, we must add a Hibernate mapping file for this class, called FamousQuote.hbm.xml:

<?xml version="1.0"?>
<!-- FamousQuote.hbm.xml -->
<!DOCTYPE hibernate-mapping SYSTEM "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" >

<hibernate-mapping
package="es.comtecsf.zkportletsample.model">
<class name="FamousQuote" table="famous_quote" lazy="false">
<id name="id" column="id" type="long">
<generator class="native"/>
</id>
<property name="quoteText" column="quote_text" length="255" not-null="true"/>
</class>
</hibernate-mapping>
7.- At this time we have our business class and the mapping between it and the database. Now we must create a GenericDAO which performs the CRUD operations in database using Hibernate. So we make a package es.comtecsf.zkportletsample.dao and there we put our DAO:

package es.comtecsf.zkportletsample.dao;
import java.util.List;

import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

public class GenericDAO extends HibernateDaoSupport {

public void saveOrUpdate(Object ob) {
super.getHibernateTemplate().saveOrUpdate(ob);

}

public void delete(Object ob) {
super.getHibernateTemplate().delete(ob);
}

public Object find(Class clazz, Long id) {
Object ob = super.getHibernateTemplate().load(clazz,id);
return ob;
}

public List findAll(Class clazz) {
List list = super.getHibernateTemplate().find("FROM " + clazz.getName());
return list;
}
}
8.- Now it's a good idea to implement a manager which could be used in the future to implement business logic. At this time it will contain only the DAO callings but an intermediate layer between controller and DAO is commonly used, so we show it here. There are also an interface to encapsulate callings from controller.

We will call this class FamousQuoteManager and IFamousQuoteManager (interface) and we put it into es.comtecsf.zkportletsample.service package:

package es.comtecsf.zkportletsample.service;

import java.util.List;

import es.comtecsf.zkportletsample.model.FamousQuote;

public interface IFamousQuoteManager {

public abstract void delete(FamousQuote current);

public abstract void update(FamousQuote current);

public abstract void add(FamousQuote quote);

public abstract List getAll();

}

---
package es.comtecsf.zkportletsample.service;

import java.util.List;
import es.comtecsf.zkportletsample.dao.GenericDAO;
import es.comtecsf.zkportletsample.model.FamousQuote;

public class FamousQuoteManager implements IFamousQuoteManager {

GenericDAO genericDAO;

public GenericDAO getGenericDAO() {
return genericDAO;
}

public void setGenericDAO(GenericDAO genericDao) {
this.genericDAO = genericDao;
}

@Override
public List getAll() {
return genericDAO.findAll(FamousQuote.class);
}


@Override
public void add(FamousQuote quote) {
genericDAO.saveOrUpdate(quote);
}


@Override
public void update(FamousQuote current) {
genericDAO.saveOrUpdate(current);
}


@Override
public void delete(FamousQuote current) {
genericDAO.delete(current);
}

}
9.- At this point we have all the business classes and manager implemented, so we must now tell our Spring beans where to get all this stuff. We could separate it in many files, but for this sample is enough having only one file with all the Spring and Hibernate configuration. We will put it directly in src folder and we will call it spring-hibernate.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!--  ApplicationContext.xml -->
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>com.mysql.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql://127.0.0.1:3306/zkportletsample</value>
</property>
<property name="username">
<value>root</value>
</property>
<property name="password">
<value>root</value>
</property>
</bean>
<bean id="factory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="mappingResources">
<list>
<value>es/comtecsf/zkportletsample/model/FamousQuote.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
org.hibernate.dialect.MySQLDialect
</prop>
<prop key="hibernate.show_sql">
false
</prop>
<prop key="hibernate.transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory
</prop>
</props>
</property>
<property name="dataSource">
<ref bean="dataSource"/>
</property>
</bean>
<bean id="genericDAO" class="es.comtecsf.zkportletsample.dao.GenericDAO">
<property name="sessionFactory">
<ref bean="factory" />
</property>
</bean>
<bean id="famousQuoteManager" class="es.comtecsf.zkportletsample.service.FamousQuoteManager">
<property name="genericDAO">
<ref bean="genericDAO"/>
</property>
</bean>
</beans>
10.- Just for loading the beans, we use a ServiceLocator class which is located into service package. This is just for testing purposes, and could be avoided by injecting Spring and Hibernate configuration file in web.xml.

package es.comtecsf.zkportletsample.service;
import org.hibernate.SessionFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ServiceLocator {
private static ApplicationContext ctx;

static {
ctx = new ClassPathXmlApplicationContext("spring-hibernate.xml");
}

private ServiceLocator() {
}

public static SessionFactory getSessionFactory() {
return (SessionFactory) ctx.getBean("factory",SessionFactory.class);
}

public static IFamousQuoteManager getFamousQuoteManager() {
return (IFamousQuoteManager) ctx.getBean("famousQuoteManager",FamousQuoteManager.class);
}
}
11.- Well, now we can begin with our controller and view. So, we rewrite the index.zul page which let us test the CRUD operations:

<?init class="org.zkoss.zkplus.databind.AnnotateDataBinderInit"?>
<window id="win" apply="es.comtecsf.zkportletsample.web.QuotesController">
<listbox id="box" multiple="true"
model="@{win$composer.allFamousQuotes, load-after='add.onClick, delete.onClick, update.onClick'}"
selectedItem="@{win$composer.current}">
<listhead>
<listheader label="Quote" sort="auto(quoteText)" />
</listhead>
<listitem self="@{each='famousQuote'}" value="@{famousQuote}">
<listcell label="@{famousQuote.quoteText}" />
</listitem>
</listbox>
<groupbox>
<caption label="Quote" />
<grid>
<columns sizable="false">
<column width="200px"/>
<column width="590px"/>
</columns>
<rows>
<row>
<label value="Quote: " />
<textbox id="quoteText" width="580px" value="@{win$composer.current.quoteText}" />
</row>
</rows>
</grid>
<button id="add" label="Add" />
<button id="update" label="Update" />
<button id="delete" label="Delete" />
</groupbox>
</window>
12.- As we have a controller defined for the zul in the apply property of the window tag, we must write this file. We call it QuotesController.java into the es.comtecsf.zkportletsample.web package:

package es.comtecsf.zkportletsample.web;

import java.util.List;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zk.ui.util.GenericForwardComposer;
import org.zkoss.zul.api.Textbox;

import es.comtecsf.zkportletsample.model.FamousQuote;
import es.comtecsf.zkportletsample.service.IFamousQuoteManager;
import es.comtecsf.zkportletsample.service.ServiceLocator;

@SuppressWarnings("serial")
public class QuotesController extends GenericForwardComposer implements Composer{
private FamousQuote current;
private Textbox quoteText;

@Override
public void doAfterCompose(Component comp) throws Exception {
super.doAfterCompose(comp);
getAllFamousQuotes();
}

public FamousQuote getCurrent() {
return current;
}

public void setCurrent(FamousQuote current) {
this.current = current;
}

public Textbox getQuoteText() {
return quoteText;
}

public void setQuoteText(Textbox quoteText) {
this.quoteText = quoteText;
}

public List getAllFamousQuotes() {
IFamousQuoteManager manager = ServiceLocator.getFamousQuoteManager();
return manager.getAll();
}

public void onClick$add() {
if (null == current) {
current = new FamousQuote();
current.setQuoteText(quoteText.getValue());
}
IFamousQuoteManager manager = ServiceLocator.getFamousQuoteManager();
manager.add(current);
deleteFields();
current = null;

}

public void onClick$update() {
IFamousQuoteManager manager = ServiceLocator.getFamousQuoteManager();
manager.update(current);
deleteFields();
current = null;
}

public void onClick$delete() {
IFamousQuoteManager manager = ServiceLocator.getFamousQuoteManager();
manager.delete(current);
deleteFields();
current = null;
}

private void deleteFields() {
quoteText.setValue("");
}
}
NOTE: This controller method is just for testing purposes, it's not optimized to an actual production environment.

13.- After this, as ZK Project is actually an WTP project, we could use our Run as... Server button in Eclipse, to run our project. In fact, for now, it is just a ZK project, it has nothing to do with Liferay at this moment. So we could try out our web application by launching our server and point our browser at: http://localhost:8080/ZKPortletSample and we will see an image as the below one.


14.- Now it's time to do the trick and convert this application in a portlet to run over Liferay. There are some configuration files that must be in WEB-INF folder. Let's start with the easier. In this file we just add the portlet to a category in Liferay. The file is liferay-display.xml:

<?xml version="1.0"?>
<!DOCTYPE display PUBLIC "-//Liferay//DTD DISPLAY 2.0.0//EN" "http://www.liferay.com/dtd/liferay-display_2_0_0.dtd">
<display>
<category name="category.sample">
<portlet id="ZKPortletSample" />
</category>
</display>
15.- Next file we will use is liferay-plugin-package.properties also in WEB-INF folder. Here we must put just one property. Without this "false" value, Liferay would compress the Javascript of ZK and the portlet would not work at all. The content of this file must be:

speed-filters-enabled=false

16.- There is another file which makes some mandatory stuff with Javascript to get ZK working in Liferay, and this file is liferay-portlet.xml. With this content will be enough, take into account that roles and so could be modified without problems.

<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 5.2.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_5_2_0.dtd">
<liferay-portlet-app>
<portlet>
<portlet-name>ZKPortletSample</portlet-name>
<header-portlet-javascript>/zkau/web/js/zk.wpd</header-portlet-javascript>
</portlet>
<role-mapper>
<role-name>user</role-name>
<role-link>User</role-link>
</role-mapper>
<role-mapper>
<role-name>power-user</role-name>
<role-link>Power User</role-link>
</role-mapper>
<role-mapper>
<role-name>administrator</role-name>
<role-link>Administrator</role-link>
</role-mapper>
</liferay-portlet-app>
17.- The main configuration file for a Liferay portlet (in fact, for all portlets) is the portlet.xml file. Here we declare what init-page will be loaded by the portlet, what class manages all the portlet stuff, and some security features. We propose this content to begin:

<?xml version="1.0" encoding="UTF-8"?>
<portlet-app version="1.0" xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd">
<portlet>
<description xml:lang="EN">ZKPortletSample</description>
<portlet-name>ZKPortletSample</portlet-name>
<display-name xml:lang="EN">ZKPortletSample</display-name>
<portlet-class>org.zkoss.zk.ui.http.DHtmlLayoutPortlet</portlet-class>
<expiration-cache>0</expiration-cache>
<supports>
<mime-type>text/html</mime-type>
<portlet-mode>view</portlet-mode>
</supports>
<supported-locale>en</supported-locale>
<portlet-info>
<title>ZK Portlet Sample</title>
<short-title>ZK Portlet Sample</short-title>
<keywords>zk</keywords>
</portlet-info>
<portlet-preferences>
<preference>
<name>zk_page</name>
<value>/index.zul</value>
</preference>
</portlet-preferences>
<security-role-ref>
<role-name>power-user</role-name>
</security-role-ref>
<security-role-ref>
<role-name>user</role-name>
</security-role-ref>
<security-role-ref>
<role-name>administrator</role-name>
</security-role-ref>
</portlet>
</portlet-app>
18.- At the end, we must add two library properties in zk.xml. This configures ZK to use the jQuery that Liferay provides. Without these lines the portlet will throw a Javascript error each time it is rendered. So, the zk.xml will have this content at the end:

<?xml version="1.0" encoding="UTF-8"?>
<zk>
<device-config>
<device-type>ajax</device-type>
<timeout-uri>/timeout.zul</timeout-uri><!-- An empty URL can cause the browser to reload the same URL -->
</device-config>
<library-property>
<name>org.zkoss.zk.portlet.PageRenderPatch.class</name>
<value>org.zkoss.zkplus.liferay.JQueryRenderPatch</value>
</library-property>
<library-property>
<name>org.zkoss.zkplus.liferay.jQueryPatch</name>
<value>500</value>
</library-property>
</zk>
19.- Once we have made all the steps described before, we can export our project as a WAR file. For instance, we could call it ZKPortletSample.war and save it directly over the Liferay hot-deploy folder. If we made all the steps as were described, we should watch this message in server console:

INFO [PortletHotDeployListener:369] 1 portlet for ZKPortletSample is available for use.

20.- At the end, with the portlet ready to use, we could add it in so many Liferay pages as we want to. The final result should be something like the one showed in the image below:




That's all for now! Next time we'll try to give you a Maven Archetype for ZK-Spring-Hibernate portlets over Liferay.

I hope this tutorial to be useful for you, please, don't hesitate to ask anything, we'll try to answer as soon as possible all the comments you make.

idiazt

No hay comentarios:

Publicar un comentario