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);
}
}
}
}
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
SystemEventListeneris already a good first step (apart from somePhaseListenerunnecessity). Next, you’d need to implement a customResourceHandlerandResource. 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 newUIOutputcomponent representing the combined resource so that you can add it usingUIViewRoot#addComponentResource(). You need to set itslibraryattribute 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 asnameattribute of the component. Storing as an application wide variable has a caching advantage for both the server and the client.Something like this:
Then, in your custom
ResourceHandlerimplementation, you’d need to implement thecreateResource()method accordingly to create a customResourceimplementation whenever the library matches the desired value:The constructor of the custom
Resourceimplementation should grab the combined resource info based on the name:This custom
Resourceimplementation must provide a propergetRequestPath()method returning an URI which will then be included in the rendered<script>or<link>element:Now, the HTML rendering part should be fine. It’ll look something like this:
Next, you have to intercept on combined resource requests made by the browser. That’s the hardest part. First, in your custom
ResourceHandlerimplementation, you need to implement thehandleResourceRequest()method accordingly:Then you have to do the whole lot of work of implementing the other methods of the custom
Resourceimplementation accordingly such asgetResponseHeaders()which should return proper caching headers,getInputStream()which should return theInputStreams of the combined resources in a singleInputStreamanduserAgentNeedsUpdate()which should respond properly on caching related requests.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:(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
CombinedResourceInputStreamlook like:Update 2: a concrete and reuseable solution is available in OmniFaces. See also
CombinedResourceHandlershowcase page and API documentation for more detail.