Project: jsf-test
/*
 * $Id$ 
 * 
 * License Agreement. 
 * 
 * Rich Faces - Natural Ajax for Java Server Faces (JSF) 
 * 
 * Copyright (C) 2007 Exadel, Inc. 
 * 
 * This library is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser General Public 
 * License version 2.1 as published by the Free Software Foundation. 
 * 
 * This library is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 * Lesser General Public License for more details. 
 * 
 * You should have received a copy of the GNU Lesser General Public 
 * License along with this library; if not, write to the Free Software 
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA 
 */
 
package org.jboss.test.faces; 
 
import java.io.File; 
import java.io.IOException; 
import java.io.InputStream; 
import java.net.MalformedURLException; 
import java.net.URL; 
import java.util.ArrayList; 
import java.util.Collection; 
import java.util.EventListener; 
import java.util.HashMap; 
import java.util.List; 
import java.util.Map; 
import java.util.Properties; 
import java.util.concurrent.CopyOnWriteArrayList; 
import java.util.logging.LogManager; 
import java.util.regex.Matcher; 
import java.util.regex.Pattern; 
 
import javax.faces.FacesException; 
import javax.faces.FactoryFinder; 
import javax.faces.application.Application; 
import javax.faces.application.ApplicationFactory; 
import javax.faces.application.StateManager; 
import javax.faces.application.ViewHandler; 
import javax.faces.context.FacesContext; 
import javax.faces.context.FacesContextFactory; 
import javax.faces.lifecycle.Lifecycle; 
import javax.faces.lifecycle.LifecycleFactory; 
import javax.faces.render.ResponseStateManager; 
import javax.faces.webapp.FacesServlet; 
import javax.servlet.Filter; 
import javax.servlet.Servlet; 
 
import org.jboss.test.faces.staging.HttpConnection; 
import org.jboss.test.faces.staging.HttpMethod; 
 
/**
 * <p class="changed_added_4_0"> 
 * </p> 
 *  
 * @author [email protected] 
 *  
 */
 
public class FacesEnvironment { 
 
    public static final String WEB_XML = "/WEB-INF/web.xml"
    public static final String FACES_CONFIG_XML = "/WEB-INF/faces-config.xml"
 
    public class FacesRequest { 
 
        /**
         * Current virtual connection. This field populated by the {@link #setupWebContent()} method only. 
         */
 
        private HttpConnection connection; 
 
        /**
         * Current {@link FacesContext} instance. This field populated by the {@link #setupWebContent()} method only. 
         */
 
        private FacesContext facesContext; 
 
        private String viewId; 
 
        public FacesRequest start() { 
            if (connection.isStarted() || connection.isFinished()) { 
                throw new IllegalStateException(); 
            } 
            connection.start(); 
            FacesContextFactory facesContextFactory = (FacesContextFactory) FactoryFinder 
                .getFactory(FactoryFinder.FACES_CONTEXT_FACTORY); 
            facesContext = facesContextFactory.getFacesContext(facesServer.getContext(), connection.getRequest(), 
                connection.getResponse(), lifecycle); 
            if (null != viewId) { 
                facesContext.setViewRoot(application.getViewHandler().createView(facesContext, viewId)); 
            } 
            return this
        } 
 
        public FacesRequest finish() { 
            if (!connection.isStarted() || connection.isFinished()) { 
                throw new IllegalStateException(); 
            } 
            connection.finish(); 
            return this
  
         
        public byte[] execute() { 
            if (connection.isStarted() || connection.isFinished()) { 
                throw new IllegalStateException(); 
            } 
            connection.execute(); 
            return connection.getResponseBody(); 
        } 
 
        public FacesRequest withViewId(String viewId) { 
            if (connection.isStarted() || connection.isFinished()) { 
                throw new IllegalStateException(); 
            } 
            this.viewId = viewId; 
            return this
        } 
 
        public FacesRequest withParameter(String name, String value) { 
            this.connection.addRequestParameter(name, value); 
            return this
        } 
 
        public String getResponseAsString() { 
         return connection.getContentAsString(); 
  
         
        public void release() { 
            if (null != facesContext) { 
                facesContext.release(); 
                facesContext = null
            } 
            if (null != connection) { 
                if (!connection.isFinished()) { 
                    connection.finish(); 
                } 
                connection = null
            } 
            requests.remove(this); 
        } 
 
        /**
         * <p class="changed_added_4_0"> 
         * </p> 
         *  
         * @return the connection 
         */
 
        public HttpConnection getConnection() { 
            return this.connection; 
        } 
 
        public FacesRequest submit() throws MalformedURLException, FacesException { 
            if (!connection.isFinished()) { 
                throw new IllegalStateException(); 
            } 
            // Extract VIEW_STATE value. 
            Map<String, String> fields = getHiddenFields(connection.getContentAsString()); 
            if (!fields.containsKey(ResponseStateManager.VIEW_STATE_PARAM)) { 
                throw new FacesException("No view state field in response"); 
            } 
            FacesRequest facesRequest = createFacesRequest(connection.getRequest().getRequestURL().toString()) 
                .withViewId(viewId); 
            facesRequest.connection.setRequestMethod(HttpMethod.POST); 
            for (Map.Entry<String, String> entry : fields.entrySet()) { 
                facesRequest.withParameter(entry.getKey(), entry.getValue()); 
            } 
            return facesRequest; 
        } 
 
    } 
 
    private List<FacesRequest> requests = new CopyOnWriteArrayList<FacesRequest>(); 
 
    private ClassLoader contextClassLoader; 
 
    /**
     * Prepared test server instance. Populated by the default {@link #setUp()} method. 
     */
 
    private ApplicationServer facesServer; 
 
    /**
     * JSF {@link Lifecycle} instance. Populated by the default {@link #setUp()} method. 
     */
 
    private Lifecycle lifecycle; 
 
    /**
     * JSF {@link Application} instance. Populated by the default {@link #setUp()} method. 
     */
 
    private Application application; 
 
    private boolean initialized = false
 
    private ServletHolder facesServletContainer; 
 
    private FilterHolder filterContainer; 
 
    private String webXmlDefault; 
 
    private File webRoot; 
 
    public FacesEnvironment() { 
        this(ApplicationServer.createApplicationServer()); 
    } 
 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     */
 
    public FacesEnvironment(ApplicationServer applicationServer) { 
        this.facesServer = applicationServer; 
        setupFacesServlet(); 
        setupFacesListener(); 
        setupJsfInitParameters(); 
        setupWebContent(); 
    } 
 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     *  
     * @return the facesServer 
     */
 
    public ApplicationServer getServer() { 
        return this.facesServer; 
    } 
 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     *  
     * @return the lifecycle 
     */
 
    public Lifecycle getLifecycle() { 
        return this.lifecycle; 
    } 
 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     *  
     * @return the application 
     */
 
    public Application getApplication() { 
        return this.application; 
    } 
 
    public FacesEnvironment withFilter(String name, Filter filter) { 
        checkNotInitialized(); 
        filterContainer = new FilterHolder(facesServletContainer.getMapping(), filter); 
        filterContainer.setName(name); 
 
        return this
    } 
 
    public FacesEnvironment withRichFaces() { 
        checkNotInitialized(); 
        try { 
            Filter ajaxFilter = createInstance("org.ajax4jsf.Filter"); 
            withFilter("ajax4jsf", ajaxFilter); 
            webXmlDefault = "org/jboss/test/faces/ajax-web.xml"
            return this
        } catch (ClassNotFoundException e) { 
            throw new TestException(e); 
        } 
    } 
 
    public FacesEnvironment withSeam() { 
        checkNotInitialized(); 
        try { 
            Filter ajaxFilter = createInstance("org.jboss.seam.servlet.SeamFilter"); 
            withFilter("ajax4jsf", ajaxFilter); 
            EventListener seamListener = createInstance("org.jboss.seam.servlet.SeamListener"); 
            facesServer.addWebListener(seamListener); 
            webXmlDefault = "org/jboss/test/faces/ajax-web.xml"
            return this
        } catch (ClassNotFoundException e) { 
            throw new TestException(e); 
        } 
    } 
 
    public FacesEnvironment withWebRoot(File root) { 
        checkNotInitialized(); 
        webRoot = root; 
        return this
    } 
 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     *  
     * @param path 
     * @param resource 
     * @see org.jboss.test.faces.staging.StagingServer#addResource(java.lang.String, java.net.URL) 
     */
 
    public FacesEnvironment withWebRoot(URL root) { 
        checkNotInitialized(); 
        this.facesServer.addResourcesFromDirectory("/", root); 
        webRoot = null
        return this
    } 
 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     *  
     * @param root 
     * @return 
     */
 
    public FacesEnvironment withWebRoot(String root) { 
        checkNotInitialized(); 
        return withWebRoot(FacesEnvironment.class.getClassLoader().getResource(root)); 
    } 
 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     *  
     * @param name 
     * @param value 
     * @see org.jboss.test.faces.staging.StagingServer#addInitParameter(java.lang.String, java.lang.String) 
     */
 
    public FacesEnvironment withInitParameter(String name, String value) { 
        checkNotInitialized(); 
        this.facesServer.addInitParameter(name, value); 
        return this
    } 
 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     *  
     * @param path 
     * @param resource 
     * @see org.jboss.test.faces.staging.StagingServer#addResource(java.lang.String, java.lang.String) 
     */
 
    public FacesEnvironment withResource(String path, String resource) { 
        this.facesServer.addResource(path, resource); 
        return this
    } 
 
    public FacesEnvironment withResource(String path, URL resource) { 
        this.facesServer.addResource(path, resource); 
        return this
    } 
 
    public FacesEnvironment withResourcesFromDirectory(String path, URL resource){ 
     this.facesServer.addResourcesFromDirectory(path, resource); 
     return this
    } 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     *  
     * @param path 
     * @param resource 
     * @see org.jboss.test.faces.staging.StagingServer#addResource(java.lang.String, java.lang.String) 
     */
 
    public FacesEnvironment withContent(String path, String pageContent) { 
        this.facesServer.addContent(path, pageContent); 
        return this
    } 
 
    /**
     * Setup staging server instance with JSF implementation. First, this method creates a local test instance and calls 
     * the other template method in the next sequence: 
     * <ol> 
     * <li>{@link #setupFacesServlet()}</li> 
     * <li>{@link #setupFacesListener()}</li> 
     * <li>{@link #setupJsfInitParameters()}</li> 
     * <li>{@link #setupWebContent()}</li> 
     * </ol> 
     * After them, test server is initialized as well as fields {@link #lifecycle} and {@link #application} populated. 
     * Also, if the resource "logging.properties" is exist in the test class package, The Java {@link LogManager} will 
     * be configured with its content. 
     *  
     * @throws java.lang.Exception 
     */
 
    public FacesEnvironment start() { 
        contextClassLoader = Thread.currentThread().getContextClassLoader(); 
        Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); 
        facesServer.addResource(WEB_XML, webXmlDefault); 
        if (null != webRoot) { 
            facesServer.addResourcesFromDirectory("/", webRoot); 
        } 
 
        facesServer.addServlet(facesServletContainer); 
 
        if (filterContainer != null) { 
            facesServer.addFilter(filterContainer); 
        } 
 
        facesServer.init(); 
        ApplicationFactory applicationFactory = (ApplicationFactory) FactoryFinder 
            .getFactory(FactoryFinder.APPLICATION_FACTORY); 
        application = applicationFactory.getApplication(); 
        LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder 
            .getFactory(FactoryFinder.LIFECYCLE_FACTORY); 
        lifecycle = lifecycleFactory.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE); 
        initialized = true
        return this
    } 
 
    /**
     * This hook method called from the {@link #setUp()} should append JSF implementation listener to the test server. 
     * Default version applends "com.sun.faces.config.ConfigureListener" or 
     * "org.apache.myfaces.webapp.StartupServletContextListener" for the existed SUN RI or MyFaces implementation. This 
     * metod also calls appropriate {@link #setupSunFaces()} or {@link #setupMyFaces()} methods. 
     */
 
    protected void setupFacesListener() { 
        EventListener listener = null
        try { 
            // Check Sun RI configuration listener class. 
            listener = createInstance("com.sun.faces.config.ConfigureListener"); 
            setupSunFaces(); 
        } catch (ClassNotFoundException e) { 
            // No JSF RI listener, check MyFaces. 
            try { 
                listener = createInstance("org.apache.myfaces.webapp.StartupServletContextListener"); 
                setupMyFaces(); 
            } catch (ClassNotFoundException e1) { 
                throw new TestException("No JSF listeners have been found", e1); 
            } 
        } 
        facesServer.addWebListener(listener); 
    } 
 
    /**
     * This template method called from {@link #setUp()} to create {@link FacesServlet} instance. The default 
     * implementation also tests presense of the "org.ajax4jsf.Filter" class. If this class is avalable, these instance 
     * appended to the Faces Servlet call chain. Default mapping to the FacesServlet instance is "*.jsf" 
     */
 
    protected void setupFacesServlet() { 
        facesServletContainer = new ServletHolder("*.jsf"new FacesServlet()); 
        facesServletContainer.setName("Faces Servlet"); 
        webXmlDefault = "org/jboss/test/faces/web.xml"
    } 
 
    /**
     * This template method called from {@link #setUp()} to append appropriate init parameters to the test server. The 
     * default implementation sets state saving method to the "server", default jsf page suffix to the ".xhtml" and 
     * project stage to UnitTest 
     */
 
    protected void setupJsfInitParameters() { 
        facesServer.addInitParameter(StateManager.STATE_SAVING_METHOD_PARAM_NAME, 
            StateManager.STATE_SAVING_METHOD_SERVER); 
        facesServer.addInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME, ".xhtml"); 
        // Do not use Jsf 2.0 classes directly because this environment should 
        // be applicable for any JSF version. 
        facesServer.addInitParameter("javax.faces.PROJECT_STAGE""UnitTest"); 
    } 
 
    /**
     * This template method called from the {@link #setupFacesListener()} if MyFaces implementation presents. The 
     * default implementation does nothing. 
     */
 
    protected void setupMyFaces() { 
        // Do nothing by default. 
    } 
 
    /**
     * This template method called from the {@link #setupFacesListener()} if Sun JSF reference implementation presents. 
     * The default implementation sets the "com.sun.faces.validateXml" "com.sun.faces.verifyObjects" init parameters to 
     * the "true" 
     */
 
    protected void setupSunFaces() { 
        facesServer.addInitParameter("com.sun.faces.validateXml""true"); 
        facesServer.addInitParameter("com.sun.faces.verifyObjects""true"); 
    } 
 
    /**
     * This template method called from the {@link #setUp()} to populate virtual server content. The default 
     * implementation tries to load web content from directory pointed by the System property "webroot" or same property 
     * from the "/webapp.properties" file. 
     */
 
    protected void setupWebContent() { 
        String webappDirectory = System.getProperty("webroot"); 
        webRoot = null
        if (null == webappDirectory) { 
            URL resource = this.getClass().getResource("/webapp.properties"); 
            if (null != resource && "file".equals(resource.getProtocol())) { 
                Properties webProperties = new Properties(); 
                try { 
                    InputStream inputStream = resource.openStream(); 
                    webProperties.load(inputStream); 
                    inputStream.close(); 
                    webRoot = new File(resource.getPath()); 
                    webRoot = new File(webRoot.getParentFile(), webProperties.getProperty("webroot")).getAbsoluteFile(); 
                } catch (IOException e) { 
                    throw new TestException(e); 
                } 
            } 
        } else { 
            webRoot = new File(webappDirectory); 
        } 
 
    } 
 
    /**
     * Setup virtual server connection to run tests inside JSF lifecycle. The default implementation setups virtual 
     * request to the "http://localhost/test.jsf" URL and creates {@link FacesContext} instance. Two template methods 
     * are called : 
     * <ol> 
     * <li>{@link #setupConnection()} to prepare request method, parameters, headers and so</li> 
     * <li>{@link #setupView()} to create default view.</li> 
     * </ol> 
     *  
     * @throws Exception 
     */
 
    public FacesRequest createFacesRequest() throws Exception { 
        String url = "http://localhost/test.jsf"
        return createFacesRequest(url).withViewId("/test.xhtml"); 
    } 
 
    /**
     * <p class="changed_added_2_0"> 
     * </p> 
     *  
     * @param url 
     * @throws MalformedURLException 
     * @throws FacesException 
     */
 
    public FacesRequest createFacesRequest(String url) throws MalformedURLException, FacesException { 
        FacesRequest request = new FacesRequest(); 
        request.connection = getServer().getConnection(new URL(url)); 
        requests.add(request); 
        return request; 
    } 
 
    /**
     * JSF and Virtual server instance cleanup. 
     *  
     * @throws java.lang.Exception 
     */
 
    public void release() { 
        checkInitialized(); 
        for (FacesRequest request : this.requests) { 
            request.release(); 
        } 
        facesServer.destroy(); 
        Thread.currentThread().setContextClassLoader(contextClassLoader); 
        facesServer = null
        application = null
        lifecycle = null
        initialized = false
    } 
 
    private void checkInitialized() { 
        if (!initialized) { 
            throw new TestException("JSF test environment has not been initialized"); 
        } 
    } 
 
    private void checkNotInitialized() { 
        if (initialized) { 
            throw new TestException("JSF test environment has already been initialized"); 
        } 
    } 
 
    /**
     * <p class="changed_added_4_0"> 
     * </p> 
     *  
     * @param <T> 
     * @param className 
     * @return 
     * @throws TestException 
     * @throws ClassNotFoundException 
     */
 
    @SuppressWarnings("unchecked"
    private <T> T createInstance(String className) throws TestException, ClassNotFoundException { 
        try { 
            Class<?> clazz = FacesEnvironment.class.getClassLoader().loadClass(className); 
            return (T) clazz.newInstance(); 
        } catch (ClassNotFoundException e) { 
            throw e; 
        } catch (Exception e) { 
            throw new TestException(e); 
        } 
    } 
 
    public static FacesEnvironment createEnvironment() { 
        return new FacesEnvironment(); 
    } 
 
    public static FacesEnvironment createEnvironment(ApplicationServer applicationServer) { 
        return new FacesEnvironment(applicationServer); 
    } 
 
     
    static final Pattern INPUT_PATTERN =Pattern.compile("<input([^>]+)>", Pattern.MULTILINE|Pattern.DOTALL); 
     
    static final Pattern NAME_PATTERN =Pattern.compile("name=[\"']([^\"']*)[\"']"); 
 
    static final Pattern VALUE_PATTERN =Pattern.compile("value=[\"']([^\"']*)[\"']"); 
 
    public static Collection<String> getInputFields(String content){ 
        List<String> inputs = new ArrayList<String>(); 
        Matcher matcher = INPUT_PATTERN.matcher(content); 
        while (matcher.find()) { 
            inputs.add(matcher.group(1)); 
        } 
        return inputs; 
    } 
     
    public static Map<String, String> getHiddenFields(String content){ 
        Collection<String> inputFields = getInputFields(content); 
        HashMap<String, String> parameters = new HashMap<String, String>(inputFields.size()); 
        for (String string : inputFields) { 
            if(string.contains("type='hidden'")||string.contains("type=\"hidden\"")){ 
                Matcher matcher = NAME_PATTERN.matcher(string); 
                if(matcher.find()){ 
                    String name = matcher.group(1); 
                    Matcher valueMatcher = VALUE_PATTERN.matcher(string); 
                    if(valueMatcher.find()){ 
                        parameters.put(name, valueMatcher.group(1)); 
                    } else { 
                        parameters.put(name, ""); 
                    } 
                } 
            } 
        } 
        return parameters; 
    } 
 
}