Liferay Portlets as Standalone Desktop Apps??? OOTB

Company Blogs August 28, 2008 By Ray Augé Staff

In Liferay we have this cool new feature to run portlets as embedded widgets from any webpage anywhere.

Tired of loosing you chat sessions on every page refresh???

No more!!!

Well feast your eyes on Liferay Portls running as APPS on your desktop. Completely standalone, no shared memory, if one process crashes the others will not.

Take a look at these babies.

 

Chat Portlet:

 

Message Boards:

Message Boards

 

Zimbra eat your heart out... Every portlet build on Liferay automatically becomes your latest Desktop App using none other than the Prism Mozilla project.

Check it out at http://wiki.mozilla.org/WebRunner.

All you need is to grab the Shared "widget URL" we generate for your portlet and setup a Prism link, done!

 

 

Is your laptop battery this dead?

Company Blogs July 30, 2008 By Ray Augé Staff

Sample scripts for the Ruby Console portlet [1]

Company Blogs July 9, 2008 By Ray Augé Staff

Here is a sample script for the Ruby Console portlet to test various aspects of the Dynamic Query API.

import com.liferay.portal.kernel.util.Time
import java.lang.System
import java.util.Date

module Orm
   include_package "com.liferay.portal.kernel.dao.orm"
end
module Model
   include_package "com.liferay.portal.model"
end
module Service
   include_package "com.liferay.portal.service"
end

$resourceResponse.setContentType "text/html"

out = $resourceResponse.getPortletOutputStream

date24HoursAgo = Date.new(System.currentTimeMillis - Time::HOUR * 24)

dq1 = Orm::DynamicQueryFactoryUtil.forClass(Model::User.java_class)

pl1 = Orm::ProjectionFactoryUtil.projectionList
pl1.add(Orm::ProjectionFactoryUtil.property("emailAddress"))
pl1.add(Orm::ProjectionFactoryUtil.property("lastLoginDate"))
pl1.add(Orm::ProjectionFactoryUtil.property("lastLoginIP"))

dq1.setProjection(pl1)
dq1.add(Orm::PropertyFactoryUtil.forName("lastLoginDate").gt(date24HoursAgo))
dq1.addOrder(Orm::OrderFactoryUtil.desc("emailAddress"))

result = Service::UserLocalServiceUtil.dynamicQuery(dq1, 0, 20)

out.println result.size
out.println "<br/>"
out.println "<table width='100%' border='1'>"
result.each do |row|
  out.println "<tr>"
  row.each do |field|
    out.println "<td>#{field}</td>"
  end
  out.println "</tr>"
end
out.println "</table>"

[HOWTO] Personalization - Getting current user attributes

Company Blogs May 16, 2008 By Ray Augé Staff

Portals are all about personalization, and Liferay is no different.

I mean, there are many, many other things involved in portals other than personalization. But none of them really make sense without it.

Personalization is the dressing that makes your portal salad bearable. So, it has to be pretty good dressing.

Beyond the portal and personalization, we have the reason the portal exists at all: Portlets. Portlets are the ingredients needed to make a portal salad worth eating.

  1. You have your greens (the hard working portlets: E-Commerce, BI, BPM, CMS, etc.).
  2. Then you have your non-green veggies (Email, Forums, Blogs, Wiki, etc.).
  3. Finally you have your crutons and bacon bits, the stuff we know we can live without but just can't bring ourselves to leave out (social networking and other generally superfluous gadgets, widgetry, and such).

Yet it doesn't end there, it's gotta be MY salad, not someone else's... and it's gotta have MY name on it too. Who wants to eat someone else's food anyway...

That said, personalization at the portlet level is very important. There are two types of portlet personalization mechanisms available in Liferay.

  1. Personalization defined by the Portlet spec(s) (low fat, low cal. dressing)
  2. Liferay personalization (fatty, rich, tasty dressing)

Let's go through putting both to use in your Portlet application and then you can decide for yourself which best suites your diet.

Personalizaton via Portlet spec(s)

Both the 1.0 version and 2.0 (upcomming) versions of the spec define a mechanism called "user-attributes" defined as such:

"The deployment descriptor of a portlet application must define the user attribute names the portlets use. The following example shows a section of a deployment descriptor defining a few user attributes:

<portlet-app>
	...
	<user-attribute>
		<description>User Given Name</description>
		<name>user.name.given</name>
	</user-attribute>
	<user-attribute>
		<description>User Last Name</description>
		<name>user.name.family</name>
	</user-attribute>
	<user-attribute>
		<description>User eMail</description>
		<name>user.home-info.online.email</name>
	</user-attribute>
	<user-attribute>
		<description>Company Organization</description>
		<name>user.business-info.postal.organization</name>
	</user-attribute>
	...
<portlet-app>
A deployer must map the portlet application’s logical user attributes to the corresponding user attributes offered by the runtime environment. At runtime, the portlet container uses this mapping to expose user attributes to the portlets of the portlet application. User attributes of the runtime environment not mapped as part of the deployment process must not be exposed to portlets."

[JavaTM Portlet Specification, version 1.0, PLT.17.1]

This means that without having to do anything other than configuration, the portlet is requesting access to the defined user attributes. The list of available attributes can be found in the spec docs.

The next part is of course getting access to those user attributes at runtime.

Here is a complete JSP which handles getting the user.name.given attribute.

<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %>

<%@ page import="javax.portlet.PortletRequest"%>
<%@ page import="java.util.Map"%>

<portlet:defineObjects />

<%
Map userInfo = (Map)renderRequest.getAttribute(PortletRequest.USER_INFO);
String givenName = (userInfo != null) ? (String)userInfo.get("user.name.given") : "";
%>

Given Name: <%= givenName %>

The result looks something like the following:

[Image 1]

That's 100% spec complient and includes the following list of attributes:

package com.liferay.portlet;

public class UserAttributes {

	// Mandatory Liferay attributes

	public static final String LIFERAY_COMPANY_ID = "liferay.company.id";

	public static final String LIFERAY_USER_ID = "liferay.user.id";

	public static final String USER_NAME_FULL = "user.name.full";

	// See page 119 of the JSR 168 spec

	public static final String USER_BDATE = "user.bdate";

	public static final String USER_GENDER = "user.gender";

	public static final String USER_EMPLOYER = "user.employer";

	public static final String USER_DEPARTMENT = "user.department";

	public static final String USER_JOBTITLE = "user.jobtitle";

	public static final String USER_NAME_PREFIX = "user.name.prefix";

	public static final String USER_NAME_GIVEN = "user.name.given";

	public static final String USER_NAME_FAMILY = "user.name.family";

	public static final String USER_NAME_MIDDLE = "user.name.middle";

	public static final String USER_NAME_SUFFIX = "user.name.suffix";

	public static final String USER_NAME_NICKNAME = "user.name.nickName";

	public static final String USER_HOME_INFO_POSTAL_NAME = "user.home-info.postal.name";

	public static final String USER_HOME_INFO_POSTAL_STREET = "user.home-info.postal.street";

	public static final String USER_HOME_INFO_POSTAL_CITY = "user.home-info.postal.city";

	public static final String USER_HOME_INFO_POSTAL_STATEPROV = "user.home-info.postal.stateprov";

	public static final String USER_HOME_INFO_POSTAL_POSTALCODE = "user.home-info.postal.postalcode";

	public static final String USER_HOME_INFO_POSTAL_COUNTRY = "user.home-info.postal.country";

