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.