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 News Form
Add Simple News

Simple News Workflow Config
Simple News Workflow Configuration

Simple News Workflow Process
Simple 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 !