	public static final String USER_HOME_INFO_POSTAL_ORGANIZATION = "user.home-info.postal.organization";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_INTCODE = "user.home-info.telecom.telephone.intcode";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_LOCCODE = "user.home-info.telecom.telephone.loccode";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_NUMBER = "user.home-info.telecom.telephone.number";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_EXT = "user.home-info.telecom.telephone.ext";

	public static final String USER_HOME_INFO_TELECOM_TELEPHONE_COMMENT = "user.home-info.telecom.telephone.comment";

	public static final String USER_HOME_INFO_TELECOM_FAX_INTCODE = "user.home-info.telecom.fax.intcode";

	public static final String USER_HOME_INFO_TELECOM_FAX_LOCCODE = "user.home-info.telecom.fax.loccode";

	public static final String USER_HOME_INFO_TELECOM_FAX_NUMBER = "user.home-info.telecom.fax.number";

	public static final String USER_HOME_INFO_TELECOM_FAX_EXT = "user.home-info.telecom.fax.ext";

	public static final String USER_HOME_INFO_TELECOM_FAX_COMMENT = "user.home-info.telecom.fax.comment";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_INTCODE = "user.home-info.telecom.mobile.intcode";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_LOCCODE = "user.home-info.telecom.mobile.loccode";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_NUMBER = "user.home-info.telecom.mobile.number";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_EXT = "user.home-info.telecom.mobile.ext";

	public static final String USER_HOME_INFO_TELECOM_MOBILE_COMMENT = "user.home-info.telecom.mobile.comment";

	public static final String USER_HOME_INFO_TELECOM_PAGER_INTCODE = "user.home-info.telecom.pager.intcode";

	public static final String USER_HOME_INFO_TELECOM_PAGER_LOCCODE = "user.home-info.telecom.pager.loccode";

	public static final String USER_HOME_INFO_TELECOM_PAGER_NUMBER = "user.home-info.telecom.pager.number";

	public static final String USER_HOME_INFO_TELECOM_PAGER_EXT = "user.home-info.telecom.pager.ext";

	public static final String USER_HOME_INFO_TELECOM_PAGER_COMMENT = "user.home-info.telecom.pager.comment";

	public static final String USER_HOME_INFO_ONLINE_EMAIL = "user.home-info.online.email";

	public static final String USER_HOME_INFO_ONLINE_URI = "user.home-info.online.uri";

	public static final String USER_BUSINESS_INFO_POSTAL_NAME = "user.business-info.postal.name";

	public static final String USER_BUSINESS_INFO_POSTAL_STREET = "user.business-info.postal.street";

	public static final String USER_BUSINESS_INFO_POSTAL_CITY = "user.business-info.postal.city";

	public static final String USER_BUSINESS_INFO_POSTAL_STATEPROV = "user.business-info.postal.stateprov";

	public static final String USER_BUSINESS_INFO_POSTAL_POSTALCODE = "user.business-info.postal.postalcode";

	public static final String USER_BUSINESS_INFO_POSTAL_COUNTRY = "user.business-info.postal.country";

	public static final String USER_BUSINESS_INFO_POSTAL_ORGANIZATION = "user.business-info.postal.organization";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_INTCODE = "user.business-info.telecom.telephone.intcode";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_LOCCODE = "user.business-info.telecom.telephone.loccode";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_NUMBER = "user.business-info.telecom.telephone.number";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_EXT = "user.business-info.telecom.telephone.ext";

	public static final String USER_BUSINESS_INFO_TELECOM_TELEPHONE_COMMENT = "user.business-info.telecom.telephone.comment";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_INTCODE = "user.business-info.telecom.fax.intcode";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_LOCCODE = "user.business-info.telecom.fax.loccode";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_NUMBER = "user.business-info.telecom.fax.number";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_EXT = "user.business-info.telecom.fax.ext";

	public static final String USER_BUSINESS_INFO_TELECOM_FAX_COMMENT = "user.business-info.telecom.fax.comment";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_INTCODE = "user.business-info.telecom.mobile.intcode";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_LOCCODE = "user.business-info.telecom.mobile.loccode";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_NUMBER = "user.business-info.telecom.mobile.number";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_EXT = "user.business-info.telecom.mobile.ext";

	public static final String USER_BUSINESS_INFO_TELECOM_MOBILE_COMMENT = "user.business-info.telecom.mobile.comment";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_INTCODE = "user.business-info.telecom.pager.intcode";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_LOCCODE = "user.business-info.telecom.pager.loccode";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_NUMBER = "user.business-info.telecom.pager.number";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_EXT = "user.business-info.telecom.pager.ext";

	public static final String USER_BUSINESS_INFO_TELECOM_PAGER_COMMENT = "user.business-info.telecom.pager.comment";

	public static final String USER_BUSINESS_INFO_ONLINE_EMAIL = "user.business-info.online.email";

	public static final String USER_BUSINESS_INFO_ONLINE_URI = "user.business-info.online.uri";

}

It should be noted that as of right now, we haven't implemented the additonal attributes defined in javax.portlet.PortletRequest.P3PUserInfos, of the current version 2.0 recommendation.



Liferay Personalizaton

Liferay personalization provides access to pojos which can be interacted with in a much richer fashion than above. It also ties your application to Liferay so, if you plan to target more than just Liferay as plaform, don't use it.

The easiest way to take avantage of Liferay personalization is to use a couple of Liferay's taglibs; more precisely, the liferay-theme and liferay-ui taglibs.

Since these are not included in a deployed portlet by default, add the following property to your liferay-plugin-package.properties file:

portal.dependency.tlds=\
    liferay-ui.tld,\
    liferay-theme.tld

and the following taglib definitions to your web.xml:

	<taglib>
		<taglib-uri>http://liferay.com/tld/theme</taglib-uri>
		<taglib-location>/WEB-INF/tld/liferay-theme.tld</taglib-location>
	</taglib>
	<taglib>
		<taglib-uri>http://liferay.com/tld/ui</taglib-uri>
		<taglib-location>/WEB-INF/tld/liferay-ui.tld</taglib-location>
	</taglib>

Now that we have the right tools available, let's start with the very simplest example that gives us the most bang.

<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
<%@ taglib uri="http://liferay.com/tld/theme" prefix="liferay-theme" %>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui" %>

<liferay-theme:defineObjects />
<portlet:defineObjects />

<liferay-ui:user-display userId="<%= user.getUserId() %>" />

The result of the above JSP code is something like the following:

[Image 2]

Now you might be asking yourself "Where did that user object suddenly come from?"

The answer is simple! It comes from the <liferay-theme:defineObjects /> tag we used. This tag puts a whole bunch of context sensitive objects into our JSP page context, including the com.liferay.portal.model.User pojo associated with the current user..

The objects that are injected into the pageContext by the <liferay-theme:defineObjects /> tag are:

  • themeDisplay - com.liferay.portal.theme.ThemeDisplay
  • company - com.liferay.portal.model.Company
  • account - com.liferay.portal.model.Account (deprecated)
  • user - com.liferay.portal.model.User
  • realUser - com.liferay.portal.model.User
  • contact - com.liferay.portal.model.Contact
  • ?layout - com.liferay.portal.model.Layout
  • ?layouts - List<com.liferay.portal.model.Layout>
  • plid - java.lang.Long
  • ?layoutTypePortlet - com.liferay.portal.model.LayoutTypePortlet
  • portletGroupId - java.lang.Long
  • permissionChecker - com.liferay.portal.security.permission.PermissionChecker
  • locale - java.util.Locale
  • timeZone - java.util.TimeZone
  • theme - com.liferay.portal.model.Theme
  • colorScheme - com.liferay.portal.model.ColorScheme
  • portletDisplay - com.liferay.portal.theme.PortletDisplay

