smartrics.rest.fitnesse.fixture.RestFixture.java Source code

Java tutorial

Introduction

Here is the source code for smartrics.rest.fitnesse.fixture.RestFixture.java

Source

/*  Copyright 2008 Fabrizio Cannizzo
 *
 *  This file is part of RestFixture.
 *
 *  RestFixture (http://code.google.com/p/rest-fixture/) is free software:
 *  you can redistribute it and/or modify it under the terms of the
 *  GNU Lesser General Public License as published by the Free Software Foundation,
 *  either version 3 of the License, or (at your option) any later version.
 *
 *  RestFixture 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 RestFixture.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  If you want to contact the author please leave a comment here
 *  http://smartrics.blogspot.com/2008/08/get-fitnesse-with-some-rest.html
 */
package smartrics.rest.fitnesse.fixture;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

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

import smartrics.rest.client.RestClient;
import smartrics.rest.client.RestData.Header;
import smartrics.rest.client.RestRequest;
import smartrics.rest.client.RestResponse;
import smartrics.rest.config.Config;
import smartrics.rest.fitnesse.fixture.support.BodyTypeAdapter;
import smartrics.rest.fitnesse.fixture.support.CellFormatter;
import smartrics.rest.fitnesse.fixture.support.CellWrapper;
import smartrics.rest.fitnesse.fixture.support.ContentType;
import smartrics.rest.fitnesse.fixture.support.HeadersTypeAdapter;
import smartrics.rest.fitnesse.fixture.support.JavascriptException;
import smartrics.rest.fitnesse.fixture.support.JavascriptWrapper;
import smartrics.rest.fitnesse.fixture.support.LetHandler;
import smartrics.rest.fitnesse.fixture.support.LetHandlerFactory;
import smartrics.rest.fitnesse.fixture.support.RestDataTypeAdapter;
import smartrics.rest.fitnesse.fixture.support.RowWrapper;
import smartrics.rest.fitnesse.fixture.support.StatusCodeTypeAdapter;
import smartrics.rest.fitnesse.fixture.support.StringTypeAdapter;
import smartrics.rest.fitnesse.fixture.support.Tools;
import smartrics.rest.fitnesse.fixture.support.Url;
import smartrics.rest.fitnesse.fixture.support.Variables;
import fit.ActionFixture;
import fit.Parse;

