/* * $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 asmirnov@exadel.com * */ 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; } }