views:

435

answers:

2

We'd like to set facelets.development to false to suppress stack traces in non-development environments, but we'd like to have it set to true in dev for debugging.

Our deployment process dictates one CI build that is migrated through the environments up to production, so we can't use an approach that requires rebuilding the app / rewriting web.xml for each environment. We'd like to change the value from the application, based on a properties file setting. Is this possible? How can the application access facelets.development?

+2  A: 

I can think of a few ways to do this, none of them very pleasant.

  • Decorate the FacesContext to control the init parameters programmatically. This is a lot of work for so little gain.
  • Patch the FaceletViewHandler class to get the behaviour you want. This may add maintenance overhead if you upgrade your Facelets libs. May make the folks who manage the app in production unhappy.
  • A variation on the patch approach is to just use the patched JARs in your dev/test machines and put them in the server libs - then use PARENT_FIRST classloading to load them over JARs in the apps (assuming your app server supports all that). The downside of this is that it imposes classloading policies and you have to manage JARs all over the place.

I would favour some other approach. If this setting is required on test machines, perhaps a deployment script could modify the app during the install for those servers. If you want it set to true in your source control, the build script could remove it as part of the build process. This approach would have no impact on your runtime code.

McDowell
+1  A: 

I've implemented a variation on option 1 above. eg

  1. wrote a dynamic proxy for a ServletContext that intercepts the getInitParameter and getInitParameterNames methods returning appropriate values (sourced from environment-specific property files in this case)
  2. wrote a very small subclass of FacesContextFactoryImpl that proxies the first/servletcontext parameter to getFacesContext and then delegates to the superclass.
  3. added a faces-context-factory clause to my faces-config naming my FacesContextFactoryImpl class
  4. I already had a mechanism in place to load environment-specific property files and make them available as a Properties object to the application (the factory in point 2 passes these properties to the proxy in point 1 to use as an alternate source of initParameter values)

The proxy looks like:

package zzzzz.framework.context;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;

import javax.servlet.ServletContext;

import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * A proxy for ServletContext that intercepts accesses to the initParameters and
 * returns values from the specified params instead. Generally useful if we have
 * a set of properties (eg SystemContext.getInstance().getConfigProperties()) that
 * we want to use in preference to the webapp initProperties.
 * 
 * 
 */
public class ServletContextProxy
        implements InvocationHandler {
    @SuppressWarnings("unused")
    private static final Log log = LogFactory.getLog(ServletContextProxy.class);

    @SuppressWarnings("unchecked")
    public static ServletContext newInstance(ServletContext subject,
            Map params) {
        return newInstance(subject,
                params,
                true);
    }

    @SuppressWarnings("unchecked")
    public static ServletContext newInstance(ServletContext subject,
            Map params,
            boolean overrideInitValues) {
        return (ServletContext) Proxy.newProxyInstance(subject.getClass()
                .getClassLoader(),
                subject.getClass()
                        .getInterfaces(),
                new ServletContextProxy(subject,
                        params,
                        overrideInitValues));
    }

    /**
     * A convenience method to help extracting the initParameters from a
     * ServletContext because it doesn't expose it's underlying Map
     * 
     * @param config
     * @return
     */
    @SuppressWarnings("unchecked")
    protected static Map copyInitParameters(Map parms,
            ServletContext config) {
        Enumeration names = config.getInitParameterNames();
        // copy all the existing initParameters
        while (names.hasMoreElements()) {
            String name = (String) names.nextElement();
            parms.put(name,
                    config.getInitParameter(name));
        }
        return parms;
    }

    private boolean overrideInitValues = true;

    @SuppressWarnings("unchecked")
    private Map params;
    private ServletContext subject;

    @SuppressWarnings("unchecked")
    public ServletContextProxy(ServletContext subject,
            Map params,
            boolean overrideInitValues) {
        this.subject = subject;
        this.overrideInitValues = overrideInitValues;
        this.params = new Hashtable();
        if (this.overrideInitValues) { // default behaviour... supplied parameters win
            // start with initParameters
            copyInitParameters(this.params,
                    subject);
            // override and supplement with supplied params
            if (params != null) {
                this.params.putAll(params);
            }
        } else {
            // start with supplied params
            if (params != null) {
                this.params.putAll(params);
            }
            // override and supplement with initParameters
            copyInitParameters(this.params,
                    subject);

        }
    }

    public Object invoke(Object proxy,
            Method m,
            Object[] args) throws Throwable {
        Object result;
        try {
            if ("getInitParameter".equals(m.getName())) {
                result = this.params.get(args[0]);
            } else if ("getInitParameterNames".equals(m.getName())) {
                result = IteratorUtils.asEnumeration(this.params.keySet()
                        .iterator());
            } else {// else let it go through to the keeper
                result = m.invoke(this.subject,
                        args);
            }
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (Exception e) {
            throw new RuntimeException("unexpected invocation exception: "
                    + e.getMessage());
        }
        return result;
    }
}

The factory looks like:

package zzz.faces.context;

import javax.faces.FacesException;
import javax.faces.context.FacesContext;
import javax.faces.lifecycle.Lifecycle;
import javax.servlet.ServletContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import zzzzz.framework.context.ServletContextProxy;
import zzzzz.context.SystemContext;

/**
 * A FacesContextFactory implementation that supplements/overrided the
 * servletContext initParemeters with properties form
 * SystemContext.configProperties
 * <p>
 * The point of this is that it allows us to substitute configuration in the
 * web.xml like this (which requires rewriting web.xml to change)
 * </p>
 * 
 * <pre>
 *   &lt;!-- Enables special Facelets debug output during development --&gt;
 * &lt;context-param&gt;
 *   &lt;param-name&gt;facelets.DEVELOPMENT&lt;/param-name&gt;
 *   &lt;param-value&gt;true&lt;/param-value&gt;
 * &lt;/context-param&gt;
 * </pre>
 * 
 * <p>
 * with settings in the relevent application.properties file like this (which
 * can be changed separately to the webapp)
 * </p>
 * 
 * <pre>
 * # Enables special Facelets debug output during development
 * facelets.DEVELOPMENT=true
 * </pre>
 * 
 * <p>
 * usage: add a clause to faces-config like this:
 * 
 * <pre>
 *   <factory>
 *   <faces-context-factory>zzzzz.faces.context.FacesContextFactoryImpl</faces-context-factory>
 * </factory>
 * </pre>
 * <p>
 * 
 */
public class FacesContextFactoryImpl extends com.sun.faces.context.FacesContextFactoryImpl {
    @SuppressWarnings("unused")
    private static final Log log = LogFactory.getLog(FacesContextFactoryImpl.class);

    public FacesContextFactoryImpl() {
        super();
    }

    @Override
    public FacesContext getFacesContext(Object sc,
            Object request,
            Object response,
            Lifecycle lifecycle) throws FacesException {

        if (sc instanceof ServletContext
                && !(sc instanceof ServletContextProxy)) {
            // wrap the servlet context with a proxy to override/supplement initParameters
            sc = ServletContextProxy.newInstance((ServletContext) sc,
                    SystemContext.getInstance()
                            .getConfigProperties(),
                    true);
        }
        return super.getFacesContext(sc,
                request,
                response,
                lifecycle);
    }
}

and the faces-config looks like

    <faces-config>
  blah waffle....
      <factory>
        <faces-context-factory>zzzz.faces.context.FacesContextFactoryImpl</faces-context-factory>
      </factory>
    </faces-config>

What SystemContext.getInstance().getConfigProperties() looks like is an excercise for another day but it just returns the Map of property values the app is supposed to use

laurie