/**
 * A fixture that allows to simply test REST APIs with minimal efforts. The core
 * principles underpinning this fixture are:
 * <ul>
 * <li>allowing documentation of a REST API by showing how the API looks like.
 * For REST this means
 * <ul>
 * <li>show what the resource URI looks like. For example
 * <code>/resource-a/123/resource-b/234</code>
 * <li>show what HTTP operation is being executed on that resource. Specifically
 * which one fo the main HTTP verbs where under test (GET, POST, PUT, DELETE,
 * HEAD, OPTIONS).
 * <li>have the ability to set headers and body in the request
 * <li>check expectations on the return code of the call in order to document
 * the behaviour of the API
 * <li>check expectation on the HTTP headers and body in the response. Again, to
 * document the behaviour
 * </ul>
 * <li>should work without the need to write/maintain java code: tests are
 * written in wiki syntax.
 * <li>tests should be easy to write and above all read.
 * </ul>
 * 
 * <b>Configuring RestFixture</b><br/>
 * RestFixture can be configured by using the {@link RestFixtureConfig}. A
 * {@code RestFixtureConfig} can define named maps with configuration key/value
 * pairs. The name of the map is passed as second parameter to the
 * {@code RestFixture}. Using a named configuration is optional: if no name is
 * passed, the default configuration map is used. See {@link RestFixtureConfig}
 * for more details.
 * <p/>
 * The following list of configuration parameters can are supported.
 * <p/>
 * <table border="1">
 * <tr>
 * <td>smartrics.rest.fitnesse.fixture.RestFixtureConfig</td>
 * <td><i>optional named config</i></td>
 * </tr>
 * <tr>
 * <td>http.proxy.host</td>
 * <td><i>http proxy host name (RestClient proxy configutation)</i></td>
 * </tr>
 * <tr>
 * <td>http.proxy.port</td>
 * <td><i>http proxy host port (RestClient proxy configutation)</i></td>
 * </tr>
 * <tr>
 * <td>http.basicauth.username</td>
 * <td><i>username for basic authentication (RestClient proxy configutation)</i>
 * </td>
 * </tr>
 * <tr>
 * <td>http.basicauth.password</td>
 * <td><i>password for basic authentication (RestClient proxy configutation)</i>
 * </td>
 * </tr>
 * <tr>
 * <td>http.client.connection.timeout</td>
 * <td><i>client timeout for http connection (default 3s). (RestClient proxy
 * configutation)</i></td>
 * </tr>
 * <tr>
 * <tr>
 * <td>http.client.use.new.http.uri.factory</td>
 * <td><i>If set to true uses a more relaxed validation rule to validate URIs.
 * It, for example, allows array parameters in the query string. Defaults to
 * false.</i></td>
 * </tr>
 * <tr>
 * <td>restfixture.display.actual.on.right</td>
 * <td><i>boolean value. if true, the actual value of the header or body in an
 * expectation cell is displayed even when the expectation is met.</i></td>
 * </tr>
 * <tr>
 * <td>restfixture.default.headers</td>
 * <td><i>comma separated list of key value pairs representing the default list
 * of headers to be passed for each request. key and values are separated by a
 * colon. Entries are sepatated by \n. {@link RestFixture#setHeader()} will
 * override this value. </i></td>
 * </tr>
 * <tr>
 * <td>restfixture.xml.namespaces.context</td>
 * <td><i>comma separated list of key value pairs representing namespace
 * declarations. The key is the namespace alias, the value is the namespace URI.
 * alias and URI are separated by a = sign. Entries are sepatated by
 * {@code System.getProperty("line.separator")}. These entries will be used to
 * define the namespace context to be used in xpaths that are evaluated in the
 * results.</i></td>
 * </tr>
 * <tr>
 * <td>restfixture.content.handlers.map</td>
 * <td><i>a map of contenty type to type adapters, entries separated by \n, and
 * kye-value separated by '='. Available type adapters are JS, TEXT, JSON, XML
 * (see {@link smartrics.rest.fitnesse.fixture.support.BodyTypeAdapterFactory}
 * ).</i></td>
 * </tr>
 * <tr>
 * <td>restfixture.null.value.representation</td>
 * <td><i>This string is used in replacement of the default string substituted
 * when a null value is set for a symbol. Because now the RestFixture labels
 * support is implemented on top of the Fitnesse symbols, such default value is
 * defined in Fitnesse, and that is the string 'null'. Hence, every substitution
 * that would result in rendering the string 'null' is replaced with the value
 * set for this config key. This value can also be the empty string to replace
 * null with empty.</i></td>
 * </tr>
 * 
 * </table>
 * 
 * @author smartrics
 */
public class RestFixture extends ActionFixture {

    /**
     * What runner this table is running on.
     * 
     * Note, the OTHER runner is primarily for testing purposes.
     * 
     * @author fabrizio
     * 
     */
    enum Runner {
        SLIM, FIT, OTHER;
    };

    private static final String LINE_SEPARATOR = "\n";

    private static final String FILE = "file";

    private static final Log LOG = LogFactory.getLog(RestFixture.class);

    protected Variables GLOBALS;

    private RestResponse lastResponse;

    private RestRequest lastRequest;

    private String fileName = null;

    private String multipartFileName = null;

    private String multipartFileParameterName = FILE;

    private String requestBody;

    private Map<String, String> requestHeaders;

    private RestClient restClient;

    private Config config;

    private boolean displayActualOnRight;

    private boolean debugMethodCall = false;

    /**
     * the headers passed to each request by default.
     */
    private Map<String, String> defaultHeaders = new HashMap<String, String>();

    private Map<String, String> namespaceContext = new HashMap<String, String>();

    private Url baseUrl;

