package com.sas.unravl; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.sas.unravl.assertions.UnRAVLAssertion; import com.sas.unravl.assertions.UnRAVLAssertion.Stage; import com.sas.unravl.extractors.UnRAVLExtractor; import com.sas.unravl.util.Json; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import javax.script.ScriptEngine; import javax.script.ScriptException; import org.apache.http.Header; import org.apache.http.message.BasicHeader; import org.apache.log4j.Logger; /** * An UnRAVL script object - this is a wrapper around a JSON UnRAVL script. An * UnRAVL script (Uniform REST API Validation Language) is an executable domain * specific language for validating REST APIs. An UNRAVL script consists of an * execution environment (bindings of name/value pairs), an HTTP method, URI, * and HTTP headers. The REST API method is called, then values may be extracted * from the results and assertions run to validate the call. * <p> * An UnRAVL script runs within an {@link UnRAVLRuntime runtime environment} * which defines the set of possible value bindings (called @link * UnRAVLExtractor s) and the set of possible {@link UnRAVLAssertion}s. * <p> * To execute an UnRAVL script, create an UnRAVLRuntime and invoke one of its * execute methods. * <p> * This class only executes a single UnRAVL test, represented by a JSON object. * It does not execute a JSON array of scripts; use UnRAVLRuntime * * TODO: This class is too large and does too much. Refactor. * * @author David.Biesack@sas.com */ public class UnRAVL { private static final String IMPLICIT_TEMPLATE = "implicit.template"; private static final String TEMPLATE_KEY = "template"; private static final String NAME_KEY = "name"; private static final String TEMPLATE_EXTENSION = ".template"; private static final String TEXT_MEDIA_TYPES_REGEX = "^(text/.*|.*/.*(xml|json)).*$"; private static final String JSON_MEDIA_TYPES_REGEX = "^.*(\\.|\\+)*json.*$"; public static final String REDIRECT_PREFIX = "@"; private UnRAVLRuntime runtime; private ObjectNode root; private String name; private UnRAVL template; private List<Header> requestHeaders; private Method method; private String uri; private List<UnRAVLExtractor> extractors; static Logger logger = Logger.getLogger(UnRAVL.class); public UnRAVL(UnRAVLRuntime runtime) { this.runtime = runtime; } /** * Create an instance * * @param runtime * the runtime environment; this may not be null * @param script * The UnRAVL test object. To run an ArrayNode, use the execute * method in UnRAVLRuntime * @throws JsonProcessingException * if the script cannot be parsed * @throws IllegalArgumentException * if the arguments are null or if the node is not an ObjectNode * @throws UnRAVLException * if the script is invalid */ public UnRAVL(UnRAVLRuntime runtime, ObjectNode script) throws JsonProcessingException, IOException, UnRAVLException { // TODO: create a new transient env for this test? // this.env = new Binding(env.getVariables()); // Unfortunately, unlike java.util.Properties, // Binding does not support dynamic parent bindings if (script == null || runtime == null) throw new IllegalArgumentException( "the runtime and script objects may not be null."); this.runtime = runtime; this.root = script; initialize(); } @Override public String toString() { return "UnRAVL:[" + getName() + " " + safe(getMethod(), "<no method>") + " " + safe(getURI(), "<no url>") + "]"; } private String safe(Object method2, String string) { return (method2 == null) ? string : method2.toString(); } public UnRAVLRuntime getRuntime() { return runtime; } // public Map<String, Object> getEnv() { // return runtime.getBindings(); // } public JsonNode getRoot() { return root; } public String getName() { return name; } public void setName() { JsonNode nameNode = root.get(NAME_KEY); if (nameNode != null) { name = nameNode.textValue(); if (name.endsWith(TEMPLATE_EXTENSION)) runtime.setTemplate(name, this); } else name = new Date().toString(); if (isRunnable()) runtime.getScripts().put(name, this); return; } public UnRAVL getTemplate() { if (template == null && !IMPLICIT_TEMPLATE.equals(getName()) // avoid // infinite // recursion // where // implicit.template // gets // itself && runtime.getTemplate(IMPLICIT_TEMPLATE) != null) { return runtime.getTemplate(IMPLICIT_TEMPLATE); } return template; } private void setTemplate() throws UnRAVLException { JsonNode tempNode = root.get(TEMPLATE_KEY); if (tempNode != null) { if (tempNode.isArray()) throw new UnRAVLException( "array template values are not yet supported."); if (!tempNode.isTextual()) throw new UnRAVLException("template value must be a text node."); String templateName = expand(tempNode.textValue()); if (!templateName.endsWith(TEMPLATE_EXTENSION)) templateName += TEMPLATE_EXTENSION; template = runtime.getTemplate(templateName); if (template == null) throw new UnRAVLException("No such template " + templateName); } } public List<Header> getRequestHeaders() { return requestHeaders; } public Method getMethod() { return method; } public String getURI() { return uri; } public void setURI(String uri) { this.uri = uri; } public List<UnRAVLExtractor> getExtractors() { return extractors; } private void initialize() throws UnRAVLException, IOException { setName(); setTemplate(); defineAPICall(); } private void defineAPICall() throws UnRAVLException, IOException { defineAPICall(this); defineHeaders(); } private void defineAPICall(UnRAVL script) throws UnRAVLException, IOException { if (script == null) return; if (script.method != null) { this.method = script.method; this.uri = script.uri; return; } // Look for a "GET", "HEAD" or other method name in this script for (Map.Entry<String, JsonNode> f : Json.fields(script.root)) { String methodName = f.getKey().toUpperCase(); Method m = httpMethod(methodName); if (m == null) continue; JsonNode node = f.getValue(); if (node == null) node = script.root.get(m.toString().toLowerCase()); if (node != null) { if (method != null) { logger.warn(String .format("Warning: HTTP method %s found but method already defined as %s %s", m, method, uri)); } method = m; if (!node.isTextual()) throw new UnRAVLException( String.format( "URI for method %s must be a string; found %s instead.", m, node)); uri = node.textValue(); } } if (method == null) defineAPICall(script.getTemplate()); } private static Method httpMethod(String methodName) { for (Method m : Method.values()) { if (m.name().equals(methodName)) { return m; } } return null; } private void defineHeaders() throws UnRAVLException { ArrayList<Header> headers = new ArrayList<Header>(); defineHeaders(this, headers); this.requestHeaders = headers; } public void addRequestHeader(Header header) { requestHeaders.add(header); } private void defineHeaders(UnRAVL from, List<Header> headers) throws UnRAVLException { if (from == null) return; UnRAVL template = from.getTemplate(); defineHeaders(template, headers); JsonNode headersNode = from.getRoot().get("headers"); if (headersNode == null) return; for (Map.Entry<String, JsonNode> h : Json.fields(headersNode)) { String string = h.getKey(); // Do not expand headers here; do so in // ApiCall.mapHeaders(List<Header> requestHeaders) // This method is called before "env" is evaluated, // so expansion here is premature. String val = h.getValue().asText(); BasicHeader header = new BasicHeader(string, val); headers.add(header); } } public ApiCall run() throws UnRAVLException, IOException { ApiCall apiCall = new ApiCall(this); try { return apiCall.run(); } finally { apiCall.report(System.out); } } /** Stop execution. */ public void cancel() { getRuntime().cancel(); } /** * Bind a value within this script's environment. This will add a new * binding if <var>varName</var> is not yet bound, or replace the old * binding. * * @param varName * the variable name * @param value * the variable value * @return this script, which allows chaining bind calls. * @see UnRAVLRuntime#bind(String, Object) */ public UnRAVL bind(String varName, Object value) { getRuntime().bind(varName, value); return this; } /** * Return the value bound to a variable in this script's environment * * @param varName * the variable name * @return the value bound to the variable * @see UnRAVLRuntime#binding(String) */ public Object binding(String varName) { return getRuntime().binding(varName); } /** * Test if the value bound in this script's environment * * @param varName * the variable name * @return true iff the variable is bound * @see UnRAVLRuntime#bound(String) */ public boolean bound(String varName) { return getRuntime().bound(varName); } public boolean bodyIsTextual(Header headers[]) { return headersMatchPattern(headers, TEXT_MEDIA_TYPES_REGEX); } public boolean bodyIsJson(Header headers[]) { return headersMatchPattern(headers, JSON_MEDIA_TYPES_REGEX); } private boolean headersMatchPattern(Header headers[], String pattern) { for (Header h : headers) if (h.getValue().matches(pattern)) return true; return false; } public static ObjectNode statusAssertion(UnRAVL script) throws UnRAVLException { if (script == null) return null; // really need to use JSONPath ... JsonNode node = script.root.get("assert"); node = ApiCall.assertionArray(node, Stage.ASSERT); if (node != null) { for (JsonNode n : Json.array(node)) { if (n.isObject() && Json.firstFieldName(n).equals("status")) return Json.object(n); } } return statusAssertion(script.getTemplate()); } public boolean isRunnable() { return name != null && !name.endsWith(TEMPLATE_EXTENSION); } public String expand(String textValue) { return getRuntime().expand(textValue); } public Object eval(String expression) throws UnRAVLException { return evalWith(expression, getRuntime().getScriptLanguage()); } public Object evalWith(String expression, String lang) throws UnRAVLException { ScriptEngine engine = getRuntime().interpreter(lang); try { Object result = engine.eval(expression, engine.getContext()); return result; } catch (ScriptException e) { logger.error("script '" + expression + "' threw a runtime script exception " + e.getClass().getName() + ", " + e.getMessage()); throw new UnRAVLException(e.getMessage(), e); } } }