/* * Copyright 2001-2008 Geert Bevin (gbevin[remove] at uwyn dot com) * Licensed under the Apache License, Version 2.0 (the "License") * $Id: MockConversation.java 3918 2008-04-14 17:35:35Z gbevin $ */ package com.uwyn.rife.test; import com.uwyn.rife.engine.Gate; import com.uwyn.rife.engine.Site; import com.uwyn.rife.engine.exceptions.EngineException; import com.uwyn.rife.tools.ArrayUtils; import com.uwyn.rife.tools.StringUtils; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.Cookie; /** * Simulates a conversation between a web browser and a servlet container. * <p>Cookies will be remembered between requests and can be easily examined. * To check which new cookies have been set during a request, the {@link * MockResponse#getNewCookieNames} method can be used. * <p>An instance of this class is tied to a regular {@link Site} structure * instance. Your tests can thus reference existing site XML declarations, * combine different sites into one, build a new site-structure on-the-fly in * Java, modify existing element declarations, override property injections, * ... * <p>Note that RIFE relies on {@link com.uwyn.rife.engine.EngineClassLoader} * to provide continuations functionalities to pure Java elements. If you want * to test elements that use continuations, you have to make sure the first * class in your test setup is loaded by {@link * com.uwyn.rife.engine.EngineClassLoader}. The easiest way to do so is to run * your main class with {@link RunWithEngineClassLoader}. * * @author Geert Bevin (gbevin[remove] at uwyn dot com) * @version $Revision: 3918 $ * @since 1.1 */ public class MockConversation { final static String SESSION_ID_COOKIE = "JSESSIONID"; final static String SESSION_ID_URL = "jsessionid"; final static String SESSION_URL_PREFIX=";"+SESSION_ID_URL+"="; private Gate mGate = null; private HashMap<String, Cookie> mCookies = new HashMap<String, Cookie>(); private HashMap<String, MockSession> mSessions = new HashMap<String, MockSession>(); private String mScheme = "http"; private String mServerName = "localhost"; private int mServerPort = 80; private String mContextPath = ""; /** * Creates a new <code>MockConversation</code> instance for a particular * site. * * @param site the site structure that will be tested * @since 1.1 */ public MockConversation(Site site) throws EngineException { MockInitConfig initconfig = new MockInitConfig(); mGate = new Gate(site); mGate.init(initconfig); } /** * Perform a request for a particular URL. * * @param url the url that should be tested * @return the response of the request as a {@link MockResponse} instance; * or * <p><code>null</code> if the scheme, hostname and port don't correspond * to the conversation setup * @see #doRequest(String, MockRequest) * @since 1.1 */ public MockResponse doRequest(String url) throws EngineException { return doRequest(url, new MockRequest()); } /** * Perform a request for a particular URL and request configuration. * <p>The request can either be complete with the scheme and hostname, or * an absolute path. These two URLs are thus considered the same: * <pre>http://localhost/some/url?name1=value1&name2=value2</pre> * <pre>/some/url?name1=value1&name2=value2</pre> * <p>Note that when the complete URL form is used, it should correspond * to the scheme, hostname and port configuration of this conversation. * * @param url the url that should be tested * @param request the request that will be used * @return the response of the request as a {@link MockResponse} instance; * or * <p><code>null</code> if the scheme, hostname and port don't correspond * to the conversation setup * @see #doRequest(String) * @since 1.1 */ public MockResponse doRequest(String url, MockRequest request) throws EngineException { if (null == url) throw new IllegalArgumentException("url can't be null"); if (null == request) throw new IllegalArgumentException("request can't be null"); request.setMockConversation(this); // strip away the server root URL, or fail the request in case // the url doesn't start with the correct server root String server_root = request.getServerRootUrl(-1); if (url.indexOf(":/") != -1) { if (!url.startsWith(server_root)) { return null; } url = url.substring(server_root.length()); } // add the parameters in the URL to the request parameters Map<String, String[]> parameters = extractParameters(url); if (parameters != null) { for (Map.Entry<String, String[]> entry : parameters.entrySet()) { if (!request.hasParameter(entry.getKey())) { request.setParameter(entry.getKey(), entry.getValue()); } } } // get the path parameters String path_parameters = null; int path_parameters_index = url.indexOf(";"); if (path_parameters_index != -1) { path_parameters = url.substring(0, path_parameters_index); } // remove the query string int index_query = url.indexOf("?"); if (index_query != -1) { url = url.substring(0, index_query); } // perform the request MockResponse response = new MockResponse(this, request); request.setMockResponse(response); request.setRequestedSessionId(path_parameters); mGate.handleRequest("", url, request, response); return response; } /** * Retrieves the scheme that is used by this conversation. * * @return the scheme of this conversation * @see #setScheme * @see #scheme * @since 1.1 */ public String getScheme() { return mScheme; } /** * Sets the scheme that will be used by this conversation. * * @param scheme the scheme * @see #getScheme * @see #scheme * @since 1.1 */ public void setScheme(String scheme) { mScheme = scheme; } /** * Sets the scheme that will be used by this conversation. * * @param scheme the scheme * @return this <code>MockConversation</code> instance * @see #getScheme * @see #setScheme * @since 1.1 */ public MockConversation scheme(String scheme) { setScheme(scheme); return this; } /** * Retrieves the server name that is used by this conversation. * * @return the server name of this conversation * @see #setServerName * @see #serverName * @since 1.1 */ public String getServerName() { return mServerName; } /** * Sets the server name that will be used by this conversation. * * @param serverName the server name * @see #getServerName * @see #serverName * @since 1.1 */ public void setServerName(String serverName) { mServerName = serverName; } /** * Sets the server name that will be used by this conversation. * * @param serverName the server name * @return this <code>MockConversation</code> instance * @see #getServerName * @see #setServerName * @since 1.1 */ public MockConversation serverName(String serverName) { setServerName(serverName); return this; } /** * Retrieves the server port that is used by this conversation. * * @return the server port of this conversation * @see #setServerPort * @see #serverPort * @since 1.1 */ public int getServerPort() { return mServerPort; } /** * Sets the server port that will be used by this conversation. * * @param serverPort the server port * @see #getServerPort * @see #serverPort * @since 1.1 */ public void setServerPort(int serverPort) { mServerPort = serverPort; } /** * Sets the server port that will be used by this conversation. * * @param serverPort the server port * @return this <code>MockConversation</code> instance * @see #getServerPort * @see #setServerPort * @since 1.1 */ public MockConversation serverPort(int serverPort) { setServerPort(serverPort); return this; } /** * Retrieves the context path that is used by this conversation. * * @return the context path of this conversation * @see #setContextPath * @see #contextPath * @since 1.1 */ public String getContextPath() { return mContextPath; } /** * Sets the context path that will be used by this conversation. * * @param contextPath the context path * @see #getContextPath * @see #contextPath * @since 1.1 */ public void setContextPath(String contextPath) { mContextPath = contextPath; } /** * Sets the context path that will be used by this conversation. * * @param contextPath the context path * @return this <code>MockConversation</code> instance * @see #getContextPath * @see #setContextPath * @since 1.1 */ public MockConversation contextPath(String contextPath) { setContextPath(contextPath); return this; } /** * Checks whether a cookie is present. * * @param name the name of the cookie. * @return <code>true</code> if the cookie was present; or * <p><code>false</code> otherwise * @see #getCookie(String) * @see #getCookieValue(String) * @see #getCookies() * @see #addCookie(Cookie) * @see #addCookie(String, String) * @since 1.1 */ public boolean hasCookie(String name) { if (null == name) throw new IllegalArgumentException("name can't be null"); if (0 == name.length()) throw new IllegalArgumentException("name can't be empty"); if (null == mCookies) { return false; } for (Cookie cookie : mCookies.values()) { if (cookie.getName().equals(name)) { return true; } } return false; } /** * Retrieves a cookie. * * @param name the name of the cookie. * @return the instance of the cookie; or * <p><code>null</code> if no such cookie is present * @see #hasCookie(String) * @see #getCookieValue(String) * @see #getCookies() * @see #addCookie(Cookie) * @see #addCookie(String, String) * @since 1.1 */ public Cookie getCookie(String name) { if (null == name) throw new IllegalArgumentException("name can't be null"); if (0 == name.length()) throw new IllegalArgumentException("name can't be empty"); if (null == mCookies) { return null; } for (Cookie cookie : mCookies.values()) { if (cookie.getName().equals(name)) { return cookie; } } return null; } /** * Retrieves the value of a cookie. * * @param name the name of the cookie. * @return the value of the cookie; or * <p><code>null</code> if no such cookie is present * @see #hasCookie(String) * @see #getCookie(String) * @see #getCookies() * @see #addCookie(Cookie) * @see #addCookie(String, String) * @since 1.1 */ public String getCookieValue(String name) { if (null == name) throw new IllegalArgumentException("name can't be null"); if (0 == name.length()) throw new IllegalArgumentException("name can't be empty"); Cookie cookie = getCookie(name); if (null == cookie) { return null; } return cookie.getValue(); } /** * Retrieves all cookies. * * @return an array with all the cookies; or * <p><code>null</code> if no cookies are present * @see #hasCookie(String) * @see #getCookie(String) * @see #getCookieValue(String) * @see #addCookie(Cookie) * @see #addCookie(String, String) * @since 1.1 */ public Cookie[] getCookies() { if (null == mCookies || 0 == mCookies.size()) { return null; } Cookie[] cookies = new Cookie[mCookies.size()]; mCookies.values().toArray(cookies); return cookies; } /** * Add a cookie. * * @param cookie the cookie instance that will be added * @see #hasCookie(String) * @see #getCookie(String) * @see #getCookieValue(String) * @see #getCookies() * @see #addCookie(String, String) * @since 1.1 */ public void addCookie(Cookie cookie) { if (null == cookie) { return; } mCookies.put(buildCookieId(cookie), cookie); } /** * Add a cookie with only a name and a value, the other fields will be * empty. * * @param name the name of the cookie * @param value the value of the cookie * @see #hasCookie(String) * @see #getCookie(String) * @see #getCookieValue(String) * @see #getCookies() * @see #addCookie(Cookie) * @since 1.1 */ public void addCookie(String name, String value) { if (null == name) throw new IllegalArgumentException("name can't be null"); if (0 == name.length()) throw new IllegalArgumentException("name can't be empty"); addCookie(new Cookie(name, value)); } /** * Add a cookie. * * @param cookie the cookie instance that will be added * @return this <code>MockConversation</code> instance * @see #hasCookie(String) * @see #getCookie(String) * @see #getCookieValue(String) * @see #getCookies() * @see #addCookie(Cookie) * @see #addCookie(String, String) * @since 1.1 */ public MockConversation cookie(Cookie cookie) { addCookie(cookie); return this; } /** * Add a cookie with only a name and a value, the other fields will be * empty. * * @param name the name of the cookie * @param value the value of the cookie * @return this <code>MockConversation</code> instance * @see #hasCookie(String) * @see #getCookie(String) * @see #getCookieValue(String) * @see #getCookies() * @see #addCookie(Cookie) * @see #addCookie(String, String) * @since 1.1 */ public MockConversation cookie(String name, String value) { addCookie(name, value); return this; } static String buildCookieId(Cookie cookie) { StringBuilder cookie_id = new StringBuilder(); if (cookie.getDomain() != null) { cookie_id.append(cookie.getDomain()); } cookie_id.append("\n"); if (cookie.getPath() != null) { cookie_id.append(cookie.getPath()); } cookie_id.append("\n"); if (cookie.getName() != null) { cookie_id.append(cookie.getName()); } return cookie_id.toString(); } static Map<String, String[]> extractParameters(String url) { if (null == url) { return null; } int index_query = url.indexOf("?"); int index_anchor = url.indexOf("#"); if (-1 == index_query) { return null; } String query = null; if (index_anchor != -1) { query = url.substring(index_query+1, index_anchor); } else { query = url.substring(index_query+1); } Map<String, String[]> parameters = new HashMap<String, String[]>(); List<String> query_parts = StringUtils.split(query, "&"); for (String query_part : query_parts) { List<String> parameter = StringUtils.split(query_part, "="); if (2 == parameter.size()) { try { String name = URLDecoder.decode(parameter.get(0), StringUtils.ENCODING_ISO_8859_1); String value = URLDecoder.decode(parameter.get(1), StringUtils.ENCODING_ISO_8859_1); String[] values = parameters.get(name); if (null == values) { values = new String[] {value}; } else { values = ArrayUtils.join(values, value); } parameters.put(name, values); } catch (UnsupportedEncodingException e) { // can't happen, encoding is always supported } } } return parameters; } MockSession getSession(String id) { return mSessions.get(id); } MockSession newHttpSession() { MockSession session = new MockSession(this); mSessions.put(session.getId(), session); return session; } void removeSession(String id) { mSessions.remove(id); } }