    @SuppressWarnings("rawtypes")
    protected RowWrapper row;

    private CellFormatter<?> formatter;

    private PartsFactory partsFactory;

    private String lastEvaluation;

    private int minLenForCollapseToggle;

    /**
     * Constructor for Fit runner.
     */
    public RestFixture() {
        super();
        this.partsFactory = new PartsFactory();
        this.displayActualOnRight = true;
        this.minLenForCollapseToggle = -1;
    }

    /**
     * Constructor for Slim runner.
     * 
     * @param args
     *            the cells following up the first cell in the first row.
     */
    public RestFixture(String hostName) {
        this(hostName, Config.DEFAULT_CONFIG_NAME);
    }

    /**
     * Constructor for Slim runner.
     * 
     * @param args
     *            the cells following up the first cell in the first row.
     */
    public RestFixture(String hostName, String configName) {
        this(new PartsFactory(), hostName, configName);
    }

    RestFixture(PartsFactory partsFactory, String hostName) {
        this(partsFactory, hostName, Config.DEFAULT_CONFIG_NAME);
    }

    RestFixture(PartsFactory partsFactory, String hostName, String configName) {
        this.displayActualOnRight = true;
        this.minLenForCollapseToggle = -1;
        this.partsFactory = partsFactory;
        this.config = Config.getConfig(configName);
        this.baseUrl = new Url(stripTag(hostName));
    }

    /**
     * @return the config used for this fixture instance
     */
    public Config getConfig() {
        return config;
    }

    /**
     * @return the result of the last evaluation performed via evalJs.
     */
    public String getLastEvaluation() {
        return lastEvaluation;
    }

    /**
     * The base URL as defined by the rest fixture ctor or input args.
     * 
     * @return the base URL as string
     */
    public String getBaseUrl() {
        if (baseUrl != null) {
            return baseUrl.toString();
        }
        return null;
    }

    /**
     * The default headers as defined in the config used to initialise this
     * fixture.
     * 
     * @return the map of default headers.
     */
    public Map<String, String> getDefaultHeaders() {
        return defaultHeaders;
    }

    /**
     * The formatter for this instance of the RestFixture.
     * 
     * @return
     */
    public CellFormatter<?> getFormatter() {
        return formatter;
    }

    /**
     * Slim Table table hook.
     * 
     * @param rows
     * @return
     */
    public List<List<String>> doTable(List<List<String>> rows) {
        initialize(Runner.SLIM);
        List<List<String>> res = new Vector<List<String>>();
        getFormatter().setDisplayActual(displayActualOnRight);
        getFormatter().setMinLenghtForToggleCollapse(minLenForCollapseToggle);
        for (List<String> r : rows) {
            processSlimRow(res, r);
        }
        return res;
    }

    @Override
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void doCells(Parse parse) {
        config = Config.getConfig(getConfigNameFromArgs());
        String url = getBaseUrlFromArgs();
        if (url != null) {
            baseUrl = new Url(stripTag(url));
        }
        initialize(Runner.FIT);
        getFormatter().setDisplayActual(displayActualOnRight);
        getFormatter().setMinLenghtForToggleCollapse(minLenForCollapseToggle);
        ((FitFormatter) getFormatter()).setActionFixtureDelegate(this);
        RowWrapper currentRow = new FitRow(parse);
        try {
            processRow(currentRow);
        } catch (Exception exception) {
            getFormatter().exception(currentRow.getCell(0), exception);
        }
    }

    /**
     * Process args to extract the optional config name.
     * 
     * @return
     */
    protected String getConfigNameFromArgs() {
        if (args.length >= 2) {
            return args[1];
        }
        return null;
    }

    /**
     * Process args ({@see fit.Fixture}) for Fit runner to extract the baseUrl
     * of each Rest request, first parameter of each RestFixture table.
     * 
     * @return
     */
    protected String getBaseUrlFromArgs() {
        if (args.length > 0) {
            return args[0];
        }
        return null;
    }

