package com.sas.unravl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.json.JsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import com.sas.unravl.annotations.UnRAVLAssertionPlugin;
import com.sas.unravl.annotations.UnRAVLExtractorPlugin;
import com.sas.unravl.assertions.UnRAVLAssertionException;
import com.sas.unravl.generators.UnRAVLRequestBodyGenerator;
import com.sas.unravl.util.Json;
import com.sas.unravl.util.VariableResolver;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.SimpleBindings;
import org.apache.log4j.Logger;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;
/**
* The runtime environment for running UnRAVL scripts. The runtime contains the
* bindings which are accessed and set by scripts, and provides environment
* expansion of strings. The runtime also contains the global mappings of
* assertions, extractors, and request body generators, and a map of scripts and
* templates
*
* @author DavidBiesack@sas.com
*/
@Component
public class UnRAVLRuntime implements Cloneable {
/**
* Prefix for property names when firing a PropertyChangeEvent when a
* variable is changed via {@link #bind(String, Object)}
*/
public static final String ENV_PROPERTY_CHANGE_PREFIX = "env.";
private static final Logger logger = Logger.getLogger(UnRAVLRuntime.class);
private Map<String, Object> env; // script variables
private Map<String, UnRAVL> scripts = new LinkedHashMap<String, UnRAVL>();
private Map<String, UnRAVL> templates = new LinkedHashMap<String, UnRAVL>();
// a history of the API calls we've made in this runtime
private ArrayList<ApiCall> calls = new ArrayList<ApiCall>();
private int failedAssertionCount;
// used to expand variable references {varName} in strings:
private VariableResolver variableResolver;
private String scriptLanguage;
private boolean cancelled;
public UnRAVLRuntime() {
this(new LinkedHashMap<String, Object>());
}
/**
* Instantiate a new runtime with the given environment
*
* @param environment
* name/value bindings
*/
public UnRAVLRuntime(Map<String, Object> environment) {
configure();
this.env = environment;
setScriptLanguage(getPlugins().getScriptLanguage());
for (Map.Entry<Object, Object> e : System.getProperties().entrySet())
bind(e.getKey().toString(), e.getValue());
bind("failedAssertionCount", Integer.valueOf(0));
resetBindings();
}
/**
* Instantiate a new runtime with the environment of the input runtime
* instance. The environment is copied, but the new runtime gets its own
* empty list of calls, scripts, and templates.
*
* @param runtime
* an existing Runtime (may not be null)
*/
public UnRAVLRuntime(UnRAVLRuntime runtime) {
env = new LinkedHashMap<String, Object>();
env.putAll(runtime.env);
calls = new ArrayList<ApiCall>();
scripts = new LinkedHashMap<String, UnRAVL>();
cancelled = false;
variableResolver = new VariableResolver(env);
templates = new LinkedHashMap<String, UnRAVL>();
setScriptLanguage(runtime.getScriptLanguage());
}
/**
* @return this runtime's default script language
*/
public String getScriptLanguage() {
return scriptLanguage;
}
/**
* Set this runtime's default script language, used to evaluate "if"
* conditions, "links"/"hrefs" from expressions, and string assertions
*
* @param language
* the script language, such as "groovy" or "javascript"
*/
public void setScriptLanguage(String language) {
this.scriptLanguage = language;
}
/**
* Return a script engine that can evaluate (interpret) script strings. The
* returned engine is determined by the UnRAVLPlugins; the default is a
* Groovy engine if Groovy is available. The system property
* unravl.script.language may be set to a valid engine such as JavaScript;
* the default is "Groovy". Run with -Dunravl.script.language=
* <em>language</em> such as -Dunravl.script.language=JavaScript to choose
* an alternate language
*
* @return a script engine
* @throws UnRAVLException
* if no interpreter exists for the configured script language
*/
public ScriptEngine interpreter() throws UnRAVLException {
return interpreter(null);
}
/**
* Return a script engine that can evaluate (interpret) script strings using
* the named script language
*
* @param lang
* the script language, such as "groovy' or "javascript"
* @return a script interpreter
* @throws UnRAVLException
* is no engine exists for the script language <var>lang</var>
*/
public ScriptEngine interpreter(String lang) throws UnRAVLException {
ScriptEngine engine = getPlugins().interpreter(lang);
SimpleBindings bindings = new SimpleBindings(getBindings());
engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
return engine;
}
public Map<String, Object> getBindings() {
return env;
}
public int getFailedAssertionCount() {
return failedAssertionCount;
}
/**
* Reset the count of failed assertions. Do this if you wish to run new API
* calls with this runtime but previous calls have failed due to assertion
* failures
*/
public void resetFailedAssertionCount() {
failedAssertionCount = 0;
}
public void incrementFailedAssertionCount() {
failedAssertionCount++;
bind("failedAssertionCount", Integer.valueOf(failedAssertionCount));
}
public Map<String, UnRAVL> getScripts() {
return scripts;
}
private Map<String, UnRAVL> getTemplates() {
return templates;
}
private static ClassPathXmlApplicationContext ctx = null;
/**
* UnRAVL can be configured with Spring by loading the Spring config
* classpath:/META-INF/spring/unravlApplicationContext.xml . If that has not
* been done, this method initializes Spring. Spring performs a
* component-scan for assertions, body generators, and extractors. As each
* such component is loaded, it's runtime property is {@literal @} autowired
* to a UnRAVLRuntime instance this calls the register*() methods. See
* {@link UnRAVLRequestBodyGenerator}, {@link UnRAVLAssertionPlugin}, and
* {@link UnRAVLExtractorPlugin}.
*/
public static synchronized void configure() {
if (ctx != null)
return;
// Configure Spring. Works on Unix; fails on Windows?
String[] contextXml = new String[] { "/META-INF/spring/unravlApplicationContext.xml" };
ctx = new ClassPathXmlApplicationContext(contextXml);
assert (ctx != null);
// Configure jsonPath to use Jackson
Configuration.Defaults jsonPathConfig = new Configuration.Defaults() {
private final JsonProvider jsonProvider = new JacksonJsonProvider();
private final MappingProvider mappingProvider = new JacksonMappingProvider();
@Override
public JsonProvider jsonProvider() {
return jsonProvider;
}
@Override
public MappingProvider mappingProvider() {
return mappingProvider;
}
@Override
public Set<Option> options() {
return EnumSet.noneOf(Option.class);
}
};
Configuration.setDefaults(jsonPathConfig);
}
public UnRAVLRuntime execute(String[] argv) throws UnRAVLException {
// for now, assume each command line arg is an UnRAVL script
cancelled = false;
for (String scriptFile : argv) {
try {
List<JsonNode> roots = read(scriptFile);
if (isCanceled())
break;
execute(roots);
} catch (IOException e) {
logger.error(e.getMessage() + " while running UnRAVL script "
+ scriptFile);
throw new UnRAVLException(e);
} catch (UnRAVLException e) {
logger.error(e.getMessage() + " while running UnRAVL script "
+ scriptFile);
throw (e);
}
}
return this;
}
public void execute(JsonNode... roots) throws JsonProcessingException,
IOException, UnRAVLException {
execute(Arrays.asList(roots));
}
public void execute(List<JsonNode> listOfScripts)
throws JsonProcessingException, IOException, UnRAVLException {
cancelled = false;
executeInternal(listOfScripts);
}
public void executeInternal(List<JsonNode> listOfScripts)
throws JsonProcessingException, IOException, UnRAVLException {
for (int i = 0; !isCanceled() && i < listOfScripts.size(); i++) {
JsonNode root = listOfScripts.get(i);
executeInternal(root);
}
}
public void executeInternal(JsonNode root) throws JsonProcessingException,
IOException, UnRAVLException {
if (root.isTextual()) {
String ref = root.textValue();
if (ref.startsWith(UnRAVL.REDIRECT_PREFIX)) {
String sublist = expand(ref.substring(UnRAVL.REDIRECT_PREFIX
.length()));
executeInternal(read(sublist));
return;
}
} else if (root.isArray()) {
for (JsonNode node : Json.array(root))
executeInternal(node);
return;
}
String label = "";
try {
UnRAVL u = null;
if (root.isTextual()) {
String name = root.textValue();
label = name;
u = getScripts().get(name);
if (u == null) {
throw new UnRAVLException(String.format(
"No such UnRAVL script named '%s'", name));
}
} else
u = new UnRAVL(this, (ObjectNode) root);
label = u.getName();
u.run();
} catch (UnRAVLAssertionException e) {
logger.error(e.getMessage() + " while running UnRAVL script "
+ label);
incrementFailedAssertionCount();
} catch (RuntimeException rte) {
if (rte.getCause() instanceof UnRAVLException) { // tunneled
// exception
UnRAVLException e = (UnRAVLException) rte.getCause();
throw e;
} else
throw rte;
}
}
public UnRAVLRuntime execute(String scriptFile) throws UnRAVLException {
cancelled = false;
// for now, assume each command line arg is an UnRAVL script
try {
List<JsonNode> roots = read(scriptFile);
execute(roots);
} catch (IOException e) {
logger.error(e.getMessage());
throw new UnRAVLException(e);
} catch (UnRAVLException e) {
logger.error(e.getMessage());
throw (e);
}
return this;
}
public boolean isCanceled() {
return cancelled;
}
/** Stop execution. */
public void cancel() {
if (!cancelled) {
pcs.firePropertyChange("cancelled", Boolean.FALSE, Boolean.TRUE);
this.cancelled = true;
}
}
/**
* Expand environment variables in a string. For example, if string is
*
* <pre>
* "{time} is the time for {which} {who} to come to the aid of their {where}"
* </pre>
*
* and the environment contains the bindings
*
* <pre>
* time = "Mon, Aug 4, 2014"
* which = 16
* who = "hackers
* where = "API"
* </pre>
*
* the result will be
*
* <pre>
* "Mon, Aug 4, 2014 is the time for 16 hackers to come to the aid of their API"
* </pre>
*
* The toString() value of each binding in the environment is substituted.
* An optional notation is allowed to provide a default value if a variable
* is not bound. <code>{varName|alt text}</code> will resolve to the value
* of varName if it is bound, or to alt text if varName is not bound. The
* alt text may also contain embedded variable expansions.
*
* @param text
* an input string. May be null.
* @return the string, with environment variables replaced. Returns null if
* the input is null.
*/
public String expand(String text) {
if (text == null)
return null;
if (variableResolver == null) {
variableResolver = new VariableResolver(getBindings());
}
return variableResolver.expand(text);
}
/**
* Bind a value within this runtime's environment. This will add a new
* binding if <var>varName</var> is not yet bound, or replace the old
* binding.
* <p>
* THis also fires a <code>PropertyChangeEvent</code> to all listeners, with
* the property name being the <var>varName</var> with
* <code>{@link #ENV_PROPERTY_CHANGE_PREFIX}</code> prefixed. For example,
* on <code>bind("two", Integer.valueOf(2))</code>, this will fire an event
* with the property named <code>"env.two"</code>.
* </p>
*
* @see #unbind(String)
* @param varName
* variable name
* @param value
* variable value
* @return this runtime, which allows chaining bind calls.
*/
public UnRAVLRuntime bind(String varName, Object value) {
if (VariableResolver.isUnicodeCodePointName(varName)) {
UnRAVLException ue = new UnRAVLException(String.format(
"Cannot rebind special Unicode variable %s", varName));
throw new RuntimeException(ue);
}
Object oldValue = binding(varName);
env.put(varName, value);
pcs.firePropertyChange(ENV_PROPERTY_CHANGE_PREFIX + varName, oldValue,
value);
logger.trace("bind("
+ varName
+ ","
+ value
+ ")"
+ ((value instanceof String) ? "" : " "
+ (value == null ? "null" : value.getClass().getName())));
return this;
}
/**
* Remove a variable binding from this runtime. This undoes what {@link
* #bind(String,Object)} does
*
* @param varName
* the name of the variable to remove
* @see #bind(String,Object)
*/
public void unbind(String varName) {
env.remove(varName);
}
/**
* Return the value bound to a variable in this runtime's environment
*
* @param varName
* the variable name
* @return the value bound to the variable
*/
public Object binding(String varName) {
return env.get(varName);
}
/**
* Test if the value bound in this script's environment
*
* @param varName
* the variable name
* @return true iff the variable is bound
*/
public boolean bound(String varName) {
return env.containsKey(varName);
}
/**
* Call this when bindings have changed.
*
* @deprecated no longer needed. This method will be removed in 1.1.0
*/
@Deprecated
public void resetBindings() {
// null signals that we need to recreate the resolver after
// the bindings have changed.
// if null, variableResolver gets recreated if needed in expand(String).
//
// We no longer need to reset the resolver instance
// since we do not copy the environment.
// This used to do:
/* variableResolver = null; */
}
public List<JsonNode> read(String scriptFile)
throws JsonProcessingException, IOException, UnRAVLException {
JsonNode root;
List<JsonNode> roots = new ArrayList<JsonNode>();
ObjectMapper mapper = new ObjectMapper();
URL url = null;
try {
url = new URL(scriptFile);
} catch (MalformedURLException e) {
}
if (url != null)
root = mapper.readTree(url);
else {
File f = new File(scriptFile);
root = mapper.readTree(f);
}
if (root.isArray()) {
for (JsonNode next : Json.array(root)) {
roots.add(next);
}
} else {
roots.add(root);
}
return roots;
}
public int report() {
int failed = (calls.size() == 0 ? 1 : 0);
for (ApiCall call : calls) {
failed += call.getFailedAssertions().size();
}
if (cancelled)
System.out.println("UnRAVL script execution was canceled.");
return failed;
}
/**
* @return a list of the API calls
*/
public List<ApiCall> getApiCalls() {
return calls;
}
/**
* @return The size of this runtime, which is the number of API calls
*/
public int size() {
return calls.size();
}
public void addApiCall(ApiCall apiCall) {
calls.add(apiCall);
pcs.firePropertyChange("calls", null, calls);
}
public UnRAVLPlugins getPlugins() {
return ctx.getBean(UnRAVLPlugins.class);
}
public UnRAVL getTemplate(String templateName) {
return getTemplates().get(templateName);
}
public void setTemplate(String name, UnRAVL template) {
// TODO: check for template cycles
if (hasTemplate(name)) {
logger.warn("Replacing template " + name);
}
getTemplates().put(name, template);
}
public boolean hasTemplate(String name) {
return getTemplates().containsKey(name);
}
/**
* Reset this instance. This removes the history of calls, turns off the
* cancelled flag, and resets the assertion failure count to 0.
*/
public void reset() {
resetFailedAssertionCount();
calls.clear();
if (cancelled) {
cancelled = false;
pcs.firePropertyChange("cancelled", Boolean.TRUE, Boolean.FALSE);
}
pcs.firePropertyChange("calls", null, calls);
}
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener listener) {
this.pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
this.pcs.removePropertyChangeListener(listener);
}
}