diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java deleted file mode 100644 index 5249681c7c..0000000000 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java +++ /dev/null @@ -1,218 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.library.scholars.webapp.controller.api.distribute.decorator; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; - -import javax.script.Invocable; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; -import javax.servlet.ServletContext; - -import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; -import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; -import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * Wrap a data distributor with a JavaScript function that will transform its - * output. - * - *

- * - * The child distributor might produce any arbitrary output. The JavaScript - * function must be written to accept that output as a String and return a - * String containing the transformed output. - * - *

- * - * For example, the function might replace all occurences of a namespace with a - * different namespace, like this: - * - *

- * function transform(rawData) {
- *     return rawData.split("http://first/").join("http://second/");
- * }
- * 
- * - * The JavaScript method must be named 'transform', must accept a String as - * argument, and must return a String as result. - * - *

- * - * The JavaScript execution environment will include a global variable named - * 'logger'. This is a binding of an org.apache.commons.logging.Log object, and - * can be used to write to the VIVO log file. - * - *

- * - * Note: this decorator is only scalable to a limited extent, since the - * JavaScript function works with Strings instead of Streams. - */ -public class JavaScriptTransformDistributor extends AbstractDataDistributor { - private static final Log log = LogFactory - .getLog(JavaScriptTransformDistributor.class); - - /** The content type to attach to the file. */ - private String contentType; - private String script; - private DataDistributor child; - private List supportingScriptPaths = new ArrayList<>(); - - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType", minOccurs = 1, maxOccurs = 1) - public void setContentType(String cType) { - contentType = cType; - } - - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#script", minOccurs = 1, maxOccurs = 1) - public void setScript(String scriptIn) { - script = scriptIn; - } - - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#child", minOccurs = 1, maxOccurs = 1) - public void setChild(DataDistributor c) { - child = c; - } - - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#supportingScript") - public void addScriptPath(String path) { - supportingScriptPaths.add(path); - } - - @Override - public String getContentType() throws DataDistributorException { - return contentType; - } - - @Override - public void init(DataDistributorContext ddc) - throws DataDistributorException { - super.init(ddc); - child.init(ddc); - } - - /** - */ - @Override - public void writeOutput(OutputStream output) - throws DataDistributorException { - ScriptEngine engine = createScriptEngine(); - addLoggerToEngine(engine); - loadSupportingScripts(engine); - loadMainScript(engine); - - writeTransformedOutput(output, - runTransformFunction(engine, runChildDistributor())); - } - - private ScriptEngine createScriptEngine() { - return new ScriptEngineManager().getEngineByName("nashorn"); - } - - private void addLoggerToEngine(ScriptEngine engine) { - String loggerName = this.getClass().getName() + "." + actionName; - Log jsLogger = LogFactory.getLog(loggerName); - engine.put("logger", jsLogger); - } - - private void loadSupportingScripts(ScriptEngine engine) - throws DataDistributorException { - log.debug("loading supporting scripts"); - for (String path : supportingScriptPaths) { - loadSupportingScript(engine, path); - log.debug("loaded supporting script: " + path); - } - } - - private void loadSupportingScript(ScriptEngine engine, String path) - throws DataDistributorException { - ServletContext ctx = ApplicationUtils.instance().getServletContext(); - - InputStream resource = ctx.getResourceAsStream(path); - if (resource == null) { - throw new DataDistributorException( - "Can't locate script resource for '" + path + "'"); - } - - try { - engine.eval(new InputStreamReader(resource)); - } catch (ScriptException e) { - throw new DataDistributorException( - "Script at '" + path + "' contains syntax errors.", e); - } - } - - private void loadMainScript(ScriptEngine engine) - throws DataDistributorException { - try { - engine.eval(script); - } catch (ScriptException e) { - throw new DataDistributorException("Script contains syntax errors.", - e); - } - } - - private String runChildDistributor() throws DataDistributorException { - ByteArrayOutputStream childOut = new ByteArrayOutputStream(); - try { - child.writeOutput(childOut); - log.debug("ran child distributor"); - } catch (Exception e) { - throw new DataDistributorException( - "Child distributor threw an exception", e); - } - try { - return childOut.toString("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("What? No UTF-8 Charset?", e); - } - } - - private String runTransformFunction(ScriptEngine engine, String childOutput) - throws DataDistributorException { - try { - Invocable invocable = (Invocable) engine; - Object result = invocable.invokeFunction("transform", childOutput); - log.debug("ran transform function"); - - if (result instanceof String) { - return (String) result; - } else { - throw new ActionFailedException( - "transform function must return a String"); - } - } catch (NoSuchMethodException e) { - throw new DataDistributorException( - "Script must have a transform() function.", e); - } catch (ScriptException e) { - throw new DataDistributorException("Script contains syntax errors.", - e); - } - } - - private void writeTransformedOutput(OutputStream output, String transformed) - throws DataDistributorException { - try { - output.write(transformed.getBytes("UTF-8")); - } catch (IOException e) { - throw new DataDistributorException(e); - } - } - - @Override - public void close() throws DataDistributorException { - child.close(); - } - -} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java deleted file mode 100644 index 454cd82a13..0000000000 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java +++ /dev/null @@ -1,73 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.library.scholars.webapp.controller.api.distribute.examples; - -import java.io.IOException; -import java.io.OutputStream; - -import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; - -/** - * A simple example of a data distributor. It sends a greeting. - */ -public class HelloDistributor extends AbstractDataDistributor { - - private static final Object NAME_PARAMETER_KEY = "name"; - - /** - * The instance is created to service one HTTP request, and init() is - * called. - * - * The DataDistributorContext provides access to the request parameters, and - * the triple-store connections. - */ - @Override - public void init(DataDistributorContext ddc) - throws DataDistributorException { - super.init(ddc); - } - - /** - * For this distributor, the browser should treat the output as simple text. - */ - @Override - public String getContentType() throws DataDistributorException { - return "text/plain"; - } - - /** - * The text written to the OutputStream will become the body of the HTTP - * response. - * - * This will only be called once for a given instance. - */ - @Override - public void writeOutput(OutputStream output) - throws DataDistributorException { - try { - if (parameters.containsKey(NAME_PARAMETER_KEY)) { - output.write(String - .format("Hello, %s!", - parameters.get(NAME_PARAMETER_KEY)[0]) - .getBytes()); - } else { - output.write("Hello, World!".getBytes()); - } - } catch (IOException e) { - throw new ActionFailedException(e); - } - } - - /** - * Release any resources. In this case, none. - * - * Garbage collection is uncertain. On the other hand, you can be confident - * that this will be called in a timely manner. - */ - @Override - public void close() throws DataDistributorException { - // Nothing to do. - } - -} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java deleted file mode 100644 index 64a2c4618b..0000000000 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java +++ /dev/null @@ -1,422 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.library.scholars.webapp.controller.api.distribute.decorator; - -import static org.junit.Assert.assertEquals; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; - -import javax.script.ScriptException; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; -import edu.cornell.mannlib.vitro.testing.AbstractTestClass; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.PatternLayout; -import org.junit.Before; -import org.junit.Test; -import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; -import stubs.edu.cornell.mannlib.vitro.webapp.modules.ApplicationStub; -import stubs.javax.servlet.ServletContextStub; - -/** - * Test the basic functions of JavaScriptTransformDistributor. - * - * The simple transform just returns a hard-coded String. - * - * The full script accepts a JSON string, parses it, substitutes a value, and - * returns the stringified result. - * - * The multi-script requires two additional scripts in order to assemble a - * hard-coded String. - */ -public class JavaScriptTransformDistributorTest extends AbstractTestClass { - private static final String ACTION_NAME = "tester"; - private static final String JAVASCRIPT_TYPE = "text/javascript"; - private static final String TEXT_TYPE = "text/plain"; - - private static final String BAD_SYNTAX_SCRIPT = "" // - + "function transform( {}"; - - private static final String WRONG_FUNCTION_SCRIPT = "" // - + "function notTransform() { \n" // - + " return 'true'; \n" // - + "}"; - - private static final String WRONG_RETURN_TYPE_SCRIPT = "" // - + "function transform() { \n" // - + " return 3; \n" // - + "}"; - - private static final String SIMPLE_SCRIPT = "" // - + "function transform() { \n" // - + " return 'true'; \n" // - + "}"; - - private static final String SIMPLE_EXPECTED_RESULT = "true"; - - private static final String ECHO_SCRIPT = "" // - + "function transform(data) { \n" // - + " return data; \n" // - + "}"; - - private static final String UNICODE_STRING = "Lévesque"; - - private static final String FULL_SCRIPT = "" // - + "function transform(data) { \n" // - + " var initial = JSON.parse(data); \n" // - + " var result = {}; \n" // - + " Object.keys(initial).forEach(populate); \n" // - + " return JSON.stringify(result); \n" // - + "\n" // - + " function populate(key) { \n" // - + " result[key] = (initial[key] == 'initial') ? \n" // - + " 'transformed' : initial[key]; \n" // - + " } \n" // - + "}"; - - private static final String TRANSFORMED_STRUCTURE = "{ 'a': 'transformed', 'b': 'constant' }" - .replace('\'', '\"'); - - private static final String INITIAL_STRUCTURE = "{ 'a': 'initial', 'b': 'constant' }" - .replace('\'', '"'); - - private static final String PATH_TO_MISSING_SCRIPT = "/no/script/here"; - private static final String PATH_TO_BAD_SYNTAX_SCRIPT = "/bad/syntax/script.js"; - - private static final String PATH_TO_SUPPORTING_SCRIPT_1 = "/support/script1.js"; - private static final String PATH_TO_SUPPORTING_SCRIPT_2 = "/support/script2.js"; - - private static final String SUPPORTING_SCRIPT_1 = "" // - + "function one() { \n" // - + " return '1'; \n" // - + "}"; - private static final String SUPPORTING_SCRIPT_2 = "" // - + "function two() { \n" // - + " return '2'; \n" // - + "}"; - private static final String SUPPORTED_SCRIPT = "" // - + "function transform() { \n" // - + " return one() + ' ' + two() + ' 3'; \n" // - + "}"; - private static final String SUPPORTED_EXPECTED_RESULT = "1 2 3"; - - private static final String LOGGING_SCRIPT = "" // - + "function transform() { \n" // - + " logger.debug('debug message'); \n" // - + " logger.info('info message'); \n" // - + " logger.warn('warn message'); \n" // - + " logger.error('error message'); \n" // - + " return ''; \n" // - + "}"; - private static final String LOGGER_NAME = JavaScriptTransformDistributor.class - .getName() + "." + ACTION_NAME; - - public JavaScriptTransformDistributor transformer; - public TestDistributor child; - public DataDistributorContext ddc; - public ByteArrayOutputStream outputStream; - public String logOutput; - - @Before - public void setup() { - ServletContextStub ctx = new ServletContextStub(); - ctx.setMockResource(PATH_TO_BAD_SYNTAX_SCRIPT, BAD_SYNTAX_SCRIPT); - ctx.setMockResource(PATH_TO_SUPPORTING_SCRIPT_1, SUPPORTING_SCRIPT_1); - ctx.setMockResource(PATH_TO_SUPPORTING_SCRIPT_2, SUPPORTING_SCRIPT_2); - ApplicationStub.setup(ctx, null); - - ddc = new DataDistributorContextStub(null); - outputStream = new ByteArrayOutputStream(); - - transformer = new JavaScriptTransformDistributor(); - transformer.setContentType(JAVASCRIPT_TYPE); - transformer.setActionName(ACTION_NAME); - } - - // ---------------------------------------------------------------------- - // Basic tests - // ---------------------------------------------------------------------- - - @Test - public void scriptSuntaxError_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "syntax", - ScriptException.class, "but found"); - transformAndCheck("", BAD_SYNTAX_SCRIPT, ""); - } - - @Test - public void noTransformFunction_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "must have", - NoSuchMethodException.class, "transform"); - transformAndCheck("", WRONG_FUNCTION_SCRIPT, ""); - } - - @Test - public void childThrowsException_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "Child", - DataDistributorException.class, "forced"); - child = new TestDistributor(JAVASCRIPT_TYPE, "", true); - transformAndCheck(SIMPLE_SCRIPT, ""); - } - - @Test - public void wrongReturnType_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "must return a String"); - transformAndCheck("", WRONG_RETURN_TYPE_SCRIPT, ""); - } - - @Test - public void mostBasicTransform() throws DataDistributorException { - transformAndCheck("", SIMPLE_SCRIPT, SIMPLE_EXPECTED_RESULT); - } - - @Test - public void parseTransformAndStringify() throws DataDistributorException { - transformAndCheck(INITIAL_STRUCTURE, FULL_SCRIPT, - TRANSFORMED_STRUCTURE); - } - - /** - * This test is intended to check whether Unicode is handled properly - * regardless of the system's default file encoding. - * - * However there might be failures that only show up if the default value of - * the system property file.encoding is not UTF-8. - * - * The commented code is a hacky way of ensuring that the file encoding is - * not UTF-8, but in Java 9 or later, it will cause warning messages. - * - * So we have a test that might pass in some environments and fail in - * others. - */ - @Test - public void unicodeCharactersArePreserved() - throws DataDistributorException, UnsupportedEncodingException { - // try { - // System.setProperty("file.encoding", "ANSI_X3.4-1968"); - // Field charset = Charset.class.getDeclaredField("defaultCharset"); - // charset.setAccessible(true); - // charset.set(null, null); - // } catch (Exception e) { - // throw new RuntimeException(e); - // } - - child = new TestDistributor(TEXT_TYPE, UNICODE_STRING); - runTransformer(ECHO_SCRIPT); - assertEquals(UNICODE_STRING, - new String(outputStream.toByteArray(), "UTF-8")); - } - - // ---------------------------------------------------------------------- - // Tests with additional scripts - // ---------------------------------------------------------------------- - - @Test - public void additionalScriptNotFound_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "Can't locate"); - addScripts(PATH_TO_MISSING_SCRIPT); - transformAndCheck("", SIMPLE_SCRIPT, ""); - } - - @Test - public void additionalScriptSyntaxError_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, - PATH_TO_BAD_SYNTAX_SCRIPT, ScriptException.class, "but found"); - addScripts(PATH_TO_BAD_SYNTAX_SCRIPT); - transformAndCheck("", SIMPLE_SCRIPT, ""); - } - - @Test - public void useAdditionalScripts() throws DataDistributorException { - addScripts(PATH_TO_SUPPORTING_SCRIPT_1, PATH_TO_SUPPORTING_SCRIPT_2); - transformAndCheck("", SUPPORTED_SCRIPT, SUPPORTED_EXPECTED_RESULT); - } - - // ---------------------------------------------------------------------- - // Tests with logging - // ---------------------------------------------------------------------- - - @Test - public void loggingAtDebug_producedFourMessages() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.DEBUG, 4); - } - - @Test - public void loggingAtInfo_producesThreeMessages() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.INFO, 3); - } - - @Test - public void loggingAtWarn_producesTwoMessages() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.WARN, 2); - } - - @Test - public void loggingAtError_producesOneMessage() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.ERROR, 1); - } - - @Test - public void loggingAtOff_producesNoMessages() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.OFF, 0); - } - - // ---------------------------------------------------------------------- - // Helper methods - // ---------------------------------------------------------------------- - - private void transformAndCheck(String initial, String script, - String expected) throws DataDistributorException { - child = new TestDistributor(JAVASCRIPT_TYPE, initial); - transformAndCheck(script, expected); - } - - private void transformAndCheck(String script, String expected) - throws DataDistributorException { - runTransformer(script); - assertEquivalentJson(expected, new String(outputStream.toByteArray())); - } - - private void transformAndCountLogLines(String initial, String script, - Level level, int expectedCount) throws DataDistributorException { - setLoggerLevel(LOGGER_NAME, level); - logOutput = runTransformer(initial, script); - assertNumberOfLines(logOutput, expectedCount); - } - - private String runTransformer(String initial, String script) - throws DataDistributorException { - child = new TestDistributor(JAVASCRIPT_TYPE, initial); - return runTransformer(script); - } - - private String runTransformer(String script) - throws DataDistributorException { - try (Writer logCapture = new StringWriter()) { - captureLogOutput(LOGGER_NAME, logCapture, true); - - transformer.setScript(script); - transformer.setChild(child); - transformer.init(ddc); - transformer.writeOutput(outputStream); - - return logCapture.toString(); - } catch (IOException e) { - throw new DataDistributorException(e); - } - } - - private void addScripts(String... paths) { - for (String path : paths) { - transformer.addScriptPath(path); - } - } - - private void assertEquivalentJson(String expected, String actual) { - try { - JsonNode expectedNode = new ObjectMapper().readTree(expected); - JsonNode actualNode = new ObjectMapper().readTree(actual); - assertEquals(expectedNode, actualNode); - } catch (IOException e) { - throw new RuntimeException("Failed to compare JSON", e); - } - } - - private void assertNumberOfLines(String s, int expected) { - int actual = s.isEmpty() ? 0 : s.split("[\\n\\r]").length; - assertEquals("number of lines", expected, actual); - } - - /** - * AbstractTextClasss has this method, but is not overloaded to accept a - * String for the logger category. - * - * Capture the log for this class to this Writer. Choose whether or not to - * suppress it from the console. - */ - protected void captureLogOutput(String category, Writer writer, - boolean suppress) { - PatternLayout layout = new PatternLayout("%p %m%n"); - - ConsoleAppender appender = new ConsoleAppender(); - appender.setWriter(writer); - appender.setLayout(layout); - - Logger logger = Logger.getLogger(category); - logger.removeAllAppenders(); - logger.setAdditivity(!suppress); - logger.addAppender(appender); - } - - // ---------------------------------------------------------------------- - // Helper classes - // ---------------------------------------------------------------------- - - /** - * Just echoes a given string with a given contentType, or throws an - * Exception, if you prefer. - */ - private static class TestDistributor extends AbstractDataDistributor { - private String contentType; - private String outputString; - private boolean throwException; - - public TestDistributor(String contentType, String outputString) { - this(contentType, outputString, false); - } - - public TestDistributor(String contentType, String outputString, - boolean throwException) { - this.contentType = contentType; - this.outputString = outputString; - this.throwException = throwException; - } - - @Override - public String getContentType() throws DataDistributorException { - return contentType; - } - - @Override - public void writeOutput(OutputStream output) - throws DataDistributorException { - try { - if (throwException) { - throw new DataDistributorException("forced exception."); - } - output.write(outputString.getBytes("UTF-8")); - } catch (IOException e) { - throw new RuntimeException(); - } - } - - @Override - public void close() throws DataDistributorException { - // Nothing to do. - } - - } - -}