/* 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import smartrics.rest.client.RestClient;
import smartrics.rest.client.RestData.Header;
import smartrics.rest.client.RestRequest;
import smartrics.rest.client.RestResponse;
import smartrics.rest.fitnesse.fixture.support.*;
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;
/**
* 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 configuration)</i></td>
* </tr>
* <tr>
* <td>http.proxy.port</td>
* <td><i>http proxy host port (RestClient proxy configuration)</i></td>
* </tr>
* <tr>
* <td>http.basicauth.username</td>
* <td><i>username for basic authentication (RestClient proxy configuration)</i>
* </td>
* </tr>
* <tr>
* <td>http.basicauth.password</td>
* <td><i>password for basic authentication (RestClient proxy configuration)</i>
* </td>
* </tr>
* <tr>
* <td>http.client.connection.timeout</td>
* <td><i>client timeout for http connection (default 3s). (RestClient proxy
* configuration)</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.requests.follow.redirects</td>
* <td><i>If set to true the underlying client is instructed to follow redirects
* for the requests in the current fixture. This setting is not applied to POST
* and PUT (for which redirection is set to false) Defaults to true.</i></td>
* </tr>
* <tr>
* <td>restfixture.resource.uris.are.escaped</td>
* <td><i>boolean value. if true, RestFixture will assume that the resource uris
* are already escaped. If false, resource uri will be escaped. 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.default.charset</td>
* <td>The default charset name (e.g. UTF-8) to use when parsing the response
* body, when a response doesn't contain a valid value in the Content-Type
* header. If a default is not specified with this property, the fixture will
* use the default system charset, available via
* <code>Charset.defaultCharset().name()</code></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 {
/**
* What runner this table is running on.
*
* Note, the OTHER runner is primarily for testing purposes.
*
* @author smartrics
*
*/
public enum Runner {
/**
* the slim runner
*/
SLIM,
/**
* the fit runner
*/
FIT,
/**
* any other runner
*/
OTHER;
};
private static final String LINE_SEPARATOR = "\n";
private static final String FILE = "file";
private static final Logger LOG = LoggerFactory.getLogger(RestFixture.class);
protected Variables GLOBALS;
private RestResponse lastResponse;
private RestRequest lastRequest;
protected String fileName = null;
protected String multipartFileName = null;
protected String multipartFileParameterName = FILE;
protected String requestBody;
protected boolean resourceUrisAreEscaped = false;
protected 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;
private boolean followRedirects = true;
/**
* Constructor for Fit runner.
*/
public RestFixture() {
super();
this.partsFactory = new PartsFactory();
this.displayActualOnRight = true;
this.minLenForCollapseToggle = -1;
this.resourceUrisAreEscaped = false;
}
/**
* Constructor for Slim runner.
*
* @param hostName
* 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 hostName
* the cells following up the first cell in the first row.
* @param configName
* the value of cell number 3 in first row of the fixture table.
*/
public RestFixture(String hostName, String configName) {
this(new PartsFactory(), hostName, configName);
}
/**
* @param partsFactory
* the factory of parts necessary to create the rest fixture
* @param hostName
* @param configName
*/
public 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;
}
/**
* sets the base url.
*
* @param url
*/
public void setBaseUrl(Url url) {
this.baseUrl = url;
}
/**
* 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 the formatter for the cells
*/
public CellFormatter<?> getFormatter() {
return formatter;
}
/**
* Slim Table table hook.
*
* @param rows
* @return the rendered content.
*/
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;
}
/**
* 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);
}
}
/**
* @return the multipart filename
*/
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);
}
}
/**
* @return the 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);
}
}
/**
* @return the multipart file parameter name.
*/
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 substitutedHeaders = GLOBALS.substitute(cell.text());
requestHeaders = parseHeaders(substitutedHeaders);
cell.body(getFormatter().gray(substitutedHeaders));
}
}
/**
* 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> | HEAD | uri | ?ret | ?headers | |</code>
* <p/>
* executes a HEAD on the uri and checks the return (a string repr the
* operation return code) and the http response headers. Head is meant to
* return no-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 HEAD() {
debugMethodCallStart();
doMethod("Head");
debugMethodCallEnd();
}
/**
* <code> | OPTIONS | uri | ?ret | ?headers | ?body |</code>
* <p/>
* executes a OPTIONS on the uri and checks the return (a string repr the
* operation return code), the http response headers, 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 OPTIONS() {
debugMethodCallStart();
doMethod("Options");
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> | TRACE | uri | ?ret | ?headers | ?body |</code>
*/
public void TRACE() {
debugMethodCallStart();
doMethod("Trace");
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();
if(row.size() != 5) {
getFormatter().exception(row.getCell(row.size() - 1), "Not all cells found: | let | label | type | expr | result |");
debugMethodCallEnd();
return;
}
String label = row.getCell(1).text().trim();
String loc = row.getCell(2).text();
CellWrapper exprCell = row.getCell(3);
try {
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;
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());
LOG.error("Exception occurred when processing cell=" + exprCell, e);
}
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();
}
}
/**
* allows to add comments to a rest fixture - basically does nothing except ignoring the text.
* the text is substituted if variables are found.
*/
@SuppressWarnings("unchecked")
public void comment() {
debugMethodCallStart();
@SuppressWarnings("rawtypes")
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 attribute lastEvaluation.
*
*/
@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();
}
protected String emptifyBody(String b) {
String body = b;
if (body == null) {
body = "";
}
return body;
}
/**
* @return the request headers
*/
public Map<String, String> getHeaders() {
Map<String, String> headers = null;
if (requestHeaders != null) {
headers = requestHeaders;
} else {
headers = defaultHeaders;
}
return headers;
}
// added for RestScriptFixture
protected String getRequestBody() {
return requestBody;
}
// added for RestScriptFixture
protected void setRequestBody(String text) {
requestBody = text;
}
protected Map<String, String> getNamespaceContext() {
return namespaceContext;
}
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 = deHtmlify(stripTag(urlCell.text()));
String resUrl = GLOBALS.substitute(url);
String rBody = GLOBALS.substitute(body);
Map<String, String> rHeaders = substitute(getHeaders());
try {
doMethod(method, resUrl, rHeaders, rBody);
completeHttpMethodExecution();
} catch (RuntimeException e) {
getFormatter().exception(
row.getCell(0),
"Execution of " + method + " caused exception '"
+ e.getMessage() + "'");
LOG.error("Exception occurred when processing method=" + method, e);
}
}
protected void doMethod(String method, String resUrl, String rBody) {
doMethod(method, resUrl, substitute(getHeaders()), rBody);
}
protected void doMethod(String method, String resUrl,
Map<String, String> headers, String rBody) {
setLastRequest(partsFactory.buildRestRequest());
getLastRequest().setMethod(RestRequest.Method.valueOf(method));
getLastRequest().addHeaders(headers);
getLastRequest().setFollowRedirect(followRedirects);
getLastRequest().setResourceUriEscaped(resourceUrisAreEscaped);
if (fileName != null) {
getLastRequest().setFileName(fileName);
}
if (multipartFileName != null) {
getLastRequest().setMultipartFileName(multipartFileName);
}
getLastRequest().setMultipartFileParameterName(
multipartFileParameterName);
String[] uri = resUrl.split("\\?");
String[] thisRequestUrlParts = buildThisRequestUrl(uri[0]);
getLastRequest().setResource(thisRequestUrlParts[1]);
if (uri.length == 2) {
getLastRequest().setQuery(uri[1]);
}
if ("Post".equals(method) || "Put".equals(method)) {
getLastRequest().setBody(rBody);
}
restClient.setBaseUrl(thisRequestUrlParts[0]);
RestResponse response = restClient.execute(getLastRequest());
setLastResponse(response);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
protected void completeHttpMethodExecution() {
String uri = getLastResponse().getResource();
String query = getLastRequest().getQuery();
if (query != null && !"".equals(query.trim())) {
uri = uri + "?" + query;
}
String clientBaseUri = restClient.getBaseUrl();
String u = clientBaseUri + 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()));
BodyTypeAdapter bodyTypeAdapter = createBodyTypeAdapter();
process(bodyCell, getLastResponse().getBody(), bodyTypeAdapter);
}
// Split out of completeHttpMethodExecution so RestScriptFixture can call
// this
protected BodyTypeAdapter createBodyTypeAdapter() {
return createBodyTypeAdapter(ContentType.parse(getLastResponse()
.getContentType()));
}
// Split out of completeHttpMethodExecution so RestScriptFixture can call
// this
protected BodyTypeAdapter createBodyTypeAdapter(ContentType ct) {
String charset = getLastResponse().getCharset();
BodyTypeAdapter bodyTypeAdapter = partsFactory.buildBodyTypeAdapter(ct,
charset);
bodyTypeAdapter.setContext(namespaceContext);
return 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());
}
}
private Map<String, String> substitute(Map<String, String> headers) {
Map<String, String> sub = new HashMap<String, String>();
for (Map.Entry<String, String> e : headers.entrySet()) {
sub.put(e.getKey(), GLOBALS.substitute(e.getValue()));
}
return sub;
}
protected RestResponse getLastResponse() {
return lastResponse;
}
protected RestRequest getLastRequest() {
return lastRequest;
}
private String[] buildThisRequestUrl(String uri) {
String[] parts = new String[2];
if (baseUrl == null || uri.startsWith(baseUrl.toString())) {
Url url = new Url(uri);
parts[0] = url.getBaseUrl();
parts[1] = url.getResource();
} else {
try {
Url attempted = new Url(uri);
parts[0] = attempted.getBaseUrl();
parts[1] = attempted.getResource();
} catch (RuntimeException e) {
parts[0] = baseUrl.toString();
parts[1] = uri;
}
}
return parts;
}
private void setLastResponse(RestResponse lastResponse) {
this.lastResponse = lastResponse;
}
private void setLastRequest(RestRequest lastRequest) {
this.lastRequest = lastRequest;
}
protected Map<String, String> parseHeaders(String str) {
return Tools.convertStringToMap(str, ":", LINE_SEPARATOR, true);
}
private Map<String, String> parseNamespaceContext(String str) {
return Tools.convertStringToMap(str, "=", LINE_SEPARATOR, true);
}
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);
resourceUrisAreEscaped = config
.getAsBoolean("restfixture.resource.uris.are.escaped",
resourceUrisAreEscaped);
followRedirects = config.getAsBoolean(
"restfixture.requests.follow.redirects", followRedirects);
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 smartrics.rest.fitnesse.fixture.PartsFactory#buildRestClient(smartrics.rest.fitnesse.fixture.support.Config)}.
*/
private void configRestClient() {
restClient = partsFactory.buildRestClient(getConfig());
}
@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
cell.body(actual);
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 processing row " + row.get(0), e);
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;
}
private String deHtmlify(String someHtml) {
return Tools.fromHtml(someHtml);
}
}