Wow, now that's lots of bacon!!! I like bacon!

I won't try to explain the use of all of these objects here on this post. Suffice it to say that after exploring all of these objects and the remaining tags available to you in the Liferay taglibs, you shouldn't run out of personalization options any time soon.

You can't use JSP taglibs?

Never fear! All these objects are actually obtained by various getter methods on the com.liferay.portal.theme.ThemeDisplay object. We just put them right into the page context to cut down on code.

So, all you need is the themeDisplay object, and you can get it from the portletRequest like so:

ThemeDisplay themeDisplay = (ThemeDisplay)req.getAttribute(WebKeys.THEME_DISPLAY);

themeDisplay.getCompany();
themeDisplay.getAccount();
themeDisplay.getUser();
themeDisplay.getRealUser();
themeDisplay.getContact();

if (themeDisplay.getLayout() != null) {
	themeDisplay.getLayout();
}

if (themeDisplay.getLayouts() != null) {
	themeDisplay.getLayouts();
}

themeDisplay.getPlid());

if (themeDisplay.getLayoutTypePortlet() != null) {
	themeDisplay.getLayoutTypePortlet();
}

new Long(themeDisplay.getPortletGroupId());
themeDisplay.getPermissionChecker();
themeDisplay.getLocale();
themeDisplay.getTimeZone();
themeDisplay.getTheme();
themeDisplay.getColorScheme();
themeDisplay.getPortletDisplay();

Well, feel to ask questions about the ones for which the meanings are less than obvious. The Message Boards are also full of answers regarding various personalization topics. And remember to have a look at the API docs on the site for the methods provided from each of these objects.

[HOWTO] Dowloading files from a Portlet

Company Blogs May 14, 2008 By Ray Augé Staff

Many people ask how to achieve file downloads from a portlet project.

I'm going to try putting this mystery to rest by first explaining the limitations and then showing some old and new ways of overcoming them in Liferay. To be clear on terminology we'll call anything, other than the usual presentation content, that we want to deliver to the user a resource. Examples of resources are: images, binary documents, css stylesheets, js files, and typically things that we want to dynamically generate access to users to download. They are also often generated on the fly, like a PDF report, or a captcha image.

To begin, the JSR-168/Portlet 1.0 spec did not have support for delivering anything other than a blob of text/html content. Nor did it support delivering such content outside of a response which included the portal's wrappings.

Here is a brief (and not all encompassing) recent history of resource downloads in Liferay.

Liferay <= 4.1.x: At this point the ONLY ways to provide download links were:

  1. provide a direct link to the resource
  2. delegate the functionality to a servlet

In Liferay's core this was very simple because we use Struts, so creating an action (and mapping) of type org.apache.struts.action.Action was relatively painless and we could quickly develop any download mechanism we needed. The only issue was that the URL could not be generated using the <portlet /> tag. A developer had to be aware of the path associated with the Action in order to access it.

For example, consider the following action-mapping from version 4.3.x's Document Library portlet:

<action path="/document_library/get_file" type="com.liferay.portlet.documentlibrary.action.GetFileAction" />

If you look at the code for com.liferay.portlet.documentlibrary.action.GetFileAction you will see that it implements a method:

public ActionForward strutsExecute(
		ActionMapping mapping, ActionForm form, HttpServletRequest req, HttpServletResponse res)
	throws Exception;

This method, though found in com.liferay.portal.struts.PortletAction, is simply a helper method which makes the PortletAction behave as a servlet by calling super.execute(mapping, form, req, res) directly.

In order to put this method to work we create an url as follows:

<a href="<%= themeDisplay.getPathMain() %>/document_library/get_file?folderId=<%= folderId %>&name=<%= HttpUtil.encodeURL(name) %>">
	<%= fileEntry.getTitle() %>
</a>

Notice that we haven't used any tag to generate the URL and we needed to include the portals context.

So, at this point if you were creating a portlet to be JSR-168 complient (outside of ext, or core), you could create an URL to a static resource like so (JSP example):

<a href="<%= renderRequest.getContextPath() %>/static_path/file_name.pdf">
	file_name.pdf
</a>

To create an URL to a dynamic resource, you would have to delegate to some servlet, like so:

<a href="<%= renderRequest.getContextPath() %>/servlet_path?p1=va&p2=v2">
	<%= someLinkText %>
</a>


Liferay <= 4.4.x and > 4.1.x: At this point we started to experiment with delivering portlet resources in a way similar to what was proposed in the upcoming JSR-286/Portlet 2.0 spec.

We had at our disposal a feature we had created which allowed us to deliver resources from portlets. It was used by specifying LiferayWindowState.EXCLUSIVE on a portlet URL. If using it from a actionURL it could deliver binary resources.

Example of returning a captcha image, from 4.3.x:

<portlet:actionURL windowState="<%= LiferayWindowState.EXCLUSIVE.toString() %>" var="captchaURL">
	<portlet:param name="struts_action" value="/message_boards/captcha" />
</portlet:actionURL>

On a renderURL it could be used for handling things like Ajax requests.

<form action="<liferay-portlet:renderURL 
			windowState="<%= LiferayWindowState.EXCLUSIVE.toString() %>">
		<portlet:param name="struts_action" value="/password_generator/view" />
	</liferay-portlet:renderURL>" 
	method="post" 
	name="<portlet:namespace />fm" 
	onSubmit="AjaxUtil.submit(this, {update: this.parentNode}); return false;">

Unfortunately, this feature was only usable from within the EXT environment or from within the core for handling binary resources, because final handling of the response required accessing the original HttpServletResponse.

Here is the response handling side of the capcha example:

public void processAction(
		ActionMapping mapping, ActionForm form, PortletConfig config,
		ActionRequest req, ActionResponse res)
	throws Exception {

	try {
		PortletSession ses = req.getPortletSession();

		String captchaText = _producer.createText();

		ses.setAttribute(WebKeys.CAPTCHA_TEXT, captchaText);

		HttpServletResponse httpRes =
			((ActionResponseImpl)res).getHttpServletResponse();

		_producer.createImage(httpRes.getOutputStream(), captchaText);

		setForward(req, ActionConstants.COMMON_NULL);
	}
	catch (Exception e) {
		_log.error(e);
	}
}


Solution for external portlets: In order to provide a feature which was usable from portlets outside of EXT & core, we created a interface available in portal-kernel.jar and made RenderResponseImpl implement this interface. The interface provide the following methods:

public interface LiferayRenderResponse extends RenderResponse {

	public void addDateHeader(String name, long date);

	public void addHeader(String name, String value);

	public void addIntHeader(String name, int value);

	public void setDateHeader(String name, long date);

	public void setHeader(String name, String value);

	public void setIntHeader(String name, int value);

	public void setResourceName(String resourceName);

}

Additionally, we added support for changing the content type returned from RenderResponse, but ONLY if the WinowState was equal to LiferayWindowState.EXCLUISVE.

Here is an example which returns a png image:

public void doView(RenderRequest req, RenderResponse res)
	throws IOException, PortletException {

	boolean logo = ParamUtil.getBoolean(req, "logo");

	if (logo && req.getWindowState().equals(LiferayWindowState.EXCLUSIVE)) {
		LiferayRenderResponse liferayRes = (LiferayRenderResponse)res;

		liferayRes.setContentType("image/png");
		liferayRes.addHeader(
			HttpHeaders.CACHE_CONTROL, "max-age=3600, must-revalidate");

		OutputStream out = liferayRes.getPortletOutputStream();

		InputStream in = // some InputStream

		if (in == null) {
			out.close();
		}
		else {
			byte[] buffer = new byte[4096];
			int len;

			while ((len = in.read(buffer)) != -1) {
				out.write(buffer, 0, len);
			}

			out.flush();
			in.close();
			out.close();
		}
	}
	else {
		include(viewJSP, req, res);
	}
}

Notice that you can specify the contentType, set headers, and write binary/text data directly to the portlet's OutputStream. One note is that the content type MUST be set before attempting to call liferayRes.getPortletOutputStream(); otherwise an exception will be raised.

And the url looks like this:

<img src="<portlet:renderURL
			portletMode="view"
			windowState="<%= LiferayWindowState.EXCLUSIVE.toString() %>">
		<portlet:param name="logo" value="true" />
	</portlet:renderURL>"
	alt="Image returned by portlet." />

This was pretty handy for everything from XML and JSON AJAX data, to dynamically generated binary resources.



Liferay >= 5.0.0: In the new release of Liferay, while still supporting all of the methods described above, we also support the brand new (and as of yet still unofficial) JSR-286/Portlet 2.0 feature ResourceRequest which is specifically designed for handling any resource delivery purely as a function of a portal & portlet which are JSR-286 compliant. It can equally well handle everything from binary files to JSON data returned by an AJAX request.

An example usage looks like this:

public void serveResource(ResourceRequest req, ResourceResponse res)
		throws PortletException, IOException {

	boolean logo = ParamUtil.getBoolean(req, "logo");

	if (logo) {
		res.setContentType("image/png");
		res.addProperty(
			HttpHeaders.CACHE_CONTROL, "max-age=3600, must-revalidate");

		OutputStream out = res.getPortletOutputStream();

		InputStream in = // some InputStream

		if (in == null) {
			out.close();
		}
		else {
			byte[] buffer = new byte[4096];
			int len;

			while ((len = in.read(buffer)) != -1) {
				out.write(buffer, 0, len);
			}

			out.flush();
			in.close();
			out.close();
		}
	}
}

And the URL looks like this:

<img src="<portlet:resourceURL>
		<portlet:param name="logo" value="true" />
	</portlet:resourceURL>"
	alt="Image returned by portlet." />

Well, there you have it.

[Tip] EhCache and Ubuntu (Hardy 8.04)

Company Blogs May 12, 2008 By Ray Augé Staff

If you are or you have tried installing a Liferay on a cluster of Ubuntu machines and are/were having problems with the ehCache, do this check:

]$ ping `hostname`

If it responds using an address on the loopback device, there's your problem.

By default, Ubuntu defines the machine's hostname on the loopback address (127.0.1.1), and since ehCache uses:

InetAddress.getLocalHost().getHostAddress()

to resolve the address used in association with the RMICachePeers it creates, these peers will have addresses of 127.0.1.1:port which will render them unreachable to other peers on the network.

Here is a successful test:

rotty@liferay-laptop:~$ ping `hostname`
PING liferay-laptop (192.168.0.243) 56(84) bytes of data.
64 bytes from liferay-laptop (192.168.0.243): icmp_seq=1 ttl=64 time=0.046 ms
64 bytes from liferay-laptop (192.168.0.243): icmp_seq=2 ttl=64 time=0.056 ms
64 bytes from liferay-laptop (192.168.0.243): icmp_seq=3 ttl=64 time=0.055 ms

Note that the address is one on the private subnet where the rest of the cluster nodes can see it.

FYI!

Expandos - What are they? And how do they help me? (Liferay Portal 5.0.1+)

Company Blogs April 24, 2008 By Ray Augé Staff

Updated: Wed May 28 10:53:09 EDT 2008.

See the follow-up article here.

In Javascript, "expando" means "to attach additional properties to an object".

That's a little bit of hint as to what Expandos are in Liferay.

In Liferay the Expando service is a "generic" service which allows you to dynamically define a collection of data. This data can be

  • typed (boolean, Date, double, int, long, short, String, and arrays of all those)
  • associated with a specific entity (e.g. 'com.liferay.portal.model.User')
  • arranged into any number of "columns"
  • available to plugins
  • accessed from Velocity templates
  • accessed via Liferay's JSON API through AJAX

 

The service also provides all the CRUD methods you typically need (Create/Retreive/Update/Delete).

You often need a way to add some custom field to Users or other portal entities and sometimes you need to do it fast and with minimal effort... it might even be temporary. Well, Expando is comming to the rescue.

To demonstrate how Expando works, I'll take you through a sample application which runs completely in as a velocity Journal Template.

So first off, lets get the required details out of the way.

Step 1) Create a Journal Structure. The structure is a requirement of any Journal Template because the connection between a Journal Article and a Journal Template is a function of the Journal Structure.

A very basic Structure is all we need in this case:

<root>
	<dynamic-element name='content' type='text'></dynamic-element>
</root>

Step 2) Create our Template

Our template "Language Type" will be VM (for Velocity), and we'll disable template caching by unckecking "Cacheable".

We'll start simple and work our way through the code. The first few lines will get some utility objects that we'll use later, and set a title for our app.

#set ($locale = $localeUtil.fromLanguageId($request.get("locale")))
#set ($dateFormatDateTime = $dateFormats.getDateTime($locale))

<h1>First Expando Bank</h1>

Step 3) We create our Article.

On the Article tab we essentially click "Add Article", give it a "Name", choose the Structure we created in Step 1) and click "Save".

The result should be something like this:

Now, the first task when using Expando is to define our table. Expando lets you do this programatically and with very little code.

Give our table a name.

...
<h1>First Expando Bank</h1>

#set ($accountsTableName = "AccountsTable")

Check to see if the table exists, and if not, create it.

...
#set ($accountsTableName = "AccountsTable")

#set ($accountsTable = $expandoTableLocalService.getTable($accountsTableName, $accountsTableName))

#if (!$accountsTable)
#set ($accountsTable = $expandoTableLocalService.addTable($accountsTableName, $accountsTableName))
#end

We now have a table and we want to add columns to it. Since we're building a Bank app fields we need are firstName, lastName, and balance. We'll also keep track of the last time the account was updated, modifiedDate. The account number will be automitically generated and will represent the primary key for our table. In Expando, this primary key doesn't require a standalone column.

Also, we don't want this process to happen every time, so we'll only do it when the table is first created.

...
#if (!$accountsTable)
	#set ($accountsTable = $expandoTableLocalService.addTable($accountsTableName, $accountsTableName))

	#set ($accountsTableId = $accountsTable.getTableId())

#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "firstName", 15)) ## STRING
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "lastName", 15)) ## STRING
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "balance", 5)) ## DOUBLE
#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "modifiedDate", 3)) ## DATE
#end

Notice how we specified, for each column, an integer as the last parameter. These integer constants are defined in com.liferay.portlet.expando.model.ExpandoColumnConstants, and include the types I mentioned above.

Now that we have our table and columns setup, we want to add some logic that will detect and handle the various operations of our application. These are the CRUD operations we need for a complete app.

Let's start with some request handling and param setup.

...
#set ($renderUrl = $request.get("render-url"))
#set ($namespace = $request.get("portlet-namespace"))
#set ($cmd = $request.get("parameters").get("cmd"))

#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)

I won't go into much detail regarding the request handling abilitites of Journal Templates. Suffice it to say that it supports it. (I'll cover that in another Blog entry.)

Determine whether we have been passed an account number. As I mentioned earlier, Expando "records" have a primary key. That field is called "classPK", so we will keep that name to re-inforce the concept.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

Now we're going to check and see what operation (if any) we were asked to perform.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
...
#elseif ($cmd.equals("delete"))
...
#elseif ($cmd.equals("edit"))
...
#end

Adding/Updating an account is our first operation. In this case, we get the params from the request.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	#set ($firstName = $request.get("parameters").get("firstName"))
#set ($lastName = $request.get("parameters").get("lastName"))
#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
#set ($date = $dateTool.getDate())
#elseif ($cmd.equals("delete")) ... #elseif ($cmd.equals("edit")) ... #end

Do some form input checking (this one just does a basic check).

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	#set ($firstName = $request.get("parameters").get("firstName"))
	#set ($lastName = $request.get("parameters").get("lastName"))
	#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
	#set ($date = $dateTool.getDate())

	#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
...
#else
Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
#end
#elseif ($cmd.equals("delete")) ... #elseif ($cmd.equals("edit")) ... #end

So now, if we're ok, store the data.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	#set ($firstName = $request.get("parameters").get("firstName"))
	#set ($lastName = $request.get("parameters").get("lastName"))
	#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
	#set ($date = $dateTool.getDate())

	#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
		#if ($classPK <= 0)
#set ($classPK = $dateTool.getDate().getTime())
#end

#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "firstName", $classPK, $firstName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "lastName", $classPK, $lastName))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "balance", $classPK, $balance))
#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "modifiedDate", $classPK, $date))

#if ($cmd.equals("update"))
Thank you, ${firstName}, for updating your account with our bank!
#else
Thank you, ${firstName}, for creating an account with our bank!
#end
#else Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50. #end #elseif ($cmd.equals("delete")) ... #elseif ($cmd.equals("edit")) ... #end

Before we can continue we need to do some cleanup.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	#set ($firstName = $request.get("parameters").get("firstName"))
	#set ($lastName = $request.get("parameters").get("lastName"))
	#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
	#set ($date = $dateTool.getDate())

	#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
		#if ($classPK <= 0)
			#set ($classPK = $dateTool.getDate().getTime())
		#end

		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "firstName", $classPK, $firstName))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "lastName", $classPK, $lastName))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "balance", $classPK, $balance))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "modifiedDate", $classPK, $date))

		#if ($cmd.equals("update"))
			Thank you, ${firstName}, for updating your account with our bank!
		#else
			Thank you, ${firstName}, for creating an account with our bank!
		#end
	#else
		Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
	#end

	#set ($classPK = 0)
#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)
#elseif ($cmd.equals("delete")) ... #elseif ($cmd.equals("edit")) ... #end

The next operation to handle is Deleting an account.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	...
#elseif ($cmd.equals("delete"))
	#if ($classPK > 0)
#set ($V = $expandoRowLocalService.deleteRow($accountsTableName, $accountsTableName, $classPK))

Account deleted!

#set ($classPK = 0)
#end
#elseif ($cmd.equals("edit")) ... #end

That's it... Pretty simple? Sure is! Next, the Edit operation.

...
#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

#if ($cmd.equals("add") || $cmd.equals("update"))
	...
#elseif ($cmd.equals("delete"))
	...
#elseif ($cmd.equals("edit"))
	Editting...

#if ($classPK > 0)
#set ($firstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $classPK, ""))
#set ($lastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $classPK, ""))
#set ($balance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $classPK, 0.0))
#end
#end

So, we retrieved the data stored in the requested account and put them into some placeholder variables.

Finally, we're ready to display some UI elements. We'll have two different views, a table listing the accounts, and a form for adding/editing accounts.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
...
#else
...
#end

Ok, so when we show the table we need some frillies like a "Create Account" button and some column headers.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />

<br /><br />

<table class="lfr-table">
<tr>
<th>Account Number</th>
<th>First Name</th>
<th>Last Name</th>
<th>Balance</th>
<th>Modified Date</th>
<th><!----></th>
</tr>
#else ... #end

Here we're going to add the calls to get the number of, and list of all the existing accounts.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
	
	<br /><br />

	<table class="lfr-table">
	<tr>
		<th>Account Number</th>
		<th>First Name</th>
		<th>Last Name</th>
		<th>Balance</th>
		<th>Modified Date</th>
		<th><!----></th>
	</tr>

	#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))
#else ... #end

Iterate through the list.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
	
	<br /><br />

	<table class="lfr-table">
	<tr>
		<th>Account Number</th>
		<th>First Name</th>
		<th>Last Name</th>
		<th>Balance</th>
		<th>Modified Date</th>
		<th><!----></th>
	</tr>

	#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
	#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))

	#foreach($row in $rows)
#set ($currentClassPK = $row.getClassPK())

...
#end

#if ($rowsCount <= 0)
<tr>
<td colspan="5">No Accounts were found.</td>
</tr>
#end

</table>

# of Accounts: ${rowsCount}
#else ... #end

Let's draw each table row (the accounts).

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
	
	<br /><br />

	<table class="lfr-table">
	<tr>
		<th>Account Number</th>
		<th>First Name</th>
		<th>Last Name</th>
		<th>Balance</th>
		<th>Modified Date</th>
		<th><!----></th>
	</tr>

	#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
	#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))

	#foreach($row in $rows)
		#set ($currentClassPK = $row.getClassPK())

		<tr>
<td>${currentClassPK}</td>

#set ($currentFirstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $currentClassPK, ""))
<td>${currentFirstName}</td>

#set ($currentLastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $currentClassPK, ""))
<td>${currentLastName}</td>

#set ($currentBalance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $currentClassPK, 0.0))
<td align="right">${numberTool.currency($currentBalance)}</td>

#set ($currentModifiedDate = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "modifiedDate", $currentClassPK, $dateTool.getDate()))
<td>${dateFormatDateTime.format($currentModifiedDate)}</td>

<td>
<a href="${renderUrl}&amp;${namespace}cmd=edit&amp;${namespace}classPK=${currentClassPK}">Edit</a> |
<a href="${renderUrl}&amp;${namespace}cmd=delete&amp;${namespace}classPK=${currentClassPK}">Delete</a>
</td>
</tr>
#end #if ($rowsCount <= 0) <tr> <td colspan="5">No Accounts were found.</td> </tr> #end </table> # of Accounts: ${rowsCount} #else ... #end

Well, that was a mouthfull, but it should be pretty familliar design pattern. The general theme here is "ease of use". It didn't take much time or anything too tricky to get at the data.

The final piece of code is of course the input form.

...
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	...
#else
	<form action="$renderUrl" method="post" name="${namespace}fm10">
<input type="hidden" name="${namespace}classPK" value="${classPK}" />
<input type="hidden" name="${namespace}cmd"
#if ($classPK > 0)
value="update"
#else
value="add"
#end
/>

<table class="lfr-table">
<tr>
<td>First Name:</td>
<td>
<input type="text" name="${namespace}firstName" value="${firstName}" />
</td>
</tr>
<tr>
<td>Last Name:</td>
<td>
<input type="text" name="${namespace}lastName" value="${lastName}" />
</td>
</tr>
<tr>
<td>Balance:</td>
<td>
<input type="text" name="${namespace}balance" value="${numberTool.format($balance)}" />
</td>
</tr>
</table>

<br />

<input type="submit" value="Save" />
<input type="button" value="Cancel" onclick="self.location = '${renderUrl}'" />
</form>
#end

Two significant items to note are ${renderUrl} and ${namespace}. Remember that we're running within the context of a portlet, which means that we have to get a base url from the portal (we can't just use any old url). The code we wrote earlier got us an url from the request. Secondly, we need to namespace any parameter we're going to post back to that url so that the container knows to which portlet it belongs (there might be more that one portlet on the page).

Now you have a result which should look something like this:

Creating/Editting an account:

List with 4 accounts:

Here's the whole template with comments.

#set ($locale = $localeUtil.fromLanguageId($request.get("locale")))
#set ($dateFormatDateTime = $dateFormats.getDateTime($locale))

<h1>First Expando Bank</h1>

##
## Define the "name" for our ExpandoTable.
##

#set ($accountsTableName = "AccountsTable")

##
## Get/Create the ExpandoTable to hold our data.
##

#set ($accountsTable = $expandoTableLocalService.getTable($accountsTableName, $accountsTableName))

#if (!$accountsTable)
	#set ($accountsTable = $expandoTableLocalService.addTable($accountsTableName, $accountsTableName))

	#set ($accountsTableId = $accountsTable.getTableId())

	##
	## Create an ExpandoColumn for each field in the form.
	##

	#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "firstName", 15)) ## STRING
	#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "lastName", 15)) ## STRING
	#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "balance", 5)) ## DOUBLE
	#set ($V = $expandoColumnLocalService.addColumn($accountsTableId, "modifiedDate", 3)) ## DATE
#end

##
## Do some request handling setup.
##

#set ($renderUrl = $request.get("render-url"))
#set ($namespace = $request.get("portlet-namespace"))
#set ($cmd = $request.get("parameters").get("cmd"))

#set ($firstName = '')
#set ($lastName = '')
#set ($balance = 0.0)

##
## Check to see if a classPK was passed in the request.
##

#set ($classPK = $getterUtil.getLong($request.get("parameters").get("classPK")))

##
## Check if we have received a form submission?
##

#if ($cmd.equals("add") || $cmd.equals("update"))
	##
	## Let's get the form values from the request.
	##
	
	#set ($firstName = $request.get("parameters").get("firstName"))
	#set ($lastName = $request.get("parameters").get("lastName"))
	#set ($balance = $getterUtil.getDouble($request.get("parameters").get("balance")))
	#set ($date = $dateTool.getDate())

	##
	## Validate the params to see if we should proceed.
	##

	#if (($cmd.equals("add") && !$firstName.equals("") && !$lastName.equals("") && $balance >= 50) || ($cmd.equals("update") && !$firstName.equals("") && !$lastName.equals("")))
		##
		## Check to see if it's a new Account.
		##
		
		#if ($classPK <= 0)
			#set ($classPK = $dateTool.getDate().getTime())
		#end

		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "firstName", $classPK, $firstName))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "lastName", $classPK, $lastName))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "balance", $classPK, $balance))
		#set ($V = $expandoValueLocalService.addValue($accountsTableName, $accountsTableName, "modifiedDate", $classPK, $date))

		##
		## Show a response.
		##
		
		#if ($cmd.equals("update"))
			Thank you, ${firstName}, for updating your account with our bank!
		#else
			Thank you, ${firstName}, for creating an account with our bank!
		#end

	#else
		Please fill the form completely in order to create an account. The minimum amount of cash required to create an account is $50.
	#end

	#set ($classPK = 0)
	#set ($firstName = '')
	#set ($lastName = '')
	#set ($balance = 0.0)

#elseif ($cmd.equals("delete"))
	##
	## Delete the specified Row.
	##
	
	#if ($classPK > 0)
		#set ($V = $expandoRowLocalService.deleteRow($accountsTableName, $accountsTableName, $classPK))

		Account deleted!

		#set ($classPK = 0)
	#end
#elseif ($cmd.equals("edit"))
	##
	## Edit the specified Row.
	##
	
	Editting...

	#if ($classPK > 0)
		##
		## Get the account specific values
		##

		#set ($firstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $classPK, ""))
		#set ($lastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $classPK, ""))
		#set ($balance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $classPK, 0.0))
	#end
#end
	
<span style="display: block; border-top: 1px solid #CCC; margin: 5px 0px 5px 0px;"></span>

#if (!$cmd.equals("edit"))
	##
	## Now we're into the display logic.
	##
	
	<input type="button" value="Create Account" onClick="self.location = '${renderUrl}&${namespace}cmd=edit';" />
	
	<br /><br />

	<table class="lfr-table">
	<tr>
		<th>Account Number</th>
		<th>First Name</th>
		<th>Last Name</th>
		<th>Balance</th>
		<th>Modified Date</th>
		<th><!----></th>
	</tr>

	##
	## Get all the current records in our ExpandoTable. We can paginate by passing a
	## "begin" and "end" params.
	##

	#set ($rowsCount = $expandoRowLocalService.getRowsCount($accountsTableName, $accountsTableName))
	#set ($rows = $expandoRowLocalService.getRows($accountsTableName, $accountsTableName, -1, -1))

	#foreach($row in $rows)
		##
		## Get the classPK of this row.
		##

		#set ($currentClassPK = $row.getClassPK())

		<tr>
			<td>${currentClassPK}</td>

			#set ($currentFirstName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "firstName", $currentClassPK, ""))
			<td>${currentFirstName}</td>
		
			#set ($currentLastName = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "lastName", $currentClassPK, ""))
			<td>${currentLastName}</td>
		
			#set ($currentBalance = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "balance", $currentClassPK, 0.0))
			<td align="right">${numberTool.currency($currentBalance)}</td>
		
			#set ($currentModifiedDate = $expandoValueLocalService.getData($accountsTableName, $accountsTableName, "modifiedDate", $currentClassPK, $dateTool.getDate()))
			<td>${dateFormatDateTime.format($currentModifiedDate)}</td>
		
			<td>
				<a href="${renderUrl}&amp;${namespace}cmd=edit&amp;${namespace}classPK=${currentClassPK}">Edit</a> |
				<a href="${renderUrl}&amp;${namespace}cmd=delete&amp;${namespace}classPK=${currentClassPK}">Delete</a>
			</td>
		</tr>
	#end

	#if ($rowsCount <= 0)
		<tr>
			<td colspan="5">No Accounts were found.</td>
		</tr>
	#end

	</table>

	# of Accounts: ${rowsCount}
#else
	##
	## Here we have our input form.
	##

	<form action="$renderUrl" method="post" name="${namespace}fm10">
	<input type="hidden" name="${namespace}classPK" value="${classPK}" />
	<input type="hidden" name="${namespace}cmd"
	#if ($classPK > 0)
		value="update"
	#else
		value="add"
	#end
	/>

	<table class="lfr-table">
	<tr>
		<td>First Name:</td>
		<td>
			<input type="text" name="${namespace}firstName" value="${firstName}" />
		</td>
	</tr>
	<tr>
		<td>Last Name:</td>
		<td>
			<input type="text" name="${namespace}lastName" value="${lastName}" />
		</td>
	</tr>
	<tr>
		<td>Balance:</td>
		<td>
			<input type="text" name="${namespace}balance" value="${numberTool.format($balance)}" />
		</td>
	</tr>
	</table>

	<br />
	
	<input type="submit" value="Save" />
	<input type="button" value="Cancel" onclick="self.location = '${renderUrl}'" />
	</form>
#end

<br /><br />

Making Liferay Social - Opensocial

Company Blogs April 15, 2008 By Ray Augé Staff

Social networks and portals are a natural fit. To that end we're currently doing work to integrate the brand new (still in incubation) Shindig project.

Shindig is the Gadget and Opensocial reference implementation currently in incubation under the Apache Software Foundation and being worked on by Google, MySpace, Orkut, Salesforce.com, and many other large social networking sites. Liferay is now joining the fray by integrating the project.

Currently, it takes the form of a Liferay Plugin, which means it's hot deployable. As the Shindig container evolves it may become more or less integrated into Liferay depending on the direction of it's internals.

Currently, it already passes Opensocial 0.7 Compliance Tests:

Opensocial 0.7 Complience Tests


While, not officially a member of the spec team, Liferay will be following along closely and participating in ongoing development.

Announcements and Alerts

Company Blogs April 14, 2008 By Ray Augé Staff

In Liferay 5.0.0+ we have added a new feature: Announcements/Alerts.

Announcements/Alerts are two portlets which can be used to broadcast some message to a list of users within a known scope. Essentially they provide a mass messaging engine you'd tend to think of as a "news letter" or one-way messaging.


They provide the following features:

  1. Configurable, unlimited number of Announcement Types through portal.properties (add locatized type names via Liferay's resource bundle extension mechanism).
  2. Delivery to known scopes, called Delivery Scope, including:
    1. Individual User (API only)
    2. Role
    3. User Group
    4. Community
    5. Organization
  3. Delivery Mechanisms include Email, SMS, and Website. (Website delivery is acheived simply by adding the portlet to any page accessible to the user. The content of the portlet is allways sensitive to the viewing user.)
  4. Scheduled delivery. Each entry has a Display and Expiration Date. The entry won't be delivered until Display Date is <= now. Expiration Date is used primarily for the "Website" delivery mechanism.
  5. Read Tracking. Website delivery tracks timestamped read status per user, per entry.
  6. Subscription Control per user, per Announcement Type. A user determines which Delivery Mechanism to use for each individual Announcement Type. See the My Account page.
  7. Broadcast Control of announcements. Broadcasting Announcements is not a permission which should be granted lightly. As such, a user must first be granted Add Entry permission on the portlet to access the Announcement management functions. Secondly, a user must have Assign Member permission on a given scope in order to be able to broadcast an Announcement/Alert to that scope.

i.e. a Community Owner is not, by default, granted permission to broadcast Announcements or Alerts to their own Community. The Portal Admin must first has to grant them Add Entry permission on the portlet. Only then would they have all the permissions required (they would already have Assign Member as a function of being the owner) to broacast to their own Community. This is to prevent abuse of portal resources and prevent needless spamming of portal users.

Via the service API we now have a means of enabling event based messaging to individual users (or any delivery scope) without having to explicitly write new delivery services or functions.

There are still a few features that I'd like to see added to this portlet, but I think we now have a good base to build on.

Staging v1.2

Company Blogs February 29, 2008 By Ray Augé Staff

(Out of Date, but still mostly relevant, has many improvements though)

Well finally it's in trunk...

 

The sponsored staging enhancements (workflow) (which I've dubbed v1.2) features are now in.

Let me know what you think.

Making remote API calls to Liferay's JSON services from PHP

Company Blogs February 12, 2008 By Ray Augé Staff

Liferay offers a wide variety of different remote API's.

A very useful one is JSON. It's useful because most languages have JSON processing functions. So the only dependencies are those and HTTP request handling functions.

Here is a complete example of adding a Country from a PHP script:

<?php

#
# This example uses HTTP_Request package located found here:
#
#     http://pear.php.net/package/HTTP_Request
#
# Tested with:
#     Liferay Portal - revision 13652
#     HTTP_Request - version 1.4.1
#
require_once("HTTP/Request.php");

#
# URL to the tunnle-web JSON API.
#
$a = &new HTTP_Request('http://localhost:8080/tunnel-web/secure/json');

#
# Use POST in 99% of cases, so that encoding of passed data is handled properly.
#
$a->setMethod("POST");

#
# Specify the authentication credientials.
#
$a->setBasicAuth("2","test");

#
# Specify the service class.
#
$a->addPostData(
"serviceClassName", "com.liferay.portal.service.http.CountryServiceJSON");

#
# Specify the service method.
#
$a->addPostData("serviceMethodName", "addCountry");

#
# List the method parameters.
#
$a->addPostData("serviceParameters", "name,a2,a3,number,idd,active");

#
# Give the values to use in the method.
#
$a->addPostData("name",   "AAAAAA");
$a->addPostData("a2",     "AA");
$a->addPostData("a3",     "AA");
$a->addPostData("number", "1000000");
$a->addPostData("idd",    "1000000");
$a->addPostData("active", "true");

#
# Send the request.
#
$a->sendRequest();

echo $a->getResponseBody();

#
# If the country does not exist, this will print:
#
#   {"active":true,"a2":"AA","a3":"AA","idd":"1000000","countryId":10904,"name":"AAAAAA","number":"1000000"}
#
#
# The above indicates success. Notice that "countryId" is included in the result.
#
?>

Runtime control of your Liferay theme

Company Blogs February 11, 2008 By Ray Augé Staff

Sometimes you want some ability to tweak your Liferay theme at runtime...

Note: Don't do this when you are packaging a theme for public consumption.

It's very easy to add some "on-line" editability/tweakability to your themes, because we all know that we never get it just right the first time... and finding a bug inevitably happens the moment it goes live and/or you are presenting it to the CEO, and/or the moment you don't have access to the source to do another build...

The solution, for convenience or as a safety net, is simple.

- Create a Journal Template of type CSS, not associated with any Structure. Take note of the URL of the template.

- In the theme file "portal_normal.vm", find the section in the head:

[snip]
<head>
    <title>$company_name - $the_title</title>

    $theme.include($top_head_include)

    #css ($css_main_file)
    #js ($js_main_file)

    ...
[/snip]

Now, using the convenient VM macros available, include a reference to the URL you noted above.

    #css ($css_main_file)
    #css ("http://...")

It's better to include the reference after the portal's own, because otherwise you can't overrride it's definitions.

That's it!

Now you can access that template and tweak the theme whenever (and wherever) you are... everyone is happy... of course eventually you can migrate permanent changes to a next iteration of the theme.

 

Hope that helps!

Publishing Flash (SWF) in a flexible way using the Liferay CMS

Company Blogs February 11, 2008 By Ray Augé Staff

Many people ask how to build portlets which display Flash (SWF) content in the portal with options for changing and controlling that content.

Well, you don't have to build a portlet to handle this. I mean, you can... but you don't have too. Liferay provides all the tools you need to do this right out of the CMS and you can give yourself as much control as you like while still making it simple for non-techincal people...

- First, make sure that the Document Library portlet is configured to allow SWF files (should by default in recent versions).

in portal(-ext).properties:

dl.file.extensions=...,.swf,...

- Upload a file.

- Next, in the Journal portlet, create a Journal Structure with the following basic schema (add more params as you see fit).

Journal Structure:
<root>
  <dynamic-element name='swf-file' type='document_library'></dynamic-element>
  <dynamic-element name='width' type='text'></dynamic-element>
  <dynamic-element name='height' type='text'></dynamic-element>
</root>

- Create a Journal Template which will process the values defined in the Structure.

Journal Template (Velocity - VM):
<object width="${width.getData()}" height="${height.getData()}">
  <param name="movie" value="http://@portal_url@${swf-file.getData()}"></param>
  <param name="wmode" value="transparent"></param>
  <embed src="http://@portal_url@${swf-file.getData()}"
  type="application/x-shockwave-flash" wmode="transparent" width="${width.getData()}" height="${height.getData()}"></embed>
</object>

Associate this template with the structure above.

Now create articles which use the above structure and you'll have a file selection dialog available to select a file from Document Library (select your file). Set the desired height, width, "Save", "Approve".

Now all you need to do is add a Journal Content portlet to any page and then select the Article you just created.

The beauty is now that changing the swf is as easy as uploading a new file, and then editting the article and choosing the new file.

Hope that helps!

What's new in the upcomming 4.4.2

Company Blogs February 11, 2008 By Ray Augé Staff

I know the title should probably read "What's new in 4.4.0", or even "4.4.1" (which was just released). But, we've made so many fixes after those two releases that it's worth it to skip right ahead to 4.4.2 (which will likely be out next month).

This will likely be the case moving right up to 5.0, but if I wait till then I'll have so much to write about it'll be to much of a chore to try and sort it all out...

We're really [i]cooking with butter[/i] over the last couple months and it all makes me very excited about Liferay like I hadn't been since we hit 4.0.0...

I mean, Liferay for me is a real love affair... but like every good love affair there is adversity that inevitably makes you wonder what it is about the relationship that keeps you in it... then things start to come into focus... and you remember.

Anyway, the last couple months have reminded me of why I'm in this relationship.

So, here are a couple features that are only the bigginning of a long list of new and/or improved ones you're likely to start hearing about.

 

1. Syndicated feed creation from the Journal portlet.

I really think that many people will like this. It's not so much a feature that users were calling for, per say, but one that we often ended up building out for clients (via some funky xsl/vm templates, and which over the years we've done in various different ways. This one is a little more integrated, and feature rich.

What aspects does this new feature support you ask? Many actually!

• Create as many named feeds as you like.
• The same feed types that we provide from other portlets; RSS 1.0, RSS 2.0, Atom 1.0.
• Constrain feed items based on "Article Type" and/or "Structure" and/or "Template"
• Ordering.
• Max items per request.
• Content rendering options:
  ∘ Article Description
  ∘ Completely rendered article using article's default template
  ∘ Completely rendered using "named" alternate template
  ∘ Specify a particular structure field as the content of items:
    ‣ If the type of the field is "Document Library" the feed will properly create either Atom reference links, or RSS enclosures. (Can you say PODCASTING???)
• Integration with published content.
  ∘ If an article is published somewhere in the target Community, the feed item url will point to that page, otherwise the item will be displayed on the specified page.
  ∘ If you want even more control, you can specify a specific Journal Content portlet on the target page where the item content will be loaded.
• Virtual Hosting friendly URLs.

What's still missing?

As of right now, one of the only missing features is specifying a feed image. (Of course, I'm prepared to admit to/implement other missing features if they are pointed out to me :). )

We still have to build out syndicated feeds for the Asset framework, but that will come, as it's the next logical feature advancement in syndicated publishing.



2. RSS Portlet Facelift.

The RSS portlet got a recent facelift.

I personally think it's much nicer, has more options... and Nate really made it nifty by fixing up my crappy CSS by adding some cool icons everywhere. We haven't added support for Atom reference links, or RSS enclosures yet (podcasting support). You can still specify any number of feeds in a single RSS portlet.

You can even customize the display by including a "header" or "footer" Journal Article in the portlet. Huh?

This give lots of freedom to re-purpose/dress up the portlet easily. For example if you wanted to have the feed items represent more complex data than would normally be handled by the typical RSS feed app, you could add some js code in the header/footer article which (re)formats and/or adds functions to feed items based on their content... So, be creative! That's the idea...

 

Well that's a start. Look for many more features as they crop up on the road to 5.0 and beyond.

Linux Utils - Episode 2

Company Blogs December 17, 2007 By Ray Augé Staff

Ever needed to do file content searches? NO! Then you probably aren't a programmer... j/k

I frequently have to search lots of code for some pattern. You'd think this would be a trivial task, but not even large IDE's like Eclipse make this easy for you... usually they do... but not always.

So, in the cases where my IDE doesn't do the trick I use Sagasu. Sagasu is a front end for traditional file system searches but takes all the complexity out if it, without loosing the power that the command line can bring...

It supports regex patterns, file name patterns, case handling, root directory, multiple tabed searchs, and many other little features that make it very useful. I like that I can specify what application will load a given search result when I click on it... and that (if the app supports it) it can go right to the line in question...

Here's a screen shot after startup:

screenshot 1 of Sagasu

 

So, recently I had to find all the files in the portal (trunk) that defined an inline style, setting a font size. The regular expression of which was style=".*font-size.*". And I only wanted to look in *.jsp, *.jspf, *.js, *.vm. I could safely ignore anything else. Also, I want to open them in gedit at the matching line. (Yes, I can probably do this search in Eclipse... But that wouldn't make for a very usefull blog post about Sagasu, now. Would it?)

Here is the setup view:

screenshot 2 of Sagasu search setup

 

And here are the results:

screenshot 3 of Sagasu search results

79 matches... Ouch!. Gotta fix that...

 

Anyway, you get the file name/path/line# and the line itself in the result list:

screenshot 4 of Sagasu search line fragment

 

And finally, here it is opening a result file in gedit at the correct line:

screenshot 5 of Sagasu opening gedit at correct line

 

Well, there you have it... Sagasu. Enjoy!

 

The ubuntu (debian) install command would be:

    sudo apt-get install sagasu

SWFUploader and Firefox in Linux

Company Blogs December 4, 2007 By Ray Augé Staff

Good news all you Linux Liferay users/developers!

With the most recent release of Adobe Flash Player (Version: 9,0,115,0), the new multi-file uploader available in the portal now works in Linux.

I'm using Ubuntu 7.10, and though there isn't a .deb yet, I just couldn't wait anymore. I downloaded the .tar.gz package, followed the instructions and voilà, it worked!

Note: I just installed it as a user, so it dropped the plugin in the ${HOME}/.mozilla/plugins folder. BUT, I did rename the xpti.dat file to xpti.dat.OLD, just in case it didn't work.

LA, here we come!

Company Blogs December 3, 2007 By Ray Augé Staff

This weekend is the annual Liferay retreat. That means that many of us are preparing to head to LA to meet and greet co-workers we don't often see in person.

For my familly and I, it is especially nice to get away and see new places... we don't get away much.

Well, mother nature was kind enough to realize that we would be away for a week and thoughtfully took it upon herself to make sure we wouldn't miss all the fun.

Mother Nature: "Why don't I send them a going away treat before they leave?"

So, here I am enjoying this wonderful gift and preparing to go to LA.

 

 

Linux Utils - Episode 1

Company Blogs November 18, 2007 By Ray Augé Staff

Need a good diff tool outside of something monolithic like your IDE?

Ever wanted to compare one project folder to another beyond their respective repositories?

Well, look no further than "Meld".

Ubuntu install command:

$ sudo apt-get install meld

Meld is a great diff tool with all the toys and features you could ask for.

The main window:

The main window

Configuring the diff action:

Configuring the diff action

Notice it supports single file, folders, and Version control (CVS, Subversion, Bazaar-ng and Mercurial). Nice...

Comparing two local project folders:

Comparing two local project folders

Now that is really cool...

Selecting a single file from the tree:

Selecting a single file from the tree

Click the arrows and the changes are pushed in that direction... sooo easy...

 

Anyway, 'till next time, lots of love...

Ray

Snippet or Full Content???

Company Blogs November 12, 2007 By Ray Augé Staff

Hey All,

 

Gonna start my first post with a big thanks to everyone who worked on getting Liferay Blogging to a very usable point.


Then, I have a question for everyone out there...

Snippet or Full Content?

By this I mean in the RSS feed of our blogs. Does the world prefer a simple snippet of the content as part of the RSS item? OR does it prefer the Full Content of the post to be included in the RSS item.

Personally? I'd like to see the Full Content in my RSS aggregator. If I decide I want to view the comments... or make one... then I'll click the link back to the source, but otherwise, just let me read... that's what I'd like.

Showing 61 - 79 of 79 results.
Items 20
of 4