    /**
     * Overrideable method to validate the state of the instance in execution. A
     * {@link RestFixture} is valid if the baseUrl is not null.
     * 
     * @return true if the state is valid, false otherwise
     */
    protected boolean validateState() {
        return baseUrl != null;
    }

    protected void setConfig(Config c) {
        this.config = c;
    }

    /**
     * Method invoked to notify that the state of the RestFixture is invalid. It
     * throws a {@link RuntimeException} with a message displayed in the
     * FitNesse page.
     * 
     * @param state
     *            as returned by {@link RestFixture#validateState()}
     */
    protected void notifyInvalidState(boolean state) {
        if (!state) {
            throw new RuntimeException("You must specify a base url in the |start|, after the fixture to start");
        }
    }

    /**
     * Allows setting of the name of the multi-part file to upload.
     * 
     * <code>| setMultipartFileName | Name of file |</code>
     * <p/>
     * body text should be location of file which needs to be sent
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void setMultipartFileName() {
        CellWrapper cell = row.getCell(1);
        if (cell == null) {
            getFormatter().exception(row.getCell(0), "You must pass a multipart file name to set");
        } else {
            multipartFileName = GLOBALS.substitute(cell.text());
            renderReplacement(cell, multipartFileName);
        }
    }

    public String getMultipartFileName() {
        return multipartFileName;
    }

    /**
     * Allows setting of the name of the file to upload.
     * 
     * <code>| setFileName | Name of file |</code>
     * <p/>
     * body text should be location of file which needs to be sent
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void setFileName() {
        CellWrapper cell = row.getCell(1);
        if (cell == null) {
            getFormatter().exception(row.getCell(0), "You must pass a file name to set");
        } else {
            fileName = GLOBALS.substitute(cell.text());
            renderReplacement(cell, fileName);
        }
    }

    public String getFileName() {
        return fileName;
    }

    /**
     * Sets the parameter to send in the request storing the multi-part file to
     * upload. If not specified the default is <code>file</code>
     * <p/>
     * <code>| setMultipartFileParameterName | Name of form parameter for the uploaded file |</code>
     * <p/>
     * body text should be the name of the form parameter, defaults to 'file'
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void setMultipartFileParameterName() {
        CellWrapper cell = row.getCell(1);
        if (cell == null) {
            getFormatter().exception(row.getCell(0), "You must pass a parameter name to set");
        } else {
            multipartFileParameterName = GLOBALS.substitute(cell.text());
            renderReplacement(cell, multipartFileParameterName);
        }
    }

    public String getMultipartFileParameterName() {
        return multipartFileParameterName;
    }

    /**
     * <code>| setBody | body text goes here |</code>
     * <p/>
     * body text can either be a kvp or a xml. The <code>ClientHelper</code>
     * will figure it out
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void setBody() {
        CellWrapper cell = row.getCell(1);
        if (cell == null) {
            getFormatter().exception(row.getCell(0), "You must pass a body to set");
        } else {
            String text = getFormatter().fromRaw(cell.text());
            requestBody = GLOBALS.substitute(text);
            renderReplacement(cell, requestBody);
        }
    }

    /**
     * <code>| setHeader | http headers go here as nvp |</code>
     * <p/>
     * header text must be nvp. name and value must be separated by ':' and each
     * header is in its own line
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void setHeader() {
        CellWrapper cell = row.getCell(1);
        if (cell == null) {
            getFormatter().exception(row.getCell(0), "You must pass a header map to set");
        } else {
            String header = GLOBALS.substitute(cell.text());
            requestHeaders = parseHeaders(header);
        }
    }

    /**
     * Equivalent to setHeader - syntactic sugar to indicate that you can now.
     * 
     * set multiple headers in a single call
     */
    public void setHeaders() {
        setHeader();
    }

    /**
     * <code> | PUT | URL | ?ret | ?headers | ?body |</code>
     * <p/>
     * executes a PUT on the URL and checks the return (a string representation
     * the operation return code), the HTTP response headers and the HTTP
     * response body
     * 
     * URL is resolved by replacing global variables previously defined with
     * <code>let()</code>
     * 
     * the HTTP request headers can be set via <code>setHeaders()</code>. If not
     * set, the list of default headers will be set. See
     * <code>DEF_REQUEST_HEADERS</code>
     */
    public void PUT() {
        debugMethodCallStart();
        doMethod(emptifyBody(requestBody), "Put");
        debugMethodCallEnd();
    }

    /**
     * <code> | GET | uri | ?ret | ?headers | ?body |</code>
     * <p/>
     * executes a GET on the uri and checks the return (a string repr the
     * operation return code), the http response headers and the http response
     * body
     * 
     * uri is resolved by replacing vars previously defined with
     * <code>let()</code>
     * 
     * the http request headers can be set via <code>setHeaders()</code>. If not
     * set, the list of default headers will be set. See
     * <code>DEF_REQUEST_HEADERS</code>
     */
    public void GET() {
        debugMethodCallStart();
        doMethod("Get");
        debugMethodCallEnd();
    }

    /**
     * <code> | DELETE | uri | ?ret | ?headers | ?body |</code>
     * <p/>
     * executes a DELETE on the uri and checks the return (a string repr the
     * operation return code), the http response headers and the http response
     * body
     * 
     * uri is resolved by replacing vars previously defined with
     * <code>let()</code>
     * 
     * the http request headers can be set via <code>setHeaders()</code>. If not
     * set, the list of default headers will be set. See
     * <code>DEF_REQUEST_HEADERS</code>
     */
    public void DELETE() {
        debugMethodCallStart();
        doMethod("Delete");
        debugMethodCallEnd();
    }

    /**
     * <code> | POST | uri | ?ret | ?headers | ?body |</code>
     * <p/>
     * executes a POST on the uri and checks the return (a string repr the
     * operation return code), the http response headers and the http response
     * body
     * 
     * uri is resolved by replacing vars previously defined with
     * <code>let()</code>
     * 
     * post requires a body that can be set via <code>setBody()</code>.
     * 
     * the http request headers can be set via <code>setHeaders()</code>. If not
     * set, the list of default headers will be set. See
     * <code>DEF_REQUEST_HEADERS</code>
     */
    public void POST() {
        debugMethodCallStart();
        doMethod(emptifyBody(requestBody), "Post");
        debugMethodCallEnd();
    }

    /**
     * <code> | let | label | type | loc | expr |</code>
     * <p/>
     * allows to associate a value to a label. values are extracted from the
     * body of the last successful http response.
     * <ul>
     * <li/><code>label</code> is the label identifier
     * 
     * <li/><code>type</code> is the type of operation to perform on the last
     * http response. At the moment only XPaths and Regexes are supported. In
     * case of regular expressions, the expression must contain only one group
     * match, if multiple groups are matched the label will be assigned to the
     * first found <code>type</code> only allowed values are <code>xpath</code>
     * and <code>regex</code>
     * 
     * <li/><code>loc</code> where to apply the <code>expr</code> of the given
     * <code>type</code>. Currently only <code>header</code> and
     * <code>body</code> are supported. If type is <code>xpath</code> by default
     * the expression is matched against the body and the value in loc is
     * ignored.
     * 
     * <li/><code>expr</code> is the expression of type <code>type</code> to be
     * executed on the last http response to extract the content to be
     * associated to the label.
     * </ul>
     * <p/>
     * <code>label</code>s can be retrieved after they have been defined and
     * their scope is the fixture instance under execution. They are stored in a
     * map so multiple calls to <code>let()</code> with the same label will
     * override the current value of that label.
     * <p/>
     * Labels are resolved in <code>uri</code>s, <code>header</code>s and
     * <code>body</code>es.
     * <p/>
     * In order to be resolved a label must be between <code>%</code>, e.g.
     * <code>%id%</code>.
     * <p/>
     * The test row must have an empy cell at the end that will display the
     * value extracted and assigned to the label.
     * <p/>
     * Example: <br/>
     * <code>| GET | /services | 200 | | |</code><br/>
     * <code>| let | id |  body | /services/id[0]/text() | |</code><br/>
     * <code>| GET | /services/%id% | 200 | | |</code>
     * <p/>
     * or
     * <p/>
     * <code>| POST | /services | 201 | | |</code><br/>
     * <code>| let  | id | header | /services/([.]+) | |</code><br/>
     * <code>| GET  | /services/%id% | 200 | | |</code>
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void let() {
        debugMethodCallStart();
        String label = row.getCell(1).text().trim();
        String loc = row.getCell(2).text();
        CellWrapper exprCell = row.getCell(3);
        exprCell.body(GLOBALS.substitute(exprCell.body()));
        String expr = exprCell.text();
        CellWrapper valueCell = row.getCell(4);
        String valueCellText = valueCell.body();
        String valueCellTextReplaced = GLOBALS.substitute(valueCellText);
        valueCell.body(valueCellTextReplaced);
        String sValue = null;
        try {
            LetHandler letHandler = LetHandlerFactory.getHandlerFor(loc);
            if (letHandler != null) {
                StringTypeAdapter adapter = new StringTypeAdapter();
                try {
                    sValue = letHandler.handle(getLastResponse(), namespaceContext, expr);
                    exprCell.body(getFormatter().gray(exprCell.body()));
                } catch (RuntimeException e) {
                    getFormatter().exception(exprCell, e.getMessage());
                }
                GLOBALS.put(label, sValue);
                adapter.set(sValue);
                getFormatter().check(valueCell, adapter);
            } else {
                getFormatter().exception(exprCell, "I don't know how to process the expression for '" + loc + "'");
            }
        } catch (RuntimeException e) {
            getFormatter().exception(exprCell, e);
        } finally {
            debugMethodCallEnd();
        }
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void comment() {
        debugMethodCallStart();
        CellWrapper messageCell = row.getCell(1);
        try {
            String message = messageCell.text().trim();
            message = GLOBALS.substitute(message);
            messageCell.body(getFormatter().gray(message));
        } catch (RuntimeException e) {
            getFormatter().exception(messageCell, e);
        } finally {
            debugMethodCallEnd();
        }
    }

    /**
     * Evaluates a string using the internal JavaScript engine. Result of the
     * last evaluation is set in the lastEvaluation field.
     * 
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void evalJs() {
        CellWrapper jsCell = row.getCell(1);
        if (jsCell == null) {
            getFormatter().exception(row.getCell(0), "Missing string to evaluate)");
            return;
        }
        JavascriptWrapper wrapper = new JavascriptWrapper();
        Object result = null;
        try {
            result = wrapper.evaluateExpression(lastResponse, jsCell.body());
        } catch (JavascriptException e) {
            getFormatter().exception(row.getCell(1), e);
            return;
        }
        lastEvaluation = null;
        if (result != null) {
            lastEvaluation = result.toString();
        }
        StringTypeAdapter adapter = new StringTypeAdapter();
        adapter.set(lastEvaluation);
        getFormatter().right(row.getCell(1), adapter);
    }

    /**
     * Process the row in input. Abstracts the test runner via the wrapper
     * interfaces.
     * 
     * @param currentRow
     */
    @SuppressWarnings("rawtypes")
    public void processRow(RowWrapper<?> currentRow) {
        row = currentRow;
        CellWrapper cell0 = row.getCell(0);
        if (cell0 == null) {
            throw new RuntimeException("Current RestFixture row is not parseable (maybe empty or not existent)");
        }
        String methodName = cell0.text();
        if ("".equals(methodName)) {
            throw new RuntimeException("RestFixture method not specified");
        }
        Method method1 = null;
        try {
            method1 = getClass().getMethod(methodName);
            method1.invoke(this);
        } catch (SecurityException e) {
            throw new RuntimeException("Not enough permissions to access method " + methodName + " for this class "
                    + this.getClass().getSimpleName(), e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(
                    "Class " + this.getClass().getName() + " doesn't have a callable method named " + methodName,
                    e);
        } catch (IllegalArgumentException e) {
            throw new RuntimeException("Method named " + methodName + " invoked with the wrong argument.", e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Method named " + methodName + " is not public.", e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Method named " + methodName + " threw an exception when executing.", e);
        }
    }

    protected void initialize(Runner runner) {
        boolean state = validateState();
        notifyInvalidState(state);
        configFormatter(runner);
        configFixture();
        configRestClient();
    }

    private String emptifyBody(String b) {
        String body = b;
        if (body == null) {
            body = "";
        }
        return body;
    }

    public Map<String, String> getHeaders() {
        Map<String, String> headers = null;
        if (requestHeaders != null) {
            headers = requestHeaders;
        } else {
            headers = defaultHeaders;
        }
        return headers;
    }

    private void doMethod(String m) {
        doMethod(null, m);
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected void doMethod(String body, String method) {
        CellWrapper urlCell = row.getCell(1);
        String url = urlCell.text();
        String resUrl = GLOBALS.substitute(url);
        setLastRequest(partsFactory.buildRestRequest());
        getLastRequest().setMethod(RestRequest.Method.valueOf(method));
        getLastRequest().addHeaders(getHeaders());
        if (fileName != null) {
            getLastRequest().setFileName(fileName);
        }
        if (multipartFileName != null) {
            getLastRequest().setMultipartFileName(multipartFileName);
        }
        getLastRequest().setMultipartFileParameterName(multipartFileParameterName);
        String[] uri = resUrl.split("\\?");
        getLastRequest().setResource(uri[0]);
        if (uri.length == 2) {
            getLastRequest().setQuery(uri[1]);
        }
        if ("Post".equals(method) || "Put".equals(method)) {
            String rBody = GLOBALS.substitute(body);
            getLastRequest().setBody(rBody);
        }
        try {
            RestResponse response = restClient.execute(getLastRequest());
            setLastResponse(response);
            completeHttpMethodExecution();
        } catch (RuntimeException e) {
            getFormatter().exception(row.getCell(0),
                    "Execution of " + method + " caused exception '" + e.getMessage() + "'");
        }
    }

    private ContentType getContentTypeOfLastResponse() {
        return ContentType.parse(getLastResponse().getHeader("Content-Type"));
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected void completeHttpMethodExecution() {
        String uri = getLastResponse().getResource();
        String query = getLastRequest().getQuery();
        if (query != null && !"".equals(query.trim())) {
            uri = uri + "?" + query;
        }
        String u = restClient.getBaseUrl() + uri;
        CellWrapper uriCell = row.getCell(1);
        getFormatter().asLink(uriCell, u, uri);
        CellWrapper cellStatusCode = row.getCell(2);
        if (cellStatusCode == null) {
            throw new IllegalStateException("You must specify a status code cell");
        }
        Integer lastStatusCode = getLastResponse().getStatusCode();
        process(cellStatusCode, lastStatusCode.toString(), new StatusCodeTypeAdapter());
        List<Header> lastHeaders = getLastResponse().getHeaders();
        process(row.getCell(3), lastHeaders, new HeadersTypeAdapter());
        CellWrapper bodyCell = row.getCell(4);
        if (bodyCell == null) {
            throw new IllegalStateException("You must specify a body cell");
        }
        bodyCell.body(GLOBALS.substitute(bodyCell.body()));
        ContentType ct = getContentTypeOfLastResponse();
        BodyTypeAdapter bodyTypeAdapter = partsFactory.buildBodyTypeAdapter(ct);
        bodyTypeAdapter.setContext(namespaceContext);
        process(bodyCell, getLastResponse().getBody(), bodyTypeAdapter);
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void process(CellWrapper expected, Object actual, RestDataTypeAdapter ta) {
        if (expected == null) {
            throw new IllegalStateException("You must specify a headers cell");
        }
        ta.set(actual);
        boolean ignore = "".equals(expected.text().trim());
        if (ignore) {
            String actualString = ta.toString();
            if (!"".equals(actualString)) {
                expected.addToBody(getFormatter().gray(actualString));
            }
        } else {
            boolean success = false;
            try {
                String substitute = GLOBALS.substitute(Tools.fromHtml(expected.text()));
                Object parse = ta.parse(substitute);
                success = ta.equals(parse, actual);
            } catch (Exception e) {
                getFormatter().exception(expected, e);
                return;
            }
            if (success) {
                getFormatter().right(expected, ta);
            } else {
                getFormatter().wrong(expected, ta);
            }
        }
    }

    private void debugMethodCallStart() {
        debugMethodCall("=> ");
    }

    private void debugMethodCallEnd() {
        debugMethodCall("<= ");
    }

    private void debugMethodCall(String h) {
        if (debugMethodCall) {
            StackTraceElement el = Thread.currentThread().getStackTrace()[4];
            LOG.debug(h + el.getMethodName());
        }
    }

    protected RestResponse getLastResponse() {
        return lastResponse;
    }

    protected RestRequest getLastRequest() {
        return lastRequest;
    }

    private void setLastResponse(RestResponse lastResponse) {
        this.lastResponse = lastResponse;
    }

    private void setLastRequest(RestRequest lastRequest) {
        this.lastRequest = lastRequest;
    }

    private Map<String, String> parseHeaders(String str) {
        return Tools.convertStringToMap(str, ":", LINE_SEPARATOR);
    }

    private Map<String, String> parseNamespaceContext(String str) {
        return Tools.convertStringToMap(str, "=", LINE_SEPARATOR);
    }

    private String stripTag(String somethingWithinATag) {
        return Tools.fromSimpleTag(somethingWithinATag);
    }

    private void configFormatter(Runner runner) {
        formatter = partsFactory.buildCellFormatter(runner);
    }

    /**
     * Configure the fixture with data from {@link RestFixtureConfig}.
     */
    private void configFixture() {

        GLOBALS = new Variables(config);

        displayActualOnRight = config.getAsBoolean("restfixture.display.actual.on.right", displayActualOnRight);

        minLenForCollapseToggle = config.getAsInteger("restfixture.display.toggle.for.cells.larger.than",
                minLenForCollapseToggle);

        String str = config.get("restfixture.default.headers", "");
        defaultHeaders = parseHeaders(str);

        str = config.get("restfixture.xml.namespace.context", "");
        namespaceContext = parseNamespaceContext(str);

        ContentType.resetDefaultMapping();
        ContentType.config(config);
    }

    /**
     * Allows to config the rest client implementation. the method shoudl
     * configure the instance attribute {@link RestFixture#restClient} created
     * by the {@link RestFixture#buildRestClient()}.
     */
    private void configRestClient() {
        restClient = partsFactory.buildRestClient(getConfig());
        if (baseUrl != null) {
            restClient.setBaseUrl(baseUrl.toString());
        }
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void renderReplacement(CellWrapper cell, String actual) {
        StringTypeAdapter adapter = new StringTypeAdapter();
        adapter.set(actual);
        if (!adapter.equals(actual, cell.body())) {
            // eg - a substitution has occurred
            getFormatter().right(cell, adapter);
        }
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void processSlimRow(List<List<String>> resultTable, List<String> row) {
        RowWrapper currentRow = new SlimRow(row);
        try {
            processRow(currentRow);
        } catch (Exception e) {
            LOG.error("Exception raised when executing method " + row.get(0));
            getFormatter().exception(currentRow.getCell(0), e);
        } finally {
            List<String> rowAsList = mapSlimRow(row, currentRow);
            resultTable.add(rowAsList);
        }
    }

    @SuppressWarnings("rawtypes")
    private List<String> mapSlimRow(List<String> resultRow, RowWrapper currentRow) {
        List<String> rowAsList = ((SlimRow) currentRow).asList();
        for (int c = 0; c < rowAsList.size(); c++) {
            // HACK: it seems that even if the content is unchanged,
            // Slim renders red cell
            String v = rowAsList.get(c);
            if (v.equals(resultRow.get(c))) {
                rowAsList.set(c, "");
            }
        }
        return rowAsList;
    }
}