16

Is anyone aware of a method to dynamically combine/minify all the h:outputStylesheet resources and then combine/minify all h:outputScript resources in the render phase? The comined/minified resource would probably need to be cached with a key based on the combined resource String or something to avoid excessive processing.

If this feature doesn't exist I'd like to work on it. Does anyone have ideas on the best way to implement something like this. A Servlet filter would work I suppose but the filter would have to do more work than necessary -- basically examining the whole rendered output and replacing matches. Implementing something in the render phase seems like it would work better as all of the static resources are available without having to parse the entire output.

Thanks for any suggestions!

Edit: To show that I'm not lazy and will really work on this with some guidance, here is a stub that captures Script Resources name/library and then removes them from the view. As you can see I have some questions about what to do next ... should I make http requests and get the resources to combine, then combine them and save them to the resource cache?

package com.davemaple.jsf.listener;

import java.util.ArrayList;
import java.util.List;

import javax.faces.component.UIComponent;
import javax.faces.component.UIOutput;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.faces.event.PreRenderViewEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;

import org.apache.log4j.Logger;

/**
 * A Listener that combines CSS/Javascript Resources
 * 
 * @author David Maple<d@davemaple.com>
 *
 */
public class ResourceComboListener implements PhaseListener, SystemEventListener {

    private static final long serialVersionUID = -8430945481069344353L;
    private static final Logger LOGGER = Logger.getLogger(ResourceComboListener.class);

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

    /*
     * (non-Javadoc)
     * @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent)
     */
    public void afterPhase(PhaseEvent event) {
        FacesContext.getCurrentInstance().getViewRoot().subscribeToViewEvent(PreRenderViewEvent.class, this);
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent)
     */
    public void beforePhase(PhaseEvent event) {
        //nothing here
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.event.SystemEventListener#isListenerForSource(java.lang.Object)
     */
    public boolean isListenerForSource(Object source) {
        return (source instanceof UIViewRoot);
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.event.SystemEventListener#processEvent(javax.faces.event.SystemEvent)
     */
    public void processEvent(SystemEvent event) throws AbortProcessingException {
        FacesContext context = FacesContext.getCurrentInstance();
        UIViewRoot viewRoot = context.getViewRoot();
        List<UIComponent> scriptsToRemove = new ArrayList<UIComponent>();

        if (!context.isPostback()) {

            for (UIComponent component : viewRoot.getComponentResources(context, "head")) {
                if (component.getClass().equals(UIOutput.class)) {
                    UIOutput uiOutput = (UIOutput) component;

                    if (uiOutput.getRendererType().equals("javax.faces.resource.Script")) {
                        String library = uiOutput.getAttributes().get("library").toString();
                        String name = uiOutput.getAttributes().get("name").toString();

                        // make https requests to get the resources?
                        // combine then and save to resource cache?
                        // insert new UIOutput script?

                        scriptsToRemove.add(component);
                    }


                }
            }

            for (UIComponent component : scriptsToRemove) {
                viewRoot.getComponentResources(context, "head").remove(component);
            }

        }
    }

}
Dave Maple
  • 8,102
  • 4
  • 45
  • 64
  • 1
    For your information, the full working example of my answer is available as part of [OmniFaces](https://code.google.com/p/omnifaces), a component library which I and my colleague are recently working on based on internal components of m4n.nl. – BalusC Feb 28 '12 at 16:41
  • love it. i got a different implementation working that runs behind nginx as a reverse proxy and writes combined/compressed resources to the filesystem. It's clunky so I'm very excited to check out your working version. – Dave Maple Feb 28 '12 at 20:05

4 Answers4

12

This answer doesn't cover minifying and compression. Minifying of individual CSS/JS resources is better to be delegated to build scripts like YUI Compressor Ant task. Manually doing it on every request is too expensive. Compression (I assume you mean GZIP?) is better to be delegated to the servlet container you're using. Manually doing it is overcomplicated. On Tomcat for example it's a matter of adding a compression="on" attribute to the <Connector> element in /conf/server.xml.


The SystemEventListener is already a good first step (apart from some PhaseListener unnecessity). Next, you'd need to implement a custom ResourceHandler and Resource. That part is not exactly trivial. You'd need to reinvent pretty a lot if you want to be JSF implementation independent.

First, in your SystemEventListener, you'd like to create new UIOutput component representing the combined resource so that you can add it using UIViewRoot#addComponentResource(). You need to set its library attribute to something unique which is understood by your custom resource handler. You need to store the combined resources in an application wide variable along an unique name based on the combination of the resources (a MD5 hash maybe?) and then set this key as name attribute of the component. Storing as an application wide variable has a caching advantage for both the server and the client.

Something like this:

String combinedResourceName = CombinedResourceInfo.createAndPutInCacheIfAbsent(resourceNames);
UIOutput component = new UIOutput();
component.setRendererType(rendererType);
component.getAttributes().put(ATTRIBUTE_RESOURCE_LIBRARY, CombinedResourceHandler.RESOURCE_LIBRARY);
component.getAttributes().put(ATTRIBUTE_RESOURCE_NAME, combinedResourceName + extension);
context.getViewRoot().addComponentResource(context, component, TARGET_HEAD);

Then, in your custom ResourceHandler implementation, you'd need to implement the createResource() method accordingly to create a custom Resource implementation whenever the library matches the desired value:

@Override
public Resource createResource(String resourceName, String libraryName) {
    if (RESOURCE_LIBRARY.equals(libraryName)) {
        return new CombinedResource(resourceName);
    } else {
        return super.createResource(resourceName, libraryName);
    }
}

The constructor of the custom Resource implementation should grab the combined resource info based on the name:

public CombinedResource(String name) {
    setResourceName(name);
    setLibraryName(CombinedResourceHandler.RESOURCE_LIBRARY);
    setContentType(FacesContext.getCurrentInstance().getExternalContext().getMimeType(name));
    this.info = CombinedResourceInfo.getFromCache(name.split("\\.", 2)[0]);
}

This custom Resource implementation must provide a proper getRequestPath() method returning an URI which will then be included in the rendered <script> or <link> element:

@Override
public String getRequestPath() {
    FacesContext context = FacesContext.getCurrentInstance();
    String path = ResourceHandler.RESOURCE_IDENTIFIER + "/" + getResourceName();
    String mapping = getFacesMapping();
    path = isPrefixMapping(mapping) ? (mapping + path) : (path + mapping);
    return context.getExternalContext().getRequestContextPath()
        + path + "?ln=" + CombinedResourceHandler.RESOURCE_LIBRARY;
}

Now, the HTML rendering part should be fine. It'll look something like this:

<link type="text/css" rel="stylesheet" href="/playground/javax.faces.resource/dd08b105bf94e3a2b6dbbdd3ac7fc3f5.css.xhtml?ln=combined.resource" />
<script type="text/javascript" src="/playground/javax.faces.resource/2886165007ccd8fb65771b75d865f720.js.xhtml?ln=combined.resource"></script>

Next, you have to intercept on combined resource requests made by the browser. That's the hardest part. First, in your custom ResourceHandler implementation, you need to implement the handleResourceRequest() method accordingly:

@Override
public void handleResourceRequest(FacesContext context) throws IOException {
    if (RESOURCE_LIBRARY.equals(context.getExternalContext().getRequestParameterMap().get("ln"))) {
        streamResource(context, new CombinedResource(getCombinedResourceName(context)));
    } else {
        super.handleResourceRequest(context);
    }
}

Then you have to do the whole lot of work of implementing the other methods of the custom Resource implementation accordingly such as getResponseHeaders() which should return proper caching headers, getInputStream() which should return the InputStreams of the combined resources in a single InputStream and userAgentNeedsUpdate() which should respond properly on caching related requests.

@Override
public Map<String, String> getResponseHeaders() {
    Map<String, String> responseHeaders = new HashMap<String, String>(3);
    SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US);
    sdf.setTimeZone(TIMEZONE_GMT);
    responseHeaders.put(HEADER_LAST_MODIFIED, sdf.format(new Date(info.getLastModified())));
    responseHeaders.put(HEADER_EXPIRES, sdf.format(new Date(System.currentTimeMillis() + info.getMaxAge())));
    responseHeaders.put(HEADER_ETAG, String.format(FORMAT_ETAG, info.getContentLength(), info.getLastModified()));
    return responseHeaders;
}

@Override
public InputStream getInputStream() throws IOException {
    return new CombinedResourceInputStream(info.getResources());
}

@Override
public boolean userAgentNeedsUpdate(FacesContext context) {
    String ifModifiedSince = context.getExternalContext().getRequestHeaderMap().get(HEADER_IF_MODIFIED_SINCE);

    if (ifModifiedSince != null) {
        SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_RFC1123_DATE, Locale.US);

        try {
            info.reload();
            return info.getLastModified() > sdf.parse(ifModifiedSince).getTime();
        } catch (ParseException ignore) {
            return true;
        }
    }

    return true;
}

I've here a complete working proof of concept, but it's too much of code to post as a SO answer. The above was just a partial to help you in the right direction. I assume that the missing method/variable/constant declarations are self-explaining enough to write your own, otherwise let me know.


Update: as per the comments, here's how you can collect resources in CombinedResourceInfo:

private synchronized void loadResources(boolean forceReload) {
    if (!forceReload && resources != null) {
        return;
    }

    FacesContext context = FacesContext.getCurrentInstance();
    ResourceHandler handler = context.getApplication().getResourceHandler();
    resources = new LinkedHashSet<Resource>();
    contentLength = 0;
    lastModified = 0;

    for (Entry<String, Set<String>> entry : resourceNames.entrySet()) {
        String libraryName = entry.getKey();

        for (String resourceName : entry.getValue()) {
            Resource resource = handler.createResource(resourceName, libraryName);
            resources.add(resource);

            try {
                URLConnection connection = resource.getURL().openConnection();
                contentLength += connection.getContentLength();
                long lastModified = connection.getLastModified();

                if (lastModified > this.lastModified) {
                    this.lastModified = lastModified;
                }
            } catch (IOException ignore) {
                // Can't and shouldn't handle it here anyway.
            }
        }
    }
}

(the above method is called by reload() method and by getters depending on one of the properties which are to be set)

And here's how the CombinedResourceInputStream look like:

final class CombinedResourceInputStream extends InputStream {

    private List<InputStream> streams;
    private Iterator<InputStream> streamIterator;
    private InputStream currentStream;

    public CombinedResourceInputStream(Set<Resource> resources) throws IOException {
        streams = new ArrayList<InputStream>();

        for (Resource resource : resources) {
            streams.add(resource.getInputStream());
        }

        streamIterator = streams.iterator();
        streamIterator.hasNext(); // We assume it to be always true; CombinedResourceInfo won't be created anyway if it's empty.
        currentStream = streamIterator.next();
    }

    @Override
    public int read() throws IOException {
        int read = -1;

        while ((read = currentStream.read()) == -1) {
            if (streamIterator.hasNext()) {
                currentStream = streamIterator.next();
            } else {
                break;
            }
        }

        return read;
    }

    @Override
    public void close() throws IOException {
        IOException caught = null;

        for (InputStream stream : streams) {
            try {
                stream.close();
            } catch (IOException e) {
                if (caught == null) {
                    caught = e; // Don't throw it yet. We have to continue closing all other streams.
                }
            }
        }

        if (caught != null) {
            throw caught;
        }
    }

}

Update 2: a concrete and reuseable solution is available in OmniFaces. See also CombinedResourceHandler showcase page and API documentation for more detail.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • I was hoping you would weigh in! This is exactly what I needed. – Dave Maple Oct 28 '11 at 17:44
  • Question: without using a phase listener, what would be the best way to subscribe the system event for every non-postback request? – Dave Maple Oct 28 '11 at 18:12
  • Register it in `` of `faces-config.xml`: `com.example.CombinedResourceListenerjavax.faces.event.PreRenderViewEvent com.example.CombinedResourceHandler`. The system event listener entry would be superfluous if you extend `UIOutput` with another component having `@ListenerFor` annotation and construct that one instead. – BalusC Oct 28 '11 at 18:18
  • Perfect. As always, thanks for your help and expertise with the framework. Are you aware of any open requests for this feature to be an official part of a future release? – Dave Maple Oct 28 '11 at 18:24
  • You're welcome. No, nothing comes to mind. Closest what I can find in JSF spec issue tracker is [issue 598](http://java.net/jira/browse/JAVASERVERFACES_SPEC_PUBLIC-598) which requests for support of external URLs (like CDNs) in JSF resource handler. As of now, it's scheduled for 2.2 (but I think it won't make it yet, not enough votes). – BalusC Oct 28 '11 at 18:48
  • 1
    Coming back to my previous comment about the listener registration being superfluous, sorry this is not true, the listener is mandatory anyway in order to create the component. Chicken-egg and so on :) It would however have been nice if annotations existed for those JSF listeners like phase, system event and action listeners. – BalusC Oct 28 '11 at 20:09
  • One more question: in your example how are you actually getting the individual resources to combine. Are you making http requests or accessing them via the classpath or some other method? If you could post the relevant bits of CombinedResourceInfo.createAndPutInCacheIfAbsent(resourceNames) that would be helpful. – Dave Maple Oct 30 '11 at 23:14
  • 1
    No, certainly not HTTP requests. Check the `CombinedResource#getInputStream()` implementation in my answer. To the point, just collect the all `Resource`s by the standard `ResourceHandler` as obtained from `Application#getResourceHandler()` and store them in `CombinedResourceInfo`. Then, when `getInputStream()` is called, construct a special `CombinedResourceInputStream` which streams all resources in sequence. I'll update the answer soon with some implementation detail. – BalusC Oct 30 '11 at 23:20
  • A thought: Since we're caching already, what would be the downside of running the entire combined content through YUICompressor and saving the now minified output as a complete item in cache? I know you mentioned the expense of running it on every request which we definitely would need to prevent but a caching mechanism for the whole combined resource would prevent that from being the case. – Dave Maple Oct 30 '11 at 23:44
  • My answer did not intend to cache the content in memory (that would grow exponentially the more different combinations can exist), but only pointers to the resources. You could however store them on (temp) disk and stream it from there instead. Mojarra also does that, which is useful if you have no control over server config, but this can in theory also be done by just a simple servlet filter. – BalusC Oct 31 '11 at 05:48
  • While `CombinedResourceHandler` is an excellent utility, I also love to share about this maven plugin[https://github.com/primefaces-extensions/primefaces-extensions.github.com/wiki/Maven-plugin-for-web-resource-optimization] that can be used to minify/compress js/css files &/or aggregate them into fewer resources **during the build time** & not dynamically during runtime which makes it a more performant solution, I believe.. – Aklin Sep 15 '13 at 19:35
  • @BalusC: Could you please clarify whether omnifaces caches the generated files ie the different files set combined ? – Aklin Sep 16 '13 at 16:59
  • 1
    @Aklin: Nope, this is leveraged to the webbrowser (via HTTP headers) and the underlying JSF impl (via `ResourceHandler`). There's no point of having a copy of different files combined while the individual files are by itself already available in the system. – BalusC Sep 16 '13 at 17:18
  • ohk.. btw does `ResourceHandler` impl(s) usually cache them ? I use Myfaces 2.1.11 – Aklin Sep 16 '13 at 17:20
  • "*There's no point of having a copy of different files combined while the individual files are by itself already available in the system.*" - but it takes time & resources to generate them again for new request at runtime!? – Aklin Sep 16 '13 at 17:21
  • @BalusC: if you could please kindly respond to my comments above. Thanks for your time! – Aklin Sep 17 '13 at 17:06
  • 1
    @Aklin: it seems that there's a misconception/confusion. `CombinedResourceHandler` doesn't minify/compress the resources at all. It just streams existing resources unmodified in sequence into a single response output stream. There's absolutely no additional CPU costs as compared to multiple HTTP requests/responses. If the `CombinedResourceHandler` would be minifying/compressing them, then caching would obviously make more sense. But this is thus not the case. If you want to minify/compress resources, just do it yourself during build time using e.g. YUI compressor or so. – BalusC Sep 17 '13 at 17:12
  • ohkay.. many thanks for clearing this up! Got confused by this thread[http://forum.primefaces.org/viewtopic.php?f=14&t=30077#p96220] whereby this person claims that there are runtime costs associated with omnifaces `CombinedResourceHandler`. Thanks anyway! – Aklin Sep 17 '13 at 18:57
3

Omnifaces provided CombinedResourceHandler is an excellent utility, but I also love to share about this excellent maven plugin:- resources-optimizer-maven-plugin that can be used to minify/compress js/css files &/or aggregate them into fewer resources during the build time & not dynamically during runtime which makes it a more performant solution, I believe.

Also have a look at this excellent library as well:- webutilities

Rajat Gupta
  • 25,853
  • 63
  • 179
  • 294
Aklin
  • 2,621
  • 2
  • 17
  • 17
  • Yeah we use the yuicompressor maven plugin to compress files when we build and then we let Omnifaces combine them at runtime. They are not mutually exclusive solutions. Everyone is encouraged to use them both together for optimal performance. – Dave Maple Sep 16 '13 at 16:52
  • "*They are not mutually exclusive solutions*" - yeah I realized this.. :) – Aklin Sep 16 '13 at 16:56
3

You may want to evaluate JAWR before implementing your own solution. I've used it in couple of projects and it was a big success. It used in JSF 1.2 projects but I think it will be easy to extend it to work with JSF 2.0. Just give it a try.

Maxim Manco
  • 1,954
  • 1
  • 12
  • 19
  • Cool. I'll check it out. I'm not sure how it would capture resources coming from third-party components such as richfaces and primefaces though. I'm really looking to implement something that combines all the static resources required by any component in a view into a single resource. – Dave Maple Oct 27 '11 at 17:17
0

I have an other solution for JSF 2. Might also rok with JSF 1, but i do not know JSF 1 so i can not say. The Idea works mainly with components from h:head and works also for stylesheets. The result is always one JavaScript (or Stylesheet) file for a page! It is hard for me to describe but i try.

I overload the standard JSF ScriptRenderer (or StylesheetRenderer) and configure the renderer for the h:outputScript component in the faces-config.xml. The new Renderer will now not write anymore the script-Tag but it will collect all resources in a list. So first resource to be rendered will be first item in the list, the next follows and so on. After last h:outputScript component ist rendered, you have to render 1 script-Tag for the JavaScript file on this page. I make this by overloading the h:head renderer.

Now comes the idea: I register an filter! The filter will look for this 1 script-Tag request. When this request comes, i will get the list of resources for this page. Now i can fill the response from the list of resources. The order will be correct, because the JSF rendering put the resources in correct order into the list. After response is filled, the list should be cleared. Also you can do more optimizations because you have the code in the filter....

I have code that works superb. My code also can handle browser caching and dynamic script rendering. If anybody is interested i can share the code.

bueyuekt
  • 1
  • 1