Liferay Workflow 2 : Create Custom Portlet Using Workflow Process

This is the 2nd part of my post which talking about Workflow Process in Liferay 6.2. In this post, I will show you how to create custom portlet using workflow inside. Sample case is very simple, we will create simple custom portlet (called Simple News), you can add Simple News. News will be published by workflow process. I assume you have basic knowledge about workflow in liferay, if you need reference you may read my post Liferay Workflow 1 : introduction. Our goal in this post are :

add_simple_newsAdd Simple News

simple_news_wk_configSimple News Workflow Configuration

simple_news_wk_processSimple News Workflow Process

Okay, it’s time to coding. here step by step :

1. Create New Entity and Build Service.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 6.2.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_6_2_0.dtd">
<service-builder package-path="com.rnd.simplenews">
	<author>HidupBersahaja</author>
	<namespace>RND</namespace>
	<entity name="News" uuid="true" local-service="true" remote-service="true">
		<column name="newsId" type="long" primary="true" />

		<column name="groupId" type="long" />
		<column name="title" type="String"/>
		<column name="content" type="String"/>

		<!-- status, status by user id, and status by username will be used by workflow -->
		<column name="status" type="int"/>
		<column name="statusByUserId" type="long"/>
		<column name="statusByUsername" type="String"/>

		<column name="createDate" type="Date" />
		<column name="modifiedDate" type="Date" />
		<column name="createdBy" type="long"/>
		<column name="modifiedBy" type="long"/>

		<order by="asc">
			<order-column name="title" />
		</order>

		<finder name="Title" return-type="Collection">
			<finder-column name="title" />
		</finder>

		<reference package-path="com.liferay.portal" entity="WorkflowInstanceLink" />
		<reference package-path="com.liferay.portlet.asset" entity="AssetEntry" />
		<reference package-path="com.liferay.portlet.asset" entity="AssetLink" />
		<reference package-path="com.liferay.portlet.asset" entity="AssetTag" />
	</entity>
</service-builder>

As you can see there are some field that will be used by worfklow (Status, StatuByUserId, StatusByUsername). After build services you must add some transient fields in NewsImpl.java. Some transient fields are :

        public boolean isApproved() {
		if (getStatus() == WorkflowConstants.STATUS_APPROVED) {
			return true;
		}
		else {
			return false;
		}
	}

	public boolean isDenied() {
		if (getStatus() == WorkflowConstants.STATUS_DENIED) {
			return true;
		}
		else {
			return false;
		}
	}

	public boolean isDraft() {
		if (getStatus() == WorkflowConstants.STATUS_DRAFT) {
			return true;
		}
		else {
			return false;
		}
	}

	public boolean isExpired() {
		if (getStatus() == WorkflowConstants.STATUS_EXPIRED) {
			return true;
		}
		else {
			return false;
		}
	}

	public boolean isInactive() {
		if (getStatus() == WorkflowConstants.STATUS_INACTIVE) {
			return true;
		}
		else {
			return false;
		}
	}

	public boolean isIncomplete() {
		if (getStatus() == WorkflowConstants.STATUS_INCOMPLETE) {
			return true;
		}
		else {
			return false;
		}
	}

	public boolean isPending() {
		if (getStatus() == WorkflowConstants.STATUS_PENDING) {
			return true;
		}
		else {
			return false;
		}
	}

	public boolean isScheduled() {
		if (getStatus() == WorkflowConstants.STATUS_SCHEDULED) {
			return true;
		}
		else {
			return false;
		}
	}

2. Create Workflow Handler Class and Register in liferay-portlet.xml file 

package com.rnd.simplenews;

import java.io.Serializable;
import java.util.Locale;
import java.util.Map;

import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.workflow.BaseWorkflowHandler;
import com.liferay.portal.kernel.workflow.WorkflowConstants;
import com.liferay.portal.security.permission.ResourceActionsUtil;
import com.liferay.portal.service.ServiceContext;
import com.rnd.simplenews.model.News;
import com.rnd.simplenews.service.NewsLocalServiceUtil;

public class SimpleNewsWorkflowHandler extends BaseWorkflowHandler{

	@Override
	public String getClassName() {
		return News.class.getName();
	}

	@Override
	public String getType(Locale locale) {
		return ResourceActionsUtil.getModelResource(locale, getClassName());
	}

	@Override
	public News updateStatus(int status,
			Map<String, Serializable> workflowContext) throws PortalException,
			SystemException {

		long userId = GetterUtil.getLong(
				(String)workflowContext.get(WorkflowConstants.CONTEXT_USER_ID));
		long classPK = GetterUtil.getLong(
			(String)workflowContext.get(
				WorkflowConstants.CONTEXT_ENTRY_CLASS_PK));
		ServiceContext serviceContext = (ServiceContext)workflowContext.get(
				"serviceContext");

		return NewsLocalServiceUtil.updateStatus(userId, classPK, status, serviceContext);
	}

}

After create workflow class handler, you should register it on the liferay-portlet.xml file inner tag < portlet >

<workflow-handler>com.rnd.simplenews.SimpleNewsWorkflowHandler</workflow-handler>

3. Insert Some Code in your Local Service Inner Save Method 

I assume you have knowledge to create simple CRUD in liferay, so in your Local Service I guess you have Save / Update Method like this :

        public void saveUpdateNews(News news, ServiceContext serviceContext){
		try{
			User user = userPersistence.findByPrimaryKey(news.getStatusByUserId());
			long groupId = serviceContext.getScopeGroupId();

			if(news.getCmd().equals(Constants.ADD)){
				news.setStatus(WorkflowConstants.STATUS_DRAFT);
				news.setGroupId(groupId);

				//save entity
				NewsLocalServiceUtil.addNews(news);	

				//update Asset
				updateAsset(
					user.getUserId(), news, serviceContext.getAssetCategoryIds(),
					serviceContext.getAssetTagNames(),
					serviceContext.getAssetLinkEntryIds());

				WorkflowHandlerRegistryUtil.startWorkflowInstance(
					user.getCompanyId(), groupId, user.getUserId(), News.class.getName(),
					news.getNewsId(), news, serviceContext);

			}else{
					NewsLocalServiceUtil.updateNews(news);
			}
		} catch (SystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (PortalException e ){
			e.printStackTrace();
		} catch (Exception e){
			e.printStackTrace();
		}
	}

       public void updateAsset(long userId, News news, long[] assetCategoryIds,
			String[] assetTagNames, long[] assetLinkEntryIds)throws PortalException, SystemException {

		boolean visible = false;

		if (news.isApproved()) {
			visible = true;
		}

		String summary = HtmlUtil.extractText(
				StringUtil.shorten(news.getContent(), 500));

		AssetEntry assetEntry = assetEntryLocalService.updateEntry(
				userId, news.getGroupId(), news.getCreateDate(),
				news.getModifiedDate(), News.class.getName(),
				news.getNewsId(), news.getUuid(), 0, assetCategoryIds,
				assetTagNames, visible, null, null, null, ContentTypes.TEXT_HTML,
				news.getTitle(), news.getContent(), summary, null, null, 0, 0,
				null, false);

	}

In the beginning field status will set to DRAFT, and then WorkflowHandlerRegistryUtil.startWorkflowInstance will check if your workflow setting is enabled / disabled and it will be updated the status value through your Workflow Handler Class

4. Create Asset Renderer & Asset Renderer Factory Class

Asset Renderer Class :

package com.rnd.simplenews.asset;

import java.util.Locale;

import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

import com.liferay.portal.kernel.util.HtmlUtil;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portlet.asset.model.BaseAssetRenderer;
import com.rnd.simplenews.model.News;

public class NewsEntryAssetRenderer extends BaseAssetRenderer{

	private News _entry;

	public NewsEntryAssetRenderer(News entry){
		_entry = entry;
	}

	@Override
	public String getClassName() {
		return News.class.getName();
	}

	@Override
	public long getClassPK() {
		return _entry.getNewsId();
	}

	@Override
	public long getGroupId() {
		return _entry.getGroupId();
	}

	@Override
	public String getSummary(Locale locale) {
		String summary = _entry.getContent();

		if (Validator.isNull(summary)) {
			summary = StringUtil.shorten(
				HtmlUtil.stripHtml(_entry.getContent()), 200);
		}

		return summary;
	}

	@Override
	public String getTitle(Locale locale) {
		return _entry.getTitle();
	}

	@Override
	public long getUserId() {
		return _entry.getStatusByUserId();
	}

	@Override
	public String getUserName() {
		return _entry.getStatusByUsername();
	}

	@Override
	public String getUuid() {
		return _entry.getUuid();
	}

	@Override
	public String render(RenderRequest renderRequest,
			RenderResponse renderResponse, String template) throws Exception {
		if (template.equals(TEMPLATE_ABSTRACT) ||
				template.equals(TEMPLATE_FULL_CONTENT)) {

				renderRequest.setAttribute("News", _entry);

				return "/html/simple_news/" + template + ".jsp";
			}
			else {
				return null;
			}
	}

}

Asset Renderer Factory class :

package com.rnd.simplenews.asset;

import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portlet.asset.model.AssetRenderer;
import com.liferay.portlet.asset.model.BaseAssetRendererFactory;
import com.rnd.simplenews.model.News;
import com.rnd.simplenews.service.NewsLocalServiceUtil;

public class NewsEntryAssetRendererFactory extends BaseAssetRendererFactory {
	public static final String TYPE = "news";

	@Override
	public AssetRenderer getAssetRenderer(long classPK, int type)
			throws PortalException, SystemException {

		News entry = NewsLocalServiceUtil.findByPrimaryKey(classPK);
		NewsEntryAssetRenderer newsEntryAssetRenderer =
					new NewsEntryAssetRenderer(entry);

		newsEntryAssetRenderer.setAssetRendererType(type);
		return newsEntryAssetRenderer;
	}

	@Override
	public String getClassName() {
		return News.class.getName();
	}

	@Override
	public String getType() {
		return TYPE;
	}
}

Register Asset Renderer Factory in your liferay-portlet.xml file

<asset-renderer-factory>com.rnd.simplenews.asset.NewsEntryAssetRendererFactory</asset-renderer-factory>

The nice article about Asset Renderer you can read here

4. Create Filter in List Grid

you can add filter in list grid to make only news has approved will display

                         if(news.isApproved()){
				long newsId = news.getNewsId();
				ResultRow row = new ResultRow(news, newsId, i);
				
				PortletURL url = renderResponse.createRenderURL();
				url.setParameter("jspPage", "/html/simple_news/add_news.jsp");
				url.setParameter(Constants.CMD, Constants.EDIT);
				url.setParameter("newsId", String.valueOf(news.getNewsId()));
				url.setParameter("backURL", currentURLObj.toString());
				
				
				row.addText(news.getTitle());
				row.addText(String.valueOf(news.getCreatedBy()));
				row.addText(news.getCreateDate().toString());
				
				
				row.addText("");
				row.addText("");
				
				row.addText("");
				resultRows.add(row);	
			}

I knew this post maybe make your confuse, because I don’t explain all code, so you can download the sample portlet which using Workflow Process here

Good Luck !

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>