package org.nate;
import static org.junit.Assert.assertThat;
import static org.nate.testutil.WhiteSpaceIgnoringXmlMatcher.matchesXmlIgnoringWhiteSpace;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import org.jruby.RubyArray;
import org.jruby.RubyHash;
import cuke4duke.annotation.I18n.EN.Given;
import cuke4duke.annotation.I18n.EN.Then;
import cuke4duke.annotation.I18n.EN.When;
public class NateSteps {
private List<Engine> nateStates = new ArrayList<Engine>();
static {
// Needed to allow us to evaluate Ruby code without interfering with Cucumber!
System.setProperty("org.jruby.embed.localcontext.scope", "singlethread");
}
@Given("^the HTML fragment \"([^\"]*)\"$")
public void setHtmlFragment(String html) {
Engine nate = Nate.newWith(html, Nate.encoders().encoderFor("XMLF"));
nateStates.add(nate);
}
@Given("^the HTML document \"([^\"]*)\"$")
public void setHtml(String html) {
Engine nate = Nate.newWith(html, Nate.encoders().encoderFor("XML"));
nateStates.add(nate);
}
@Given("^the file \"([^\"]*)\"$")
public void theFile(String filename) {
Engine nate = Nate.newWith(new File(filename));
nateStates.add(nate);
}
@When("^([^\"]*) is injected$")
public void inject(String data) throws Exception {
nateStates.add(currentNateEngine().inject(parseRubyExpression(data)));
}
@Then("^the HTML fragment is (.*)$")
public void test(String expectedHtml) throws Exception {
assertThat(currentNateEngine().render(), matchesXmlIgnoringWhiteSpace(expectedHtml));
}
@When("^(.*) is injected sometime later$")
public void isInjectedSometimeLater(String data) throws Exception {
inject(data);
}
@Then("^the original HTML fragment is(.*)$")
public void theOriginalHTMLFragmentIs(String expectedHtml) throws Exception {
assertThat(nateStates.get(0).render(), matchesXmlIgnoringWhiteSpace(expectedHtml));
}
@When("^\"([^\"]*)\" is selected$")
public void isSelected(String selector) {
nateStates.add(currentNateEngine().select(selector));
}
// This method is needed because the features express the data used to fill in the templates using ruby syntax like:
// { 'h2' => 'Monkey' }
private Object parseRubyExpression(String rubyString) throws ScriptException {
ScriptEngine rubyEngine = new ScriptEngineManager().getEngineByName("jruby");
defineRubyConstantsAndMethods(rubyEngine);
Object result = rubyEngine.eval(rubyString, new SimpleScriptContext());
return convertToOrdinaryJavaClasses(result);
}
private void defineRubyConstantsAndMethods(ScriptEngine rubyEngine) throws ScriptException {
// This is so that the features can use Nate::Engine::CONTENT_ATTRIBUTE
rubyEngine.eval(
"require 'java'\n" +
"module Nate\n class Engine\n" +
" CONTENT_ATTRIBUTE = '*content*'\n" +
" def self.from_string source\n" +
" org.nate.Nate.newWith(source)\n" +
" end\n" +
" end\n end\n");
}
// Would actually not need to do this, except that RubyHash and RubyArray seem to have been loaded
// in a different class loader (at least I think that is the case!)!!!!
private Object convertToOrdinaryJavaClasses(Object rubyObject) {
if (rubyObject == null || isAnAcceptableJavaType(rubyObject)) {
return rubyObject;
}
if (rubyObject instanceof RubyHash) {
return convertToJavaMap((RubyHash) rubyObject);
}
if (rubyObject instanceof RubyArray) {
return convertToJavaList((RubyArray) rubyObject);
}
throw new IllegalStateException("Cannot handle " + rubyObject.getClass());
}
@SuppressWarnings("unchecked")
private List convertToJavaList(RubyArray rubyArray) {
List result = new ArrayList();
for (Object object : rubyArray) {
result.add(convertToOrdinaryJavaClasses(object));
}
return result;
}
private boolean isAnAcceptableJavaType(Object rubyObject) {
return rubyObject instanceof String || rubyObject instanceof Number || rubyObject instanceof Engine;
}
private Map<String, Object> convertToJavaMap(RubyHash hash) {
Map<String, Object> result = new HashMap<String, Object>();
for (Object key : hash.keySet()) {
if (!(key instanceof String)) {
throw new IllegalStateException("Keys must be strings, but got:" + key.getClass());
}
result.put((String) key, convertToOrdinaryJavaClasses(hash.get(key)));
}
return result;
}
private Engine currentNateEngine() {
return nateStates.get(nateStates.size() - 1);
}
}