Convert an existing AngularJS App into a Liferay Portlet

As an experiment I tried to deploy an existing AngularJS Application on an Liferay Portal Server. Just to see if it would be possible to use HTML 5 + JSON Services for Portlet development too.
It turned out to be quite simple, but it feels a bit weird of course. For example AngularJS uses hashbangs and they make browser navigation suddenly available on the portlet level.
The approach is also limited, because it might be difficult to have multiple HTML 5 portlets on a single portal page. The DOM manipulation would most likely not work properly. But it could be interesting if you provide portlets for mobile devices that run maximized anyway.

Below a step by step guide:

1. Convert the front-end

Let’s say your index.html looks like this:

<!doctype html>
<html ng-app="my-angularjs-app">
  <head>
    <link rel="stylesheet" href="css/app.cs">
  </head>
  <body>
    <div ng-controller="HelloCntl">
     Your name: <input type="text" ng-model="name"/>
    <hr/>
    Hello {{name || "World"}}!
    </div>

    <script type="text/javascript" src="js/app.js"></script>
  </body>
</html>

To run this as a portlet you have to do the following:

  1. Create a Java Web-Application as usual and copy all your resources into it
  2. In the document root create a view.jsp and copy everything within the body of index.html into it
  3. Wrap the content of view.jsp with an additional div layer and add the attribute ng-app=”my-angularjs-app”
  4. Create the usual portlet configuration files under WEB-INF
  5. Configure Liferay to add the Javascript to the footer of every portal page that contains the portlet
  6. Configure Liferay to add the Stylesheet to the header section of every portal page that contains the portlet

Here the view.jsp:

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

<portlet:defineObjects />

<div ng-app="my-angularjs-app">

  <!-- The body of index.html -->
  <div ng-controller="HelloCntl">
     Your name: <input type="text" ng-model="name"/>
    <hr/>
    Hello {{name || "World"}}!
    </div>

</div>

The portlet.xml and liferay-display.xml config files are straightforward:

<?xml version="1.0"?>
<portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" version="2.0">
    <portlet>
        <portlet-name>my-angularjs-app-portlet</portlet-name>        
        <display-name>my-angularjs-app-portlet</display-name>
        <portlet-class>com.liferay.util.bridges.mvc.MVCPortlet</portlet-class>        
        <init-param>
            <name>view-jsp</name>
            <value>/view.jsp</value>
        </init-param>
        <expiration-cache>0</expiration-cache>
        <supports>
            <mime-type>text/html</mime-type>
        </supports>
    </portlet>
</portlet-app>
<?xml version="1.0"?>
<!DOCTYPE display PUBLIC "-//Liferay//DTD Display 6.1.0//EN" "http://www.liferay.com/dtd/liferay-display_6_1_0.dtd">
<display>
    <category name="AngularJSPortlets">
        <portlet id="my-angularjs-app-portlet" />
    </category>
</display>

In liferay-portlet.xml we need to include the javascripts and stylesheets from the index.html:

<?xml version="1.0"?>
<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 6.0.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_6_0_0.dtd">
<liferay-portlet-app>
	<portlet>
		<portlet-name>my-angularjs-app-portlet</portlet-name>
		<header-portlet-css>/css/app.css</header-portlet-css>
		<footer-portlet-javascript>/js/app.js</footer-portlet-javascript>		
	</portlet>
</liferay-portlet-app>

If you have multiple template files in your application you might have to fix the path to them, because relative URLs won’t work in Portal environment. The URLs need to point to the context of your deployed application. For example, if you have a route like this:

$routeProvider.when('/page1',
  {templateUrl: 'page1.html', controller: 'Page1Ctrl'});
$routeProvider.when('/page2', 
  {templateUrl: 'page2.html', controller: 'Page2Ctrl'});

They should now look like this:

$routeProvider.when('/page1', 
  {templateUrl: '/my-application/page1.html', controller: 'Page1Ctrl'});
$routeProvider.when('/page2', 
  {templateUrl: '/my-application/page2.html', controller: 'Page2Ctrl'});

I know, that’s a bit odd, to hardcode an application context, but you could make the path configurable.

That’s basically it for the front-end. If you deploy your application within a WAR on Liferay you should be able to add your portlet to a portal page.

2. Integrate the back-end

Essentially, there are two possibilities here:

  1. Call remote REST services. Here you could get troubles with the Same-Origin-Policy, but fortunately, AngularJS supports JSONP. Is you choose this solution you’ve nothing else to do.
  2. Deploy the back-end (the services) together with your portlet. You cannot get problems with the Same-Origin-Policy and it is even possible to obtain the currently authenticated portal user in the service code.

I chose option 2, and deployed the Spring MVC REST back-end as part of the portlet. To hide the application context of the portlet and to be able to obtain the shared session attributes of Liferay (e.g. the authenticated user) I’ve used Liferay’s PortalDelegateServlet:

In web.xml of the portlet application:

<servlet>
  <servlet-name>portalDelegateServlet</servlet-name>
  <servlet-class>com.liferay.portal.kernel.servlet.PortalDelegateServlet</servlet-class>
    <init-param>
      <param-name>servlet-class</param-name>
      <param-value>org.springframework.web.servlet.DispatcherServlet</param-value>
    </init-param>
    <init-param>
      <param-name>sub-context</param-name>
      <param-value>rest-services</param-value>
    </init-param>
    <init-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>classpath:/META-INF/webapp-context.xml</param-value>
    </init-param>        
  <load-on-startup>1</load-on-startup>
</servlet>

If you’ve a service defined like this in Spring MVC:

@RequestMapping("/rest-services")
public class MyService {
  @RequestMapping("/customers")
  public List<Customer> customers() {
    //...
  }
}

You can now access your the service under:

http://<liferay-portal>/delegate/rest-services/customers

Please note: This only works, if the sub-context in the delegate definition matches the path prefix of the services.

The advantage of using the delegate (instead of directly accessing the service in the context of the webapp containing your portlet) is that could can obtain the currently authenticated Liferay user like this:

request.getSession().getAttribute("USER_ID")

3. Alternative approach with Spring Portlet MVC

I also tried to use Spring Portlet MVC to get a more traditional portlet setup. It would also have the advantage that the back-end is actually container authenticated and e.g. request.isInRole() could be used.

The problem was, it required a lot of additional configuration and a specific Portlet controller. Also, “normal” URLs couldn’t be used for service calls, they needed to be constructed as Action URLs like this:

<portlet:actionURL var="serviceURL" >
    <portlet:param name="serviceName" value="myServiceName"/>
</portlet:actionURL>
   // Javascript
   var url = "${serviceURL}";

At the end it was too much effort for me to get it running.

Remove duplicate javscript resources of JSF portlets in Liferay

A common problem when developing JSF portlets for a portal server is that each portlet pulls all required resources (JavaScript, CSS) into the resulting portal page. If you have six portlets on a portal page, all the huge JavaScript files are loaded and parsed six times.

Although this is a common problem, i found no common solution for that. I tried a couple of approaches and the best one seemed to be to write a custom JSF head renderer to suppress rendering a resource if it already has been rendered by another portlet (within the same request). So, only the first rendered portlet pulls the resource into the portlet page.

My implementation is very specific for IceFaces and the Liferay Faces Bridge, but the concept itself should be applicable for all JSF frameworks.

Below the implementation of the HeadRenderer. It simple stores all already rendered resources in a shared request attribute (by default all attributes starting with LIFERAY_SHARED_ are automatically shared for all portlets, even across multiple WARs).

package at.nonblocking.icefaces.renderkit;

public class SuppressDuplicateJsHeadRenderer extends HeadRendererICEfacesImpl {

  private static final Logger LOG = LoggerFactory.getLogger(SuppressDuplicateJsHeadRenderer.class);
  
  private static final String PREFIX_SHARED_REQUEST_ATTRIBUTES  = "LIFERAY_SHARED_"; 
  private static final String ICEFACES_VERSION_KEY = "ICEFACE_3_0_0";  
  private static final String RENDERED_RESOURCES_MAP_REQUEST_ATTRIBUTE = PREFIX_SHARED_REQUEST_ATTRIBUTES + "ADDED_RESOURCES_" + ICEFACES_VERSION_KEY;

  private static final String EXTENSION_CSS = "css";

  @SuppressWarnings("unchecked")
  @Override
  public void encodeBegin(FacesContext facesContext, UIComponent uiComponent) throws IOException {
    // Ignore AJAX requests
    if (facesContext.getPartialViewContext().isAjaxRequest()) {
      return;
    }

    ExternalContext externalContext = facesContext.getExternalContext();
    PortletRequest portletRequest = (PortletRequest) externalContext.getRequest();
    BridgeContext bridgeContext = BridgeContext.getCurrentInstance();
    PortletContainer portletContainer = bridgeContext.getPortletContainer();
    String portletName = bridgeContext.getPortletConfig().getPortletName();

    PortletNamingContainerUIViewRoot uiViewRoot = (PortletNamingContainerUIViewRoot) facesContext.getViewRoot();
    
    List<UIComponent> uiComponentResources = buildCssJsResourceList(facesContext, uiComponent, uiViewRoot);

    // Save a temporary reference to the ResponseWriter provided by the
    // FacesContext.
    ResponseWriter responseWriterBackup = facesContext.getResponseWriter();

    // Replace the ResponseWriter in the FacesContext with a HeadResponseWriter
    // that knows how to write to
    // the <head>...</head> section of the rendered portal page.
    HeadResponseWriter headResponseWriter = (HeadResponseWriter) portletRequest.getAttribute("headResponseWriter");

    if (headResponseWriter == null) {
      headResponseWriter = (HeadResponseWriter) portletContainer.getHeadResponseWriter(responseWriterBackup);
    }

    portletRequest.setAttribute("headResponseWriter", headResponseWriter);
    facesContext.setResponseWriter(headResponseWriter);

    // Store the rendered resources in the portlet request as shared attribute - works accross multiple WARs
    List<String> addedResources = (List<String>) portletRequest.getAttribute(RENDERED_RESOURCES_MAP_REQUEST_ATTRIBUTE);
    if (addedResources == null) {
      addedResources = new ArrayList<String>();
      portletRequest.setAttribute(RENDERED_RESOURCES_MAP_REQUEST_ATTRIBUTE, addedResources);
    }

    for (UIComponent uiComponentResource : uiComponentResources) {
      String resourceName = (String) uiComponentResource.getAttributes().get("name");

      if (addedResources.contains(resourceName)) {
        LOG.debug("Ignoring resource '{}' of portlet '{}' because it already has been added to html head.", resourceName, portletName);
      } else {
        LOG.debug("Adding resource '{}' of portlet '{}' to html head.", resourceName, portletName);

        // Command the resource to render itself to the HeadResponseWriter
        uiComponentResource.encodeAll(facesContext);
        addedResources.add(resourceName);
      }
    }

    // Restore the temporary ResponseWriter reference.
    facesContext.setResponseWriter(responseWriterBackup);
  }

  private List<UIComponent> buildCssJsResourceList(FacesContext facesContext, UIComponent uiComponent, PortletNamingContainerUIViewRoot uiViewRoot) {
    List<UIComponent> uiComponentResources = new ArrayList<UIComponent>();

    // Add the list of components that are to appear first.
    List<UIComponent> firstResources = getFirstResources(facesContext, uiComponent);

    if (firstResources != null) {
      uiComponentResources.addAll(firstResources);
    }

    // Sort the components that are in the view root into stylesheets and
    // scripts.
    List<UIComponent> uiViewRootComponentResources = uiViewRoot.getComponentResources(facesContext, TARGET_HEAD);
    List<UIComponent> uiViewRootStyleSheetResources = null;
    List<UIComponent> uiViewRootScriptResources = null;

    for (UIComponent curComponent : uiViewRootComponentResources) {
      String resourceName = (String) curComponent.getAttributes().get("name");

      if ((resourceName != null) && resourceName.endsWith(EXTENSION_CSS)) {

        if (uiViewRootStyleSheetResources == null) {
          uiViewRootStyleSheetResources = new ArrayList<UIComponent>();
        }

        uiViewRootStyleSheetResources.add(curComponent);
      } else {

        if (uiViewRootScriptResources == null) {
          uiViewRootScriptResources = new ArrayList<UIComponent>();
        }

        uiViewRootScriptResources.add(curComponent);
      }
    }

    // Add the list of stylesheet components that are in the view root.
    if (uiViewRootStyleSheetResources != null) {
      uiComponentResources.addAll(uiViewRootStyleSheetResources);
    }

    // Add the list of components that are to appear in the middle.
    List<UIComponent> middleResources = getMiddleResources(facesContext, uiComponent);

    if (middleResources != null) {
      uiComponentResources.addAll(middleResources);
    }

    // Add the list of script components that are in the view root.
    if (uiViewRootScriptResources != null) {
      uiComponentResources.addAll(uiViewRootScriptResources);
    }

    // Add the list of components that are to appear last.
    List<UIComponent> lastResources = getLastResources(facesContext, uiComponent);

    if (lastResources != null) {
      uiComponentResources.addAll(lastResources);
    }

    return uiComponentResources;
  }

}

To install the new HeadRenderer i’ve created a JSF RendererKitFactory and defined it in META-INF/services/javax.faces.render.RenderKitFactory:

at.nonblocking.icefaces.renderkit.SuppressDuplicateJsRenderKitFactory

So far, so good, but now it gets a bit tricky. We have to take care to not unintentionally remove the existing IceFaces, Mojarra and Bridge RenderKitFactory’s. What I did is to inherit my custom RenderKitFactory from com.liferay.faces.bridge.renderkit.html_basic.RenderKitBridgeImpl and just replace the existing Bridge RenderKitFactory.

The RenderKitFactory hierarchy before installing the custom Factory is:
-> org.icefaces.impl.renderkit.DOMRenderKit
  -> com.liferay.faces.bridge.renderkit.html_basic.RenderKitBridgeImpl
    -> com.sun.faces.renderkit.RenderKitImpl

And after it:
-> org.icefaces.impl.renderkit.DOMRenderKit
  -> at.nonblocking.icefaces.renderkit.SuppressDuplicateJsRenderKitFactory
    -> com.sun.faces.renderkit.RenderKitImpl

Below my RenderKitFactory and the RenderKit to introduce the SuppressDuplicateJsHeadRenderer:

package at.nonblocking.icefaces.renderkit;

public class SuppressDuplicateJsRenderKitFactory extends RenderKitFactoryImpl {

  private static final Logger LOG = LoggerFactory.getLogger(SuppressDuplicateJsRenderKitFactory.class);
  
  @Override
  public void addRenderKit(String renderKitId, RenderKit renderKit) {

    if (renderKitId.equals(HTML_BASIC_RENDER_KIT) && renderKit instanceof DOMRenderKit) {
      LOG.info("Installing Javascript reducing RenderKit {}", SuppressDuplicateJsRenderKit.class);
      
      try {
        Field delegateField = renderKit.getClass().getDeclaredField("delegate");
        delegateField.setAccessible(true); //Reflection hack
        
        RenderKitBridgeImpl bridgeRenderKit = (RenderKitBridgeImpl) delegateField.get(renderKit);        
        RenderKit originalRenderKit = bridgeRenderKit.getWrapped();  //instance of com.sun.faces.renderkit.RenderKitImpl
               
        delegateField.set(renderKit, new SuppressDuplicateJsRenderKit(originalRenderKit));
        
      } catch (NoSuchFieldException e) {
        LOG.error("Failed to install Javascript reducing RenderKit!", e);
      } catch (IllegalArgumentException e) {
        LOG.error("Failed to install Javascript reducing RenderKit!", e);
      } catch (IllegalAccessException e) {
        LOG.error("Failed to install Javascript reducing RenderKit!", e);
      }
    }

    super.addRenderKit(renderKitId, renderKit);
  }
}
package at.nonblocking.icefaces.renderkit;

public class SuppressDuplicateJsRenderKit extends RenderKitBridgeImpl {

  private static final String JAVAX_FACES_HEAD = "javax.faces.Head";
  private static final String JAVAX_FACES_OUTPUT = UIOutput.COMPONENT_FAMILY;
  
  public SuppressDuplicateJsRenderKit(RenderKit wrappedRenderKit) {
    super(wrappedRenderKit);
  }
  
  @Override
  public Renderer getRenderer(String family, String rendererType) {
      if (JAVAX_FACES_OUTPUT.equals(family) && JAVAX_FACES_HEAD.equals(rendererType)) {
          return new SuppressDuplicateJsHeadRenderer();      
      }
      
      return super.getRenderer(family, rendererType); 
  } 
}

That’s it. You can test the effect with the Google Page Speed plug-in, it will warn you if any resources are still present multiple times.

There might be better approaches to suppress duplicate resources in a portal and if you know one, please leave a comment.

JMeter tests with Liferay 6 and IceFaces 3

Recently I’ve been given the task to stress test a Liferay 6 portal with several IceFaces portlets on each page. I found the combination quite challenging, so here the approach I used to get it done.

A good starting point was this article describing how to do JMeter tests with plain IceFaces applications. In a few words: JSF and IceFaces add a bunch of dynamic information to each html form, which needs to parametrized in the JMeter script. With JSF 2 and IceFaces 3 the following parameters have to be handled:

  • ice.view
  • ice.window
  • javax.faces.ViewState
  • javax.faces.encodedURL
  • and, if you use single submit: -single-submit

You can get the actual values for this parameters by applying regular expression to the HTML response body of the prior request. But unfortunately, within a portal it’s a bit more complicated. Because each portlet on a portal page have it’s own javax.faces.ViewState and it’s own ice.view value and so on. So you have to tweak the regular expression to only match the fields within the HTML form which is actually going to be submitted.

Liferay adds a prefix to form and input name, which should be calculated the same way on every server (if you’re lucky). In the example below the prefix is A8901:

  <form action="<PORTAL_URL>" class="iceFrm" enctype="application/x-www-form-urlencoded" id="A8901:form" method="post" onsubmit="return false;">
    <input name="A8901:form" type="hidden" value="A8901:form" /> 
    <input name="javax.faces.encodedURL" type="hidden" value="<PORTAL_URL>" />
    <input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState" value="2576275416532915251:-8728981932903525164" autocomplete="off" />
    <input name="ice.window" type="hidden" value="mlh540wgif" />
    <input name="ice.view" type="hidden" value="vlahktfsa" />

    <!-- The actual form -->

  </form>

And that’s still not it: Liferay also adds an authentication token (p_auth) to some URLs to prevent cross site scripting (XSS), this token also has to be extracted from subsequent responses.

So, the following Regular Expression Extractors has to be added to your Test plan (Add -> Post Processors -> Regular Expression Extractor):

name: Extract ice.view of Portlet A8901:

  • Reference Name: iceView
  • Regular Expression: id=”A8901:form” .*?
  • Template: $1$
  • Match No.: 1

Name: Extract ice.window of Portlet A8901:

  • Reference Name: iceWindow
  • Regular Expression: id=”A8901:form” .*?
  • Template: $1$
  • Match No.: 1

Name: Extract JSF ViewState of Portlet A8901:

  • Reference Name: jsfViewState
  • Regular Expression: id=”A8901:form” .*?
  • Template: $1$
  • Match No.: 1

Name: Extract JSF ViewState of Portlet A8901:

  • Reference Name: jsfEncodedURL
  • Regular Expression: id=”A8901:form” .*?
  • Template: $1$
  • Match No.: 1

Name: Extract JSF ViewState of Portlet A8901Extract Liferay p_auth:

  • Reference Name: pAuth
  • Regular Expression: [;?]p_auth=(.+?)&
  • Template: $1$
  • Match No.: 0

After recording the test (see JMeter documentation for that), the Parameter values can be replaced by the Reference Names from the regular expression extractors above:

If you find request URLs which contain something like p_auth=abcdef, replace it with p_auth=${pAuth). The login request for example will contain such a token.

To do all the replacement manually is a lot of work, so I’ve created a XSL script to transform JMeter files accordingly (actually I’ve just adapted the script from the starting point I mentioned above):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

	<xsl:template match="/">
		<xsl:apply-templates />
	</xsl:template>

	<xsl:template match="*|@*">
		<xsl:copy>
			<xsl:apply-templates select="@*" />
			<xsl:apply-templates />
		</xsl:copy>
	</xsl:template>

	<xsl:template match="stringProp[@name='Argument.value']">
		<xsl:choose>
			<xsl:when
				test="preceding-sibling::stringProp[@name='Argument.name' and text()='ice.view']">
				<xsl:copy>
					<xsl:copy-of select="@*" />
					<xsl:value-of select="'${iceView}'" />
				</xsl:copy>
			</xsl:when>
			<xsl:when
				test="preceding-sibling::stringProp[@name='Argument.name' and contains(text(), '-single-submit')]">
				<xsl:copy>
					<xsl:copy-of select="@*" />
					<xsl:value-of select="'${iceView}-single-submit'" />
				</xsl:copy>
			</xsl:when>
			<xsl:when
				test="preceding-sibling::stringProp[@name='Argument.name' and text()='ice.window']">
				<xsl:copy>
					<xsl:copy-of select="@*" />
					<xsl:value-of select="'${iceWindow}'" />
				</xsl:copy>
			</xsl:when>
			<xsl:when
				test="preceding-sibling::stringProp[@name='Argument.name' and text()='javax.faces.ViewState']">
				<xsl:copy>
					<xsl:copy-of select="@*" />
					<xsl:value-of select="'${jsfViewState}'" />
				</xsl:copy>
			</xsl:when>
			<xsl:when
				test="preceding-sibling::stringProp[@name='Argument.name' and text()='javax.faces.encodedURL']">
				<xsl:copy>
					<xsl:copy-of select="@*" />
					<xsl:value-of select="'${jsfEncodedURL}'" />
				</xsl:copy>
			</xsl:when>
			<xsl:otherwise>
				<xsl:copy>
					<xsl:apply-templates select="@*" />
					<xsl:apply-templates />
				</xsl:copy>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>

	<xsl:template
		match="stringProp[@name='Argument.name' and contains(text(), '-single-submit')]">
		<xsl:copy>
			<xsl:apply-templates select="@*" />
			<xsl:value-of select="'${iceView}-single-submit'" />
		</xsl:copy>
	</xsl:template>
</xsl:stylesheet>

The simplest way to apply this script is to store your JMeter .jmx file in a Eclipse project, select from the context menu Run as… -> Run Configurations and create a new XSL run configuration:

From now on you can replace all parameters by simply selecting the created run configuration.