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.