From c53b821c85ba5ba115d8e6848c7ad881f64b0666 Mon Sep 17 00:00:00 2001 From: Kenji Nagahashi Date: Tue, 28 May 2013 13:28:37 -0700 Subject: [PATCH 1/3] FIX: 73f58dc56a broke XML variant of scripting console response, and will produce incompatible structure (extra nesting with scriptExec element). moved ScriptExecution code to ScriptModel, which is now a bean, not Map. XmlMarshaller is extended to support simple JavaBeans to XML marshalling, borrowing annotation classes from javax.xml. --- .../crawler/restlet/ScriptResource.java | 117 ++------------- .../crawler/restlet/XmlMarshaller.java | 97 +++++++++++++ .../crawler/restlet/models/ScriptModel.java | 135 +++++++++++++++--- .../org/archive/crawler/restlet/Script.ftl | 21 +-- 4 files changed, 236 insertions(+), 134 deletions(-) diff --git a/engine/src/main/java/org/archive/crawler/restlet/ScriptResource.java b/engine/src/main/java/org/archive/crawler/restlet/ScriptResource.java index 6d53f434..5f3da976 100644 --- a/engine/src/main/java/org/archive/crawler/restlet/ScriptResource.java +++ b/engine/src/main/java/org/archive/crawler/restlet/ScriptResource.java @@ -20,8 +20,6 @@ package org.archive.crawler.restlet; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.io.Writer; import java.util.Collection; import java.util.Collections; @@ -31,14 +29,11 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import javax.script.Bindings; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; -import javax.script.ScriptException; import org.apache.commons.lang.StringUtils; -import org.archive.crawler.framework.BeanLookupBindings; import org.archive.crawler.restlet.models.ScriptModel; import org.archive.crawler.restlet.models.ViewModel; import org.restlet.Context; @@ -52,7 +47,6 @@ import org.restlet.resource.Representation; import org.restlet.resource.ResourceException; import org.restlet.resource.Variant; import org.restlet.resource.WriterRepresentation; -import org.springframework.context.ApplicationContext; import freemarker.template.Configuration; import freemarker.template.ObjectWrapper; @@ -84,7 +78,6 @@ public class ScriptResource extends JobRelatedResource { } }); } - ScriptExecution scriptExec = null; protected String chosenEngine = FACTORIES.isEmpty() ? "" : FACTORIES.getFirst().getNames().get(0); private Configuration _templateConfiguration; @@ -106,92 +99,8 @@ public class ScriptResource extends JobRelatedResource { public Configuration getTemplateConfiguration(){ return _templateConfiguration; } - - /** - * JavaBean that packages script, its execution and the result. - * - * TODO: the script could create an object that persists and retains a - * reference to the Bindings. by encapsulating this way, I've made it - * a bit more difficult? we could {@code eng} and {@code script} arguments - * to execute() method. - */ - public static class ScriptExecution { - private ScriptEngine eng; - private ApplicationContext appCtx; - private Bindings bindings; - private String script; - - private StringWriter rawString; - private StringWriter htmlString; - private Throwable exception; - private int linesExecuted; - - public ScriptExecution(ScriptEngine eng, ApplicationContext appCtx, String script) { - this.eng = eng; - this.appCtx = appCtx; - this.bindings = new BeanLookupBindings(appCtx); - this.script = script; - if (StringUtils.isBlank(this.script)) { - this.script = ""; - } - this.rawString = new StringWriter(); - this.htmlString = new StringWriter(); - } - public void bind(String name, Object obj) { - bindings.put(name, obj); - } - public Object unbind(String name) { - return bindings.remove(name); - } - public void execute() { - PrintWriter rawOut = new PrintWriter(rawString); - PrintWriter htmlOut = new PrintWriter(htmlString); - bind("rawOut", rawOut); - bind("htmlOut", htmlOut); - bind("appCtx", appCtx); - try { - eng.eval(script, bindings); - // TODO: should count with RE rather than creating String[]? - linesExecuted = script.split("\r?\n").length; - } catch (ScriptException ex) { - Throwable cause = ex.getCause(); - exception = cause != null ? cause : ex; - } catch (RuntimeException ex) { - exception = ex; - } finally { - rawOut.flush(); - htmlOut.flush(); - // TODO: are these really necessary? - unbind("rawOut"); - unbind("htmlOut"); - unbind("appCtx"); - } - } - public boolean isFailure() { - return exception != null; - } - public String getStackTrace() { - if (exception == null) return ""; - StringWriter s = new StringWriter(); - exception.printStackTrace(new PrintWriter(s)); - return s.toString(); - } - public Throwable getException() { - return exception; - } - public int getLinesExecuted() { - return linesExecuted; - } - public String getRawOutput() { - return rawString.toString(); - } - public String getHtmlOutput() { - return htmlString.toString(); - } - public String getScript() { - return script; - } - } + + private ScriptModel scriptModel; @Override public void acceptRepresentation(Representation entity) throws ResourceException { @@ -204,12 +113,10 @@ public class ScriptResource extends JobRelatedResource { ScriptEngine eng = MANAGER.getEngineByName(chosenEngine); - scriptExec = new ScriptExecution(eng, cj.getJobContext(), script); - scriptExec.bind("job", cj); - scriptExec.bind("scriptResource", this); - scriptExec.execute(); - scriptExec.unbind("job"); - scriptExec.unbind("scriptResource"); + scriptModel = makeDataModel(); + scriptModel.bind("scriptResource", this); + scriptModel.execute(eng, script); + scriptModel.unbind("scriptResource"); //TODO: log script, results somewhere; job log INFO? @@ -217,11 +124,14 @@ public class ScriptResource extends JobRelatedResource { } public Representation represent(Variant variant) throws ResourceException { + if (scriptModel == null) { + scriptModel = makeDataModel(); + } Representation representation; if (variant.getMediaType() == MediaType.APPLICATION_XML) { representation = new WriterRepresentation(MediaType.APPLICATION_XML) { public void write(Writer writer) throws IOException { - XmlMarshaller.marshalDocument(writer,"script", makeDataModel()); + XmlMarshaller.marshalDocument(writer,"script", scriptModel); } }; } else { @@ -262,10 +172,9 @@ public class ScriptResource extends JobRelatedResource { } Reference baseRefRef = new Reference(baseRef); - ScriptModel model = new ScriptModel(cj.getShortName(), + ScriptModel model = new ScriptModel(cj, new Reference(baseRefRef, "..").getTargetRef().toString(), - getAvailableScriptEngines(), - scriptExec); + getAvailableScriptEngines()); return model; } @@ -284,7 +193,7 @@ public class ScriptResource extends JobRelatedResource { viewModel.put("cssRef", getStylesheetRef()); viewModel.put("staticRef", getStaticRef("")); viewModel.put("baseResourceRef",getRequest().getRootRef().toString()+"/engine/static/"); - viewModel.put("model",makeDataModel()); + viewModel.put("model", scriptModel); viewModel.put("selectedEngine", chosenEngine); try { diff --git a/engine/src/main/java/org/archive/crawler/restlet/XmlMarshaller.java b/engine/src/main/java/org/archive/crawler/restlet/XmlMarshaller.java index 88f94218..e933ec7a 100644 --- a/engine/src/main/java/org/archive/crawler/restlet/XmlMarshaller.java +++ b/engine/src/main/java/org/archive/crawler/restlet/XmlMarshaller.java @@ -19,10 +19,23 @@ package org.archive.crawler.restlet; +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; import java.io.IOException; import java.io.Writer; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; import java.util.Map; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.XmlType; + +import org.apache.commons.lang.StringUtils; import org.restlet.util.XmlWriter; import org.xml.sax.SAXException; @@ -90,6 +103,88 @@ public class XmlMarshaller { } } } + /** + * sort PropertyDescriptors according to propOrder. + * properties listed in propOrder come first, in the order they are listed, and + * then come remaining unlisted properties, in arbitrary order (likely alphabetical). + * this semantics is rather relaxed compared to original JAXB semantics, where props + * and propOrder must be the same set (except for those marked XmlTransient). + * @param props PropertyDescriptor array to be sorted in-place. + * @param propOrder list of property names in order of desired appearance. + */ + protected static void orderProperties(PropertyDescriptor[] props, final String[] propOrder) { + if (propOrder == null || propOrder.length == 0) return; + final Map order = new HashMap(); + for (int i = 0; i < propOrder.length; i++) { + order.put(propOrder[i], i); + } + final Integer LAST = Integer.valueOf(propOrder.length); + Arrays.sort(props, new Comparator() { + @Override + public int compare(PropertyDescriptor o1, PropertyDescriptor o2) { + Integer c1 = order.get(o1.getName()); + Integer c2 = order.get(o2.getName()); + return (c1 != null ? c1 : LAST).compareTo(c2 != null ? c2 : LAST); + } + }); + } + /** + * test if {@code obj} has {@link XmlRootElement} annotation. + * to avoid unexpected side effects, objects are mapped to nested XML structure + * only when its class has {@link XmlRootElement} annotation. + * note semantics is slightly different from JAXB - just borrowing XmlRootElement + * as substitute of XmlElement, because Map entry cannot be annotated. + * @param obj object to test + * @return true if obj's class has XmlRootElement annotation. + */ + protected static boolean marshalAsElement(Object obj) { + XmlRootElement ann = obj.getClass().getAnnotation(XmlRootElement.class); + return ann != null; + } + /** + * generate nested XML structure for a bean {@code obj}. enclosing element + * will not be generated if {@code key} is empty. each readable JavaBeans property + * is mapped to an nested element named after its name. Those properties + * annotated with {@link XmlTransient} are ignored. + * @param xmlWriter XmlWriter + * @param key name of enclosing element + * @param obj bean + * @throws SAXException + */ + protected static void marshalBean(XmlWriter xmlWriter, String key, Object obj) throws SAXException { + if (!StringUtils.isEmpty(key)) + xmlWriter.startElement(key); + try { + BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass(), Object.class); + PropertyDescriptor[] props = beanInfo.getPropertyDescriptors(); + XmlType xmlType = obj.getClass().getAnnotation(XmlType.class); + if (xmlType != null) { + String[] propOrder = xmlType.propOrder(); + if (propOrder != null) { + // TODO: should cache this sorted version? + orderProperties(props, propOrder); + } + } + for (PropertyDescriptor prop : props) { + Method m = prop.getReadMethod(); + if (m == null || m.getAnnotation(XmlTransient.class) != null) + continue; + try { + Object propValue = m.invoke(obj); + if (propValue != null && !"".equals(propValue)) { + marshal(xmlWriter, prop.getName(), propValue); + } + } catch (Exception ex) { + // generate empty element, for now. generate comment? + xmlWriter.emptyElement(prop.getName()); + } + } + } catch (IntrospectionException ex) { + // ignored, for now. + } + if (!StringUtils.isEmpty(key)) + xmlWriter.endElement(key); + } protected static void marshal(XmlWriter xmlWriter, String key, Object value) throws SAXException { if (value == null) { @@ -98,6 +193,8 @@ public class XmlMarshaller { marshal(xmlWriter, key, (Map) value); } else if (value instanceof Iterable) { marshal(xmlWriter, key, (Iterable) value); + } else if (marshalAsElement(value)) { + marshalBean(xmlWriter, key, value); } else { xmlWriter.dataElement(key, value.toString()); } diff --git a/engine/src/main/java/org/archive/crawler/restlet/models/ScriptModel.java b/engine/src/main/java/org/archive/crawler/restlet/models/ScriptModel.java index f7ef1c9c..e748ed76 100644 --- a/engine/src/main/java/org/archive/crawler/restlet/models/ScriptModel.java +++ b/engine/src/main/java/org/archive/crawler/restlet/models/ScriptModel.java @@ -1,28 +1,51 @@ package org.archive.crawler.restlet.models; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import org.archive.crawler.restlet.ScriptResource.ScriptExecution; +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptException; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; -@SuppressWarnings("serial") -public class ScriptModel extends LinkedHashMap { +import org.archive.crawler.framework.BeanLookupBindings; +import org.archive.crawler.framework.CrawlJob; - public ScriptModel(String crawlJobShortName, String crawlJobUrl, - Collection> scriptEngines, - ScriptExecution scriptExec) {// int linesExecuted, Exception - // exception, String rawOutput, - // String htmlOutput){ - super(); - this.put("crawlJobUrl",crawlJobUrl); - this.put("crawlJobShortName", crawlJobShortName); - this.put("availableScriptEngines", scriptEngines); - - if (scriptExec != null) - this.put("scriptExec", scriptExec); +@XmlRootElement(name="script") +@XmlType(propOrder={ + "crawlJobUrl", "crawlJobShortName", "availableScriptEngines", + "script", "linesExecuted", "exception", "rawOutput", "htmlOutput", + "availableGlobalVariables" +}) +public class ScriptModel { + private CrawlJob cj; + private String crawlJobUrl; + private Collection> availableScriptEngines; + + //private ApplicationContext appCtx; + private ScriptEngine eng; + private String script; + private Bindings bindings; + + private StringWriter rawString; + private StringWriter htmlString; + private Throwable exception; + private int linesExecuted; + + private List> availableGlobalVariables; + + public ScriptModel(CrawlJob cj, + String crawlJobUrl, + Collection> scriptEngines) { + this.crawlJobUrl = crawlJobUrl; + this.cj = cj; + this.availableScriptEngines = scriptEngines; List> vars = new LinkedList>(); Map var; @@ -51,6 +74,86 @@ public class ScriptModel extends LinkedHashMap { var.put("variable", "scriptResource"); var.put("description", "the ScriptResource implementing this page, which offers utility methods"); vars.add(var); - this.put("availableGlobalVariables", vars); + + this.availableGlobalVariables = vars; + + this.bindings = new BeanLookupBindings(this.cj.getJobContext()); + this.script = ""; + } + + public void bind(String name, Object obj) { + bindings.put(name, obj); + } + public Object unbind(String name) { + return bindings.remove(name); + } + public void execute(ScriptEngine eng, String script) { + // TODO: update through setter rather than passing as method arguments? + this.eng = eng; + this.script = script; + + bind("job", cj); + rawString = new StringWriter(); + htmlString = new StringWriter(); + PrintWriter rawOut = new PrintWriter(rawString); + PrintWriter htmlOut = new PrintWriter(htmlString); + bind("rawOut", rawOut); + bind("htmlOut", htmlOut); + bind("appCtx", cj.getJobContext()); + try { + this.eng.eval(this.script, bindings); + // TODO: should count with RE rather than creating String[]? + linesExecuted = script.split("\r?\n").length; + } catch (ScriptException ex) { + Throwable cause = ex.getCause(); + exception = cause != null ? cause : ex; + } catch (RuntimeException ex) { + exception = ex; + } finally { + rawOut.flush(); + htmlOut.flush(); + // TODO: are these really necessary? + unbind("rawOut"); + unbind("htmlOut"); + unbind("appCtx"); + unbind("job"); + } + } + public boolean isFailure() { + return exception != null; + } + public String getStackTrace() { + if (exception == null) return ""; + StringWriter s = new StringWriter(); + exception.printStackTrace(new PrintWriter(s)); + return s.toString(); + } + public Throwable getException() { + return exception; + } + public int getLinesExecuted() { + return linesExecuted; + } + public String getRawOutput() { + return rawString != null ? rawString.toString() : ""; + } + public String getHtmlOutput() { + return htmlString != null ? htmlString.toString() : ""; + } + public String getScript() { + return script; + } + + public String getCrawlJobShortName() { + return cj.getShortName(); + } + public Collection> getAvailableScriptEngines() { + return availableScriptEngines; + } + public List> getAvailableGlobalVariables() { + return availableGlobalVariables; + } + public String getCrawlJobUrl() { + return crawlJobUrl; } } diff --git a/engine/src/main/resources/org/archive/crawler/restlet/Script.ftl b/engine/src/main/resources/org/archive/crawler/restlet/Script.ftl index 3ccbb385..9201c258 100644 --- a/engine/src/main/resources/org/archive/crawler/restlet/Script.ftl +++ b/engine/src/main/resources/org/archive/crawler/restlet/Script.ftl @@ -15,29 +15,26 @@

Execute script for job ${model.crawlJobShortName}

- - <#if model.scriptExec??> - <#if (model.scriptExec.linesExecuted > 0)> - ${model.scriptExec.linesExecuted} ${(model.scriptExec.linesExecuted>1)?string("lines","line")} executed + <#if (model.linesExecuted > 0)> + ${model.linesExecuted} ${(model.linesExecuted>1)?string("lines","line")} executed - <#if model.scriptExec.failure> -
${model.scriptExec.stackTrace}
+	<#if model.failure>
+	
${model.stackTrace}
 	
- <#assign htmlOutput=model.scriptExec.htmlOutput> + <#assign htmlOutput=model.htmlOutput> <#if (htmlOutput?length > 0)>
htmlOut ${htmlOutput}
- <#assign rawOutput=model.scriptExec.rawOutput> + <#assign rawOutput=model.rawOutput> <#if (rawOutput?length > 0)>
rawOutput
${rawOutput}
 		
-
@@ -46,7 +43,7 @@ selected="selected" value="${scriptEngine.engine}">${scriptEngine.language} - + The script will be executed in an engine preloaded @@ -69,7 +66,3 @@ - - - - From 3a54600a79a95eb09479d88ef57e51a4c32dce76 Mon Sep 17 00:00:00 2001 From: Kenji Nagahashi Date: Wed, 29 May 2013 12:03:49 -0700 Subject: [PATCH 2/3] ADD unit test for XmlMarshaller --- .../crawler/restlet/XmlMarshallerTest.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 engine/src/test/java/org/archive/crawler/restlet/XmlMarshallerTest.java diff --git a/engine/src/test/java/org/archive/crawler/restlet/XmlMarshallerTest.java b/engine/src/test/java/org/archive/crawler/restlet/XmlMarshallerTest.java new file mode 100644 index 00000000..ac44ac2a --- /dev/null +++ b/engine/src/test/java/org/archive/crawler/restlet/XmlMarshallerTest.java @@ -0,0 +1,112 @@ +/** + * + */ +package org.archive.crawler.restlet; + +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +import junit.framework.TestCase; + +/** + * @author kenji + * + */ +public class XmlMarshallerTest extends TestCase { + + /* (non-Javadoc) + * @see junit.framework.TestCase#setUp() + */ + protected void setUp() throws Exception { + super.setUp(); + } + + /** + * Map with nested Map + * @throws Exception + */ + public void testMarshalMap() throws Exception { + Map map = new LinkedHashMap(); + map.put("c", 1); + map.put("a", "a-value"); + Map nestedMap = new LinkedHashMap(); + nestedMap.put("x", 10); + nestedMap.put("y", "y-value"); + map.put("m", nestedMap); + + StringWriter w = new StringWriter(); + XmlMarshaller.marshalDocument(w, "doc", map); + + String expected = "(?s:)" + + "^<\\?xml version=\"1\\.0\" standalone='yes'\\?>\\s*" + + "\\s*1\\s*a-value\\s*" + + "\\s*10\\s*y-value\\s*\\s*" + + "\\s*$"; + String xml = w.toString(); + System.out.println(xml); + assertTrue("xml matches expected RE", xml.matches(expected)); + } + + @XmlRootElement + @XmlType(propOrder={"c", "a", "m"}) + public static class Model { + public int getC() { return 1; } + public String getA() { return "a-value"; } + public Object getM() { + return new NestedModel(); + } + } + @XmlRootElement + @XmlType(propOrder={"x", "y"}) + public static class NestedModel { + public int getX() { return 10; } + public String getY() { return "y-value"; } + } + + /** + * Bean with nested Bean + * @throws Exception + */ + public void testMashalBean() throws Exception { + Object bean = new Model(); + StringWriter w = new StringWriter(); + XmlMarshaller.marshalDocument(w, "doc", bean); + + String expected = "(?s:)" + + "^<\\?xml version=\"1\\.0\" standalone='yes'\\?>\\s*" + + "\\s*1\\s*a-value\\s*" + + "\\s*10\\s*y-value\\s*\\s*" + + "\\s*$"; + String xml = w.toString(); + System.out.println(xml); + assertTrue("xml matches expected RE", xml.matches(expected)); + } + + /** + * Map with nested Bean + * @throws Exception + */ + public void testMarshalBeanInMap() throws Exception { + Map map = new LinkedHashMap(); + map.put("c", 1); + map.put("a", "a-value"); + Object m = new NestedModel(); + map.put("m", m); + + StringWriter w = new StringWriter(); + XmlMarshaller.marshalDocument(w, "doc", map); + + String expected = "(?s:)" + + "^<\\?xml version=\"1\\.0\" standalone='yes'\\?>\\s*" + + "\\s*1\\s*a-value\\s*" + + "\\s*10\\s*y-value\\s*\\s*" + + "\\s*$"; + String xml = w.toString(); + System.out.println(xml); + assertTrue("xml matches expected RE", xml.matches(expected)); + } +} From 358afd448673798ea22d99d616b776e8bea2bffa Mon Sep 17 00:00:00 2001 From: Kenji Nagahashi Date: Mon, 10 Jun 2013 14:17:27 -0700 Subject: [PATCH 3/3] revised scripting console xml response fix based on comments. removed scripting console actions from ScriptModel into ScriptingConsole class (resurrection of ScriptExec). added basic ScriptingConsoleTest. --- .../crawler/restlet/ScriptResource.java | 20 ++- .../crawler/restlet/ScriptingConsole.java | 129 ++++++++++++++++++ .../crawler/restlet/models/ScriptModel.java | 114 ++-------------- .../crawler/restlet/ScriptingConsoleTest.java | 64 +++++++++ 4 files changed, 215 insertions(+), 112 deletions(-) create mode 100644 engine/src/main/java/org/archive/crawler/restlet/ScriptingConsole.java create mode 100644 engine/src/test/java/org/archive/crawler/restlet/ScriptingConsoleTest.java diff --git a/engine/src/main/java/org/archive/crawler/restlet/ScriptResource.java b/engine/src/main/java/org/archive/crawler/restlet/ScriptResource.java index 5f3da976..dd0fa55c 100644 --- a/engine/src/main/java/org/archive/crawler/restlet/ScriptResource.java +++ b/engine/src/main/java/org/archive/crawler/restlet/ScriptResource.java @@ -92,6 +92,8 @@ public class ScriptResource extends JobRelatedResource { tmpltCfg.setClassForTemplateLoading(this.getClass(),""); tmpltCfg.setObjectWrapper(ObjectWrapper.BEANS_WRAPPER); setTemplateConfiguration(tmpltCfg); + + scriptingConsole = new ScriptingConsole(cj); } public void setTemplateConfiguration(Configuration tmpltCfg) { _templateConfiguration=tmpltCfg; @@ -100,7 +102,7 @@ public class ScriptResource extends JobRelatedResource { return _templateConfiguration; } - private ScriptModel scriptModel; + private ScriptingConsole scriptingConsole; @Override public void acceptRepresentation(Representation entity) throws ResourceException { @@ -113,10 +115,9 @@ public class ScriptResource extends JobRelatedResource { ScriptEngine eng = MANAGER.getEngineByName(chosenEngine); - scriptModel = makeDataModel(); - scriptModel.bind("scriptResource", this); - scriptModel.execute(eng, script); - scriptModel.unbind("scriptResource"); + scriptingConsole.bind("scriptResource", this); + scriptingConsole.execute(eng, script); + scriptingConsole.unbind("scriptResource"); //TODO: log script, results somewhere; job log INFO? @@ -124,14 +125,11 @@ public class ScriptResource extends JobRelatedResource { } public Representation represent(Variant variant) throws ResourceException { - if (scriptModel == null) { - scriptModel = makeDataModel(); - } Representation representation; if (variant.getMediaType() == MediaType.APPLICATION_XML) { representation = new WriterRepresentation(MediaType.APPLICATION_XML) { public void write(Writer writer) throws IOException { - XmlMarshaller.marshalDocument(writer,"script", scriptModel); + XmlMarshaller.marshalDocument(writer,"script", makeDataModel()); } }; } else { @@ -172,7 +170,7 @@ public class ScriptResource extends JobRelatedResource { } Reference baseRefRef = new Reference(baseRef); - ScriptModel model = new ScriptModel(cj, + ScriptModel model = new ScriptModel(scriptingConsole, new Reference(baseRefRef, "..").getTargetRef().toString(), getAvailableScriptEngines()); @@ -193,7 +191,7 @@ public class ScriptResource extends JobRelatedResource { viewModel.put("cssRef", getStylesheetRef()); viewModel.put("staticRef", getStaticRef("")); viewModel.put("baseResourceRef",getRequest().getRootRef().toString()+"/engine/static/"); - viewModel.put("model", scriptModel); + viewModel.put("model", makeDataModel()); viewModel.put("selectedEngine", chosenEngine); try { diff --git a/engine/src/main/java/org/archive/crawler/restlet/ScriptingConsole.java b/engine/src/main/java/org/archive/crawler/restlet/ScriptingConsole.java new file mode 100644 index 00000000..e18629b9 --- /dev/null +++ b/engine/src/main/java/org/archive/crawler/restlet/ScriptingConsole.java @@ -0,0 +1,129 @@ +/** + * + */ +package org.archive.crawler.restlet; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import org.archive.crawler.framework.BeanLookupBindings; +import org.archive.crawler.framework.CrawlJob; + +/** + * ScriptingConsole implements view-independent logic of scripting console. + * + * Currently it is short-lived; it is created by ScriptResource for each request and + * destroyed after rendering the view. + * + * @contributor kenji + * + */ +public class ScriptingConsole { + private final CrawlJob cj; + + private ScriptEngine eng; + private String script; + private Bindings bindings; + + private StringWriter rawString; + private StringWriter htmlString; + private Throwable exception; + private int linesExecuted; + + private List> availableGlobalVariables; + + public ScriptingConsole(CrawlJob job) { + this.cj = job; + this.bindings = new BeanLookupBindings(this.cj.getJobContext()); + this.script = ""; + + setupAvailableGlobalVariables(); + } + protected void addGlobalVariable(String name, String desc) { + Map var = new LinkedHashMap(); + var.put("variable", name); + var.put("description", desc); + availableGlobalVariables.add(var); + } + private void setupAvailableGlobalVariables() { + availableGlobalVariables = new LinkedList>(); + addGlobalVariable("rawOut", "a PrintWriter for arbitrary text output to this page"); + addGlobalVariable("htmlOut", "a PrintWriter for HTML output to this page"); + addGlobalVariable("job", "the current CrawlJob instance"); + addGlobalVariable("appCtx", "current job ApplicationContext, if any"); + // TODO: a bit awkward to have this here, because ScriptingConsole has no ref to + // ScriptResource. better to have ScriptResource call #addGlobalVariable(String, String)? + addGlobalVariable("scriptResource", + "the ScriptResource implementing this page, which offers utility methods"); + } + + public void bind(String name, Object obj) { + bindings.put(name, obj); + } + public Object unbind(String name) { + return bindings.remove(name); + } + public void execute(ScriptEngine eng, String script) { + // TODO: update through setter rather than passing as method arguments? + this.eng = eng; + this.script = script; + + bind("job", cj); + rawString = new StringWriter(); + htmlString = new StringWriter(); + PrintWriter rawOut = new PrintWriter(rawString); + PrintWriter htmlOut = new PrintWriter(htmlString); + bind("rawOut", rawOut); + bind("htmlOut", htmlOut); + bind("appCtx", cj.getJobContext()); + exception = null; + try { + this.eng.eval(this.script, bindings); + // TODO: should count with RE rather than creating String[]? + linesExecuted = script.split("\r?\n").length; + } catch (ScriptException ex) { + Throwable cause = ex.getCause(); + exception = cause != null ? cause : ex; + } catch (RuntimeException ex) { + exception = ex; + } finally { + rawOut.flush(); + htmlOut.flush(); + // TODO: are these really necessary? + unbind("rawOut"); + unbind("htmlOut"); + unbind("appCtx"); + unbind("job"); + } + } + public CrawlJob getCrawlJob( ) { + return cj; + } + public Throwable getException() { + return exception; + } + public int getLinesExecuted() { + return linesExecuted; + } + public String getRawOutput() { + return rawString != null ? rawString.toString() : ""; + } + public String getHtmlOutput() { + return htmlString != null ? htmlString.toString() : ""; + } + public String getScript() { + return script; + } + + public List> getAvailableGlobalVariables() { + return availableGlobalVariables; + } +} diff --git a/engine/src/main/java/org/archive/crawler/restlet/models/ScriptModel.java b/engine/src/main/java/org/archive/crawler/restlet/models/ScriptModel.java index e748ed76..0fa5a970 100644 --- a/engine/src/main/java/org/archive/crawler/restlet/models/ScriptModel.java +++ b/engine/src/main/java/org/archive/crawler/restlet/models/ScriptModel.java @@ -3,19 +3,13 @@ package org.archive.crawler.restlet.models; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import javax.script.Bindings; -import javax.script.ScriptEngine; -import javax.script.ScriptException; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; -import org.archive.crawler.framework.BeanLookupBindings; -import org.archive.crawler.framework.CrawlJob; +import org.archive.crawler.restlet.ScriptingConsole; @XmlRootElement(name="script") @XmlType(propOrder={ @@ -24,134 +18,52 @@ import org.archive.crawler.framework.CrawlJob; "availableGlobalVariables" }) public class ScriptModel { - private CrawlJob cj; private String crawlJobUrl; private Collection> availableScriptEngines; - - //private ApplicationContext appCtx; - private ScriptEngine eng; - private String script; - private Bindings bindings; - - private StringWriter rawString; - private StringWriter htmlString; - private Throwable exception; - private int linesExecuted; - - private List> availableGlobalVariables; + private ScriptingConsole scriptingConsole; - public ScriptModel(CrawlJob cj, + public ScriptModel(ScriptingConsole cc, String crawlJobUrl, Collection> scriptEngines) { + scriptingConsole = cc; this.crawlJobUrl = crawlJobUrl; - this.cj = cj; this.availableScriptEngines = scriptEngines; - - List> vars = new LinkedList>(); - Map var; - - var = new LinkedHashMap(); - var.put("variable", "rawOut"); - var.put("description", "a PrintWriter for arbitrary text output to this page"); - vars.add(var); - - var = new LinkedHashMap(); - var.put("variable", "htmlOut"); - var.put("description", "a PrintWriter for HTML output to this page"); - vars.add(var); - - var = new LinkedHashMap(); - var.put("variable", "job"); - var.put("description", "the current CrawlJob instance"); - vars.add(var); - - var = new LinkedHashMap(); - var.put("variable", "appCtx"); - var.put("description", "current job ApplicationContext, if any"); - vars.add(var); - - var = new LinkedHashMap(); - var.put("variable", "scriptResource"); - var.put("description", "the ScriptResource implementing this page, which offers utility methods"); - vars.add(var); - - this.availableGlobalVariables = vars; - - this.bindings = new BeanLookupBindings(this.cj.getJobContext()); - this.script = ""; } - public void bind(String name, Object obj) { - bindings.put(name, obj); - } - public Object unbind(String name) { - return bindings.remove(name); - } - public void execute(ScriptEngine eng, String script) { - // TODO: update through setter rather than passing as method arguments? - this.eng = eng; - this.script = script; - - bind("job", cj); - rawString = new StringWriter(); - htmlString = new StringWriter(); - PrintWriter rawOut = new PrintWriter(rawString); - PrintWriter htmlOut = new PrintWriter(htmlString); - bind("rawOut", rawOut); - bind("htmlOut", htmlOut); - bind("appCtx", cj.getJobContext()); - try { - this.eng.eval(this.script, bindings); - // TODO: should count with RE rather than creating String[]? - linesExecuted = script.split("\r?\n").length; - } catch (ScriptException ex) { - Throwable cause = ex.getCause(); - exception = cause != null ? cause : ex; - } catch (RuntimeException ex) { - exception = ex; - } finally { - rawOut.flush(); - htmlOut.flush(); - // TODO: are these really necessary? - unbind("rawOut"); - unbind("htmlOut"); - unbind("appCtx"); - unbind("job"); - } - } public boolean isFailure() { - return exception != null; + return scriptingConsole.getException() != null; } public String getStackTrace() { + Throwable exception = scriptingConsole.getException(); if (exception == null) return ""; StringWriter s = new StringWriter(); exception.printStackTrace(new PrintWriter(s)); return s.toString(); } public Throwable getException() { - return exception; + return scriptingConsole.getException(); } public int getLinesExecuted() { - return linesExecuted; + return scriptingConsole.getLinesExecuted(); } public String getRawOutput() { - return rawString != null ? rawString.toString() : ""; + return scriptingConsole.getRawOutput(); } public String getHtmlOutput() { - return htmlString != null ? htmlString.toString() : ""; + return scriptingConsole.getHtmlOutput(); } public String getScript() { - return script; + return scriptingConsole.getScript(); } public String getCrawlJobShortName() { - return cj.getShortName(); + return scriptingConsole.getCrawlJob().getShortName(); } public Collection> getAvailableScriptEngines() { return availableScriptEngines; } public List> getAvailableGlobalVariables() { - return availableGlobalVariables; + return scriptingConsole.getAvailableGlobalVariables(); } public String getCrawlJobUrl() { return crawlJobUrl; diff --git a/engine/src/test/java/org/archive/crawler/restlet/ScriptingConsoleTest.java b/engine/src/test/java/org/archive/crawler/restlet/ScriptingConsoleTest.java new file mode 100644 index 00000000..34c096b3 --- /dev/null +++ b/engine/src/test/java/org/archive/crawler/restlet/ScriptingConsoleTest.java @@ -0,0 +1,64 @@ +package org.archive.crawler.restlet; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + +import junit.framework.TestCase; + +import org.archive.crawler.framework.CrawlJob; +import org.archive.spring.PathSharingContext; + +public class ScriptingConsoleTest extends TestCase { + // barebone CrawlJob object. + public static class TestCrawlJob extends CrawlJob { + public TestCrawlJob() { + super(null); + this.ac = new PathSharingContext(new String[0]); + } + @Override + protected void scanJobLog() { + } + } + CrawlJob cj; + ScriptingConsole sc; + + protected void setUp() throws Exception { + super.setUp(); + cj = new TestCrawlJob(); + sc = new ScriptingConsole(cj); + } + + public void testInitialState() { + assertEquals("script is empty", "", sc.getScript()); + assertNull("exception is null", sc.getException()); + } + + public void testExecute() { + final String script = "rawOut.println 'elk'"; + final ScriptEngineManager manager = new ScriptEngineManager(); + final ScriptEngine eng = manager.getEngineByName("groovy"); + sc.execute(eng, script); + + assertNull("exception is null", sc.getException()); + assertEquals("has the same script", sc.getScript(), script); + assertEquals("linesExecuted", 1, sc.getLinesExecuted()); + assertEquals("rawOut", "elk\n", sc.getRawOutput()); + + } + + public void testExecuteError() { + final String script = "rawOut.println undef"; + final ScriptEngineManager manager = new ScriptEngineManager(); + final ScriptEngine eng = manager.getEngineByName("groovy"); + sc.execute(eng, script); + + assertNotNull("exception is non-null", sc.getException()); + assertEquals("rawOut", "", sc.getRawOutput()); + assertEquals("linesExecuted", 0, sc.getLinesExecuted()); + + // extra test - it is okay to fail this test is okay because + // ScriptingConsole is single-use now. + sc.execute(eng, "rawOut.println 1"); + assertNull("exception is cleared", sc.getException()); + } +}