Skip to content

Combine hardcoded PrimeFaces resources using CombinedResourceHandler

Melloware edited this page Jan 29, 2021 · 5 revisions

Introduction

PrimeFaces has a few CSS/JS resources which are not declared via @ResourceDependency annotation and therefore cannot be combined by CombinedResourceHandler. Those resources are hardcoded in PrimeFaces own HeadRenderer class.

The PrimeFaces theme.css file has a dynamic library name (representing the theme name) and therefore cannot be declared as @ResourceDependency because the library/resource name must be compiletime constants.

The since PrimeFaces 4.0 introduced validation.js and beanvalidation.js files needs to be conditionally included (depending on whether <p:clientValidator> is present in the view and whether JSR303 bean validation is supported) and therefore cannot be declared as @ResourceDependency because it cannot be conditionally disabled.

Additionally, it also renders some PrimeFaces.settings constants in a plain vanilla <script> element.

Solution

In order to properly combine those hardcoded PrimeFaces CSS/JS resources with CombinedResourceHandler, follow the steps below.

1. Disable PrimeFaces HeadRenderer

First you need to disable the PrimeFaces HeadRenderer by putting back the default HeadRenderer of your JSF implementation in faces-config.xml. Below example assumes that you're using Mojarra.

<render-kit>
    <renderer>
        <component-family>javax.faces.Output</component-family>
        <renderer-type>javax.faces.Head</renderer-type>
        <renderer-class>com.sun.faces.renderkit.html_basic.HeadRenderer</renderer-class>
    </renderer>
</render-kit>

In case you're using MyFaces, use org.apache.myfaces.renderkit.html.HtmlHeadRenderer as <renderer-class>.

2. Create a custom PhaseListener

Then, create a custom PhaseListener for RENDER_RESPONSE phase which will during beforePhase() dynamically add those PrimeFaces resources via UIViewRoot#addComponentResource(). This will run far before those @ResourceDependency annotations are processed. This satisfies PrimeFaces' intent of having those hardcoded resources to be rendered before of the dependencies of their components.

The below beforePhase() logic is based on PrimeFaces 6.1 source.

public class PrimeFacesResourceProcessor implements PhaseListener {

    private static final long serialVersionUID = 1L;

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.RENDER_RESPONSE;
    }

    @Override
    public void beforePhase(PhaseEvent event) {
        FacesContext context = event.getFacesContext();
        PrimeConfiguration primeConfiguration = RequestContext.getCurrentInstance().getApplicationContext().getConfig();

        String theme = "aristo";
        String themeParamValue = primeConfiguration.getTheme();

        if (themeParamValue != null) {
            ELContext elContext = context.getELContext();
            ExpressionFactory expressionFactory = context.getApplication().getExpressionFactory();
            ValueExpression ve = expressionFactory.createValueExpression(elContext, themeParamValue, String.class);
            theme = (String) ve.getValue(elContext);
        }

        if (theme != null && !theme.equals("none")) {
            addCSS(context, "primefaces-" + theme, "theme.css");
        }

        if (primeConfiguration.isFontAwesomeEnabled()) {
            addCSS(context, "primefaces", "fa/font-awesome.css");
        }

        if (primeConfiguration.isClientSideValidationEnabled()) {
            addJS(context, "primefaces", "validation/validation.js");

            if (primeConfiguration.isBeanValidationAvailable()) {
                addJS(context, "primefaces", "validation/beanvalidation.js");
            }
        }
    }

    @Override
    public void afterPhase(PhaseEvent event) {
        // NOOP.
    }

    private void addCSS(FacesContext context, String library, String name) {
        UIOutput css = new UIOutput();
        css.setRendererType("javax.faces.resource.Stylesheet");
        css.getAttributes().put("library", library);
        css.getAttributes().put("name", name);
        context.getViewRoot().addComponentResource(context, css, "head");
    }

    private void addJS(FacesContext context, String library, String name) {
        UIOutput js = new UIOutput();
        js.setRendererType("javax.faces.resource.Script");
        js.getAttributes().put("library", library);
        js.getAttributes().put("name", name);
        context.getViewRoot().addComponentResource(context, js, "head");
    }

}

Register it as below in faces-config.xml:

<lifecycle>
    <phase-listener>com.example.PrimeFacesResourceProcessor</phase-listener>
</lifecycle>

3. Create a custom SystemEventListener

Finally, you need to create a custom SystemEventListener for PostAddToViewEvent on UIViewRoot. This will run after all those @ResourceDependency annotations of PrimeFaces components have been processed. This is thus an ideal moment to add the PrimeFaces.settings script as a component resource, as intented by PrimeFaces.

The below processEvent() logic is based on PrimeFaces 6.1 source.

public class PrimeFacesScriptProcessor implements SystemEventListener {

    @Override
    public boolean isListenerForSource(Object source) {
        return source instanceof UIViewRoot;
    }

    @Override
    public void processEvent(SystemEvent event) throws AbortProcessingException {
        FacesContext context = event.getFacesContext(); // Or FacesContext.getCurrentInstance() if not JSF 2.3 yet.
        ProjectStage projectStage = context.getApplication().getProjectStage();
        PrimeConfiguration primeConfiguration = RequestContext.getCurrentInstance().getApplicationContext().getConfig();

        StringBuilder script = new StringBuilder();

        script.append("if(window.PrimeFaces){");
        script.append("PrimeFaces.settings.locale='").append(context.getViewRoot().getLocale()).append("';");

        if (primeConfiguration.isClientSideValidationEnabled()) {
            script.append("PrimeFaces.settings.validateEmptyFields=").append(primeConfiguration.isValidateEmptyFields()).append(";");
            script.append("PrimeFaces.settings.considerEmptyStringNull=").append(primeConfiguration.isInterpretEmptyStringAsNull()).append(";");
        }

        if (primeConfiguration.isLegacyWidgetNamespace()) {
            script.append("PrimeFaces.settings.legacyWidgetNamespace=true;");
        }

        if (primeConfiguration.isEarlyPostParamEvaluation()) {
            script.append("PrimeFaces.settings.earlyPostParamEvaluation=true;");
        }

        if (!projectStage.equals(ProjectStage.Production)) {
            script.append("PrimeFaces.settings.projectStage='").append(projectStage.toString()).append("';");
        }

        script.append("}");

        addJS(context, script.toString());
    }

    private void addJS(FacesContext context, String script) {
        UIOutput js = new UIOutput();
        js.setRendererType("javax.faces.resource.Script");
        UIOutput content = new UIOutput();
        content.setValue(script);
        js.getChildren().add(content);
        context.getViewRoot().addComponentResource(context, js);
    }

}

Register it as below in faces-config.xml:

<application>
    <system-event-listener>
        <system-event-listener-class>com.example.PrimeFacesScriptProcessor</system-event-listener-class>
        <system-event-class>javax.faces.event.PostAddToViewEvent</system-event-class>
        <source-class>javax.faces.component.UIViewRoot</source-class>
    </system-event-listener>
</application>

Note

Note that you can this way not make use of the PrimeFaces specific <facet name="first|middle|last"> facets in <h:head> to render resources at a specific location in the HTML <head>. But this PrimeFaces-specific feature is already not recognized by CombinedResourceHandler anyway.

PrimeFaces Extensions

Starting with 10.0.0 PrimeFaces Extensions now includes these classes so you don't have to keep them up to date with every PF version change. See ticket: https://github.com/primefaces-extensions/primefaces-extensions/issues/293