/* * Copyright 2013 Guidewire Software, Inc. */ package gw.test.servlet; import gw.config.CommonServices; import gw.lang.reflect.IConstructorInfo; import gw.lang.reflect.IParameterInfo; import gw.lang.reflect.IType; import gw.lang.reflect.ITypeInfo; import gw.lang.reflect.TypeSystem; import gw.lang.reflect.gs.IGosuClass; import gw.lang.reflect.java.IJavaType; import gw.test.TestClass; import gw.lang.reflect.IParameterInfo; import gw.lang.reflect.java.JavaTypes; import gw.test.TestEnvironment; import gw.test.TestMetadata; import gw.test.TestSpec; import gw.test.remote.ForwardingTestEnvironment; import gw.test.remote.RemoteTestResult; import gw.util.GosuStringUtil; import gw.lang.reflect.java.JavaTypes; import gw.test.TestClass; import gw.test.remote.RemoteTestResult; import gw.xml.simple.SimpleXmlNode; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.util.Collection; public class RemoteTestServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { handleRequest(request, response); } public static String TEST_INFO_COMMAND = "testInfo"; public static String RUN_TEST_METHOD_COMMAND = "runTestMethod"; public static String BEFORE_TEST_CLASS_COMMAND = "beforeTestClass"; public static String AFTER_TEST_CLASS_COMMAND = "afterTestClass"; public static String REFRESH_TYPES_COMMAND = "refreshTypes"; public static String PING = "ping"; public static String REDIRECT_SYSTEM_OUT_COMMAND = "redirectSystemOut"; public static String REDIRECT_SYSTEM_ERR_COMMAND = "redirectSystemErr"; public static String STOP_REDIRECTING_OUTPUT_COMMAND = "stopRedirectingOutput"; public static final String SET_UP_TEST_ENVIRONMENT_COMMAND = "setUpTestEnvironment"; public static final String TEAR_DOWN_TEST_ENVIRONMENT_COMMAND = "tearDownTestEnvironment"; protected void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { _originalOut.println("Incoming request for " + request.getPathInfo()); String[] pathComponents = GosuStringUtil.split(request.getPathInfo(), "/"); if (!handleRequest(pathComponents, response)) { throw new IllegalArgumentException("Unrecognized path " + request.getContextPath()); } } protected boolean handleRequest(String[] pathComponents, HttpServletResponse response) { String lastComponent = pathComponents[pathComponents.length - 1]; if (lastComponent.equals(TEST_INFO_COMMAND)) { handleTestInfoRequest(pathComponents, response); } else if (lastComponent.equals(RUN_TEST_METHOD_COMMAND)) { handleRunTestMethodRequest(pathComponents, response); } else if (lastComponent.equals(BEFORE_TEST_CLASS_COMMAND)) { handleBeforeTestClassRequest(pathComponents, response); } else if (lastComponent.equals(AFTER_TEST_CLASS_COMMAND)) { handleAfterTestClassRequest(pathComponents, response); } else if (lastComponent.equals(REFRESH_TYPES_COMMAND)) { handleRefreshTypesRequest(response); } else if (lastComponent.equals(PING)) { handlePingRequest(response); } else if (lastComponent.equals(REDIRECT_SYSTEM_OUT_COMMAND)) { handleRedirectSystemOutRequest(response); } else if (lastComponent.equals(REDIRECT_SYSTEM_ERR_COMMAND)) { handleRedirectSystemErrRequest(response); } else if (lastComponent.equals(STOP_REDIRECTING_OUTPUT_COMMAND)) { handleStopRedirectingOutputRequest(response); } else if (lastComponent.equals(SET_UP_TEST_ENVIRONMENT_COMMAND)) { handleSetUpTestEnvironmentRequest(pathComponents, response); } else if (lastComponent.equals(TEAR_DOWN_TEST_ENVIRONMENT_COMMAND)) { handleTearDownTestEnvironmentRequest(pathComponents, response); } else { // Return false if the request falls through and isn't handled return false; } // Return true if any other branch was taken, meaning that something handled it return true; } private void handleTestInfoRequest(String[] pathComponents, HttpServletResponse response) { String typeName = constructTypeName(pathComponents, 0, pathComponents.length - 1); String testInfoResults; try { testInfoResults = constructTestInfo(typeName); } catch (Throwable t) { t.printStackTrace(); // If there's an error, return an empty test info with no methods, rather than just nothing at all // TODO - AHK - Return additional error information back SimpleXmlNode classInfoNode = new SimpleXmlNode("TestClassInfo"); classInfoNode.getAttributes().put("name", typeName); testInfoResults = classInfoNode.toXmlString(); } try { response.setContentType("text/html; charset=UTF8"); PrintWriter out = response.getWriter(); out.print(testInfoResults); out.close(); } catch (IOException e) { e.printStackTrace(); } } private String constructTestInfo(String typeName) { IType testType = TypeSystem.getByFullName(typeName); // If the type doesn't parse, then we need to bail out if (!testType.isValid()) { if (testType instanceof IGosuClass) { throw new RuntimeException(((IGosuClass)testType).getParseResultsException()); } else { throw new IllegalStateException("Test type " + testType + " is not valid."); } } String[] methodNames = TestSpec.extractTestMethods(testType); // TODO - AHK - Should be consolidated with code in TestClassWrapper ITypeInfo typeInfo = testType.getTypeInfo(); IConstructorInfo noArgCons = typeInfo.getConstructor(); TestClass test; if (noArgCons != null) { test = (TestClass) noArgCons.getConstructor().newInstance(); test.createClassMetadata(); } else { IConstructorInfo oneArgCons = typeInfo.getConstructor(JavaTypes.STRING()); if (oneArgCons != null) { test = (TestClass) oneArgCons.getConstructor().newInstance("dummy"); test.createClassMetadata(); } else { throw new IllegalStateException("Test type " + testType + " does not have either a no-arg constructor or a one-arg constructor taking a String"); } } SimpleXmlNode classInfoNode = new SimpleXmlNode("TestClassInfo"); classInfoNode.getAttributes().put("name", typeName); Collection<TestMetadata> classMetadata = test.createClassMetadata(); for (String methodName : methodNames) { SimpleXmlNode methodInfoNode = new SimpleXmlNode("TestMethodInfo"); methodInfoNode.getAttributes().put("name", methodName); classInfoNode.getChildren().add(methodInfoNode); for (TestMetadata md : classMetadata) { methodInfoNode.getChildren().add(md.serializeToXml()); } Collection<TestMetadata> methodMetadata = test.createMethodMetadata(methodName); for (TestMetadata md : methodMetadata) { methodInfoNode.getChildren().add(md.serializeToXml()); } } return classInfoNode.toXmlString(); } private void handleBeforeTestClassRequest(String[] pathComponents, HttpServletResponse response) { handleTestClassMethod(pathComponents, pathComponents.length - 1, response, new TestClassCallback() { @Override public void invoke(TestClass testClass) { testClass.beforeTestClass(); } }); } private void handleAfterTestClassRequest(String[] pathComponents, HttpServletResponse response) { handleTestClassMethod(pathComponents, pathComponents.length - 1, response, new TestClassCallback() { @Override public void invoke(TestClass testClass) { testClass.afterTestClass(); } }); } private void handleRunTestMethodRequest(final String[] pathComponents, HttpServletResponse response) { handleTestClassMethod(pathComponents, pathComponents.length - 2, response, new TestClassCallback() { @Override public void invoke(TestClass testClass) throws Throwable { // TODO - AHK - This should reuse more of the code from TestClassWrapper String methodName = pathComponents[pathComponents.length - 2]; testClass.setName(methodName); testClass.initMetadata(methodName); callTestMethod(testClass); } }); } private void callTestMethod(TestClass testClass) throws Throwable { // TODO - AHK - This should probably just work through the TestExecutionManager testClass.beforeTestMethod(); try { testClass.reallyRunBare(); } catch (Throwable e) { testClass.afterTestMethod(e); throw e; } testClass.afterTestMethod(null); } private interface TestClassCallback { void invoke(TestClass testClass) throws Throwable; } private void handleTestClassMethod(String[] pathComponents, int lastNameIndex, HttpServletResponse response, TestClassCallback callback) { RemoteTestResult testResult = new RemoteTestResult(); try { String typeName = constructTypeName(pathComponents, 0, lastNameIndex); TestClass testClass = createTestClassInstance(typeName); callback.invoke(testClass); } catch (Throwable t) { testResult.setException(t); } sendRemoteTestResult(response, testResult); } private String constructTypeName(String[] components, int start, int end) { StringBuilder typeName = new StringBuilder(); for (int i = start; i < end; i++) { if (i > start) { typeName.append("."); } typeName.append(components[i]); } return typeName.toString(); } private TestClass createTestClassInstance(String typeName) { IType testType = TypeSystem.getByFullName(typeName); if( !(testType instanceof IJavaType) ){ try { if( !(testType instanceof IGosuClass) || ((IGosuClass)testType).getBackingClass() != getTestClass( typeName, testType ) ) { // So we have a java class and an non-java class. Go boom throw(new IllegalStateException("Type " + typeName + " exists as both a java type and a non-java type.")); } } catch (ClassNotFoundException e) { // This is fine } } return TestClass.createTestClass(testType); } private Class<?> getTestClass( String typeName, IType testType ) throws ClassNotFoundException { return Class.forName( typeName, false, testType.getTypeLoader().getModule().getModuleTypeLoader().getDefaultTypeLoader().getGosuClassLoader().getActualLoader() ); } private void sendRemoteTestResult(HttpServletResponse response, RemoteTestResult testResult) { try { response.setContentType("text/html; charset=UTF8"); PrintWriter out = response.getWriter(); out.print(testResult.toXML()); out.close(); } catch (IOException e) { e.printStackTrace(); } } private void handleRefreshTypesRequest(HttpServletResponse response) { try { ChangedTypesRefresher.getInstance().reloadChangedTypes(); response.getOutputStream().close(); } catch (IOException e) { e.printStackTrace(); } catch (Throwable t) { t.printStackTrace(); } } private void handlePingRequest(HttpServletResponse response) { try { response.getOutputStream().close(); } catch (IOException e) { e.printStackTrace(); } } protected void handleSetUpTestEnvironmentRequest(String[] pathComponents, HttpServletResponse response) { TestEnvironment testEnvironment = createTestEnvironment(pathComponents); boolean success; try { testEnvironment.beforeRemoteExecution(); success = true; } catch (Exception e) { e.printStackTrace(); success = false; } try { response.setContentType("text/html; charset=UTF8"); PrintWriter out = response.getWriter(); out.print(success); out.close(); } catch (IOException e) { e.printStackTrace(); } } protected void handleTearDownTestEnvironmentRequest(String[] pathComponents, HttpServletResponse response) { TestEnvironment testEnvironment = createTestEnvironment(pathComponents); boolean success; try { testEnvironment.afterRemoteExecution(); success = true; } catch (Exception e) { e.printStackTrace(); success = false; } try { response.setContentType("text/html; charset=UTF8"); PrintWriter out = response.getWriter(); out.print(success); out.close(); } catch (IOException e) { e.printStackTrace(); } } private TestEnvironment createTestEnvironment(String[] pathComponents) { // TODO - AHK - Error handling // TODO - AHK - Validate that it's actually a TestEnvironment // TODO - AHK - Handle the case where there's no appropriate constructor String typeName = constructTypeName(pathComponents, 0, pathComponents.length - 2); String[] constructorArgs = splitArgs(pathComponents[pathComponents.length - 2]); IType testEnvironmentType = TypeSystem.getByFullName(typeName); TestEnvironment testEnvironment = null; for (IConstructorInfo cons : testEnvironmentType.getTypeInfo().getConstructors()) { if (cons.getParameters().length == constructorArgs.length) { Object[] convertedArgs = convertConstructorArgs(constructorArgs, cons.getParameters()); testEnvironment = (TestEnvironment) cons.getConstructor().newInstance(convertedArgs); break; } } return testEnvironment; } private String[] splitArgs(String constructorArgs) { if (constructorArgs.equals(ForwardingTestEnvironment.NO_ARGS_STRING)) { return new String[0]; } else { return GosuStringUtil.split(constructorArgs, ","); } } private Object[] convertConstructorArgs(String[] constructorArgs, IParameterInfo[] params) { Object[] results = new Object[constructorArgs.length]; for (int i = 0; i < constructorArgs.length; i++) { results[i] = convertConstructorArg(constructorArgs[i], params[i].getFeatureType()); } return results; } private Object convertConstructorArg(String arg, IType targetType) { if (targetType.equals(JavaTypes.BOOLEAN()) || targetType.equals(JavaTypes.pBOOLEAN())) { return Boolean.valueOf(arg); } else { throw new IllegalArgumentException("Unhandled type TestEnvironment constructor argument type " + targetType); } } private static PrintStream _originalOut = System.out; private static PrintStream _originalErr = System.err; protected void handleRedirectSystemOutRequest(HttpServletResponse response) { redirectOutput(response, new SystemOutInfo()); } private void handleRedirectSystemErrRequest(HttpServletResponse response) { redirectOutput(response, new SystemErrInfo()); } private void handleStopRedirectingOutputRequest(HttpServletResponse response) { stopRedirectingOutput(new SystemOutInfo()); stopRedirectingOutput(new SystemErrInfo()); try { response.getOutputStream().close(); } catch (IOException e) { e.printStackTrace(); } } private void stopRedirectingOutput(StreamInfo streamInfo) { if (!streamInfo.isCurrentStream(streamInfo.getOriginalStream())) { streamInfo.setStream(streamInfo.getOriginalStream()); afterStreamRedirected(streamInfo); } } protected static abstract class StreamInfo { public abstract PrintStream getOriginalStream(); public abstract void setStream(PrintStream newStream); public abstract boolean isCurrentStream(PrintStream stream); } protected static class SystemOutInfo extends StreamInfo { @Override public PrintStream getOriginalStream() { return _originalOut; } @Override public void setStream(PrintStream newStream) { System.setOut(newStream); } @Override public boolean isCurrentStream(PrintStream stream) { return System.out == stream; } } protected static class SystemErrInfo extends StreamInfo { @Override public PrintStream getOriginalStream() { return _originalErr; } @Override public void setStream(PrintStream newStream) { System.setErr(newStream); } @Override public boolean isCurrentStream(PrintStream stream) { return System.err == stream; } } private void redirectOutput(HttpServletResponse response, StreamInfo streamInfo) { // TODO - AHK - Soooo . . . this is a total hack to keep the thread handling this alive. Is there some // better way to do this? How does this ever get closed? try { RedirectingPrintStream redirectingStream = new RedirectingPrintStream(response.getOutputStream(), streamInfo.getOriginalStream()); streamInfo.setStream(redirectingStream); afterStreamRedirected(streamInfo); while (true) { try { Thread.sleep(500); // We want this thread to die if either A) the system output has been directed elsewhere or B) // there's been an error in the redirected stream, which generally means that the socket has closed // on the other end. If we're exiting while the output is still redirected, we want to change it back if (!streamInfo.isCurrentStream(redirectingStream) || redirectingStream.hasOutputStreamError()) { if (streamInfo.isCurrentStream(redirectingStream)) { streamInfo.setStream(streamInfo.getOriginalStream()); afterStreamRedirected(streamInfo); } break; } } catch (InterruptedException e) { // Ignore } } } catch (IOException e) { // TODO - Handle this better somehow? e.printStackTrace(); } } protected void afterStreamRedirected(StreamInfo streamInfo) { } private static class RedirectingPrintStream extends PrintStream { private PrintStream _s1; private PrintStream _s2; private RedirectingPrintStream(OutputStream out, PrintStream originalStream) { super(out, true); _s1 = new PrintStream(out, true); _s2 = originalStream; } public boolean hasOutputStreamError() { return _s1.checkError(); } @Override public void flush() { _s1.flush(); _s2.flush(); } @Override public void close() { _s1.close(); _s2.close(); } @Override public void write(int b) { _s1.write(b); _s2.write(b); } @Override public void write(byte[] buf, int off, int len) { _s1.write(buf, off, len); _s2.write(buf, off, len); } @Override public void print(boolean b) { _s1.print(b); _s2.print(b); } @Override public void print(char c) { _s1.print(c); _s2.print(c); } @Override public void print(int i) { _s1.print(i); _s2.print(i); } @Override public void print(long l) { _s1.print(l); _s2.print(l); } @Override public void print(float f) { _s1.print(f); _s2.print(f); } @Override public void print(double d) { _s1.print(d); _s2.print(d); } @Override public void print(char[] s) { _s1.print(s); _s2.print(s); } @Override public void print(String s) { _s1.print(s); _s2.print(s); } @Override public void print(Object obj) { _s1.print(obj); _s2.print(obj); } @Override public void println() { _s1.println(); _s2.println(); } @Override public void println(boolean x) { _s1.println(x); _s2.println(x); } @Override public void println(char x) { _s1.println(x); _s2.println(x); } @Override public void println(int x) { _s1.println(x); _s2.println(x); } @Override public void println(long x) { _s1.println(x); _s2.println(x); } @Override public void println(float x) { _s1.println(x); _s2.println(x); } @Override public void println(double x) { _s1.println(x); _s2.println(x); } @Override public void println(char[] x) { _s1.println(x); _s2.println(x); } @Override public void println(String x) { _s1.println(x); _s2.println(x); } @Override public void println(Object x) { _s1.println(x); _s2.println(x); } @Override public void write(byte[] b) throws IOException { _s1.write(b); _s2.write(b); } } }