diff options
Diffstat (limited to '')
-rw-r--r-- | infrastructure/net.appjet.oui/execution.scala | 660 |
1 files changed, 660 insertions, 0 deletions
diff --git a/infrastructure/net.appjet.oui/execution.scala b/infrastructure/net.appjet.oui/execution.scala new file mode 100644 index 0000000..dc17c29 --- /dev/null +++ b/infrastructure/net.appjet.oui/execution.scala @@ -0,0 +1,660 @@ +/** + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.appjet.oui; + +import java.net.URLDecoder; +import java.util.Enumeration; +import java.util.concurrent.atomic.AtomicLong; + +import javax.servlet.http.{HttpServletRequest, HttpServletResponse, HttpServlet}; + +import scala.collection.mutable.{ListBuffer, LinkedHashSet, HashMap, ArrayBuffer}; +import scala.collection.immutable.Map; +import scala.collection.jcl.Conversions; + +import org.mozilla.javascript.{Scriptable, Context, Function, ScriptableObject, JavaScriptException}; +import org.mortbay.jetty.RetryRequest; + +import net.appjet.bodylock.{BodyLock, Executable, JSRuntimeException, JSCompileException}; +import net.appjet.common.util.{HttpServletRequestFactory, BetterFile}; + +import Util.enumerationToRichEnumeration; + +// Removed due to licensing issues; REMOVED_COS_OF_COS +// import com.oreilly.servlet.MultipartFilter; + +class RequestWrapper(val req: HttpServletRequest) { + req.setCharacterEncoding("UTF-8"); +// REMOVED_COS_OF_COS ... ? +// private lazy val parameterNames = +// (for (i <- Conversions.convertSet(req.getParameterMap.keySet().asInstanceOf[java.util.Set[String]])) yield i).toList.toArray +// private def parameterValues(k: String) = req.getParameterValues(k); + def headerCapitalize(s: String) = + s.split("-").map( + s => + if (s == null || s.length < 1) s + else s.substring(0, 1).toUpperCase()+s.substring(1).toLowerCase() + ).mkString("-"); + def isFake = false; + lazy val path = req.getRequestURI(); + lazy val host = { + val hostFromHeader = req.getHeader("Host"); + if ((hostFromHeader ne null) && hostFromHeader.indexOf(':') >= 0) { + // fix the port, which may be wrong in Host header (e.g. IE 6) + hostFromHeader.substring(0, hostFromHeader.indexOf(':')) + ":" + + req.getLocalPort; + } + else { + hostFromHeader; + } + } + lazy val query = req.getQueryString(); + lazy val method = req.getMethod(); + lazy val scheme = req.getScheme(); + lazy val clientAddr = req.getRemoteAddr(); + + def decodeWwwFormUrlencoded(content: => String): Map[String, Array[String]] = { + val map = new HashMap[String, ArrayBuffer[String]]; + if (content != null) { + for (pair <- content.split("&").map(_.split("=", 2))) { + val key = URLDecoder.decode(pair(0), "UTF-8"); + val list = map.getOrElseUpdate(key, new ArrayBuffer[String]); + if (pair.length > 1) { + list += URLDecoder.decode(pair(1), "UTF-8"); + } + } + } + Map((for ((k, v) <- map) yield (k, v.toArray)).toSeq: _*); + } + + def postParams = decodeWwwFormUrlencoded(content.asInstanceOf[String]); + def getParams = decodeWwwFormUrlencoded(query); + + lazy val params_i = { + if (contentType != null && contentType.startsWith("application/x-www-form-urlencoded")) { + if (req.getAttribute("ajcache_parameters") == null) { + req.setAttribute("ajcache_parameters", + Map((for (k <- (postParams.keys ++ getParams.keys).toList) + yield (k, postParams.getOrElse(k, Array[String]()) ++ + getParams.getOrElse(k, Array[String]()))).toSeq: _*)); + } + req.getAttribute("ajcache_parameters").asInstanceOf[Map[String, Array[String]]]; + } else { + Conversions.convertMap(req.getParameterMap().asInstanceOf[java.util.Map[String, Array[String]]]); + } + } + + def params(globalScope: Scriptable) = new ScriptableFromMapOfStringArrays( + globalScope, + params_i.keys.toList, + params_i.get(_), + false); + def headers(globalScope: Scriptable) = new ScriptableFromMapOfStringArrays( + globalScope, + req.getHeaderNames().asInstanceOf[Enumeration[String]] + .map(headerCapitalize).toList, + h => h match { + case "Host" => Some(Array(host)); + case hh => Some(Util.enumerationToArray(req.getHeaders(headerCapitalize(hh)).asInstanceOf[Enumeration[String]])) }, + true); + lazy val protocol = req.getProtocol(); + lazy val contentType = req.getHeader("Content-Type"); + lazy val postParamsInBody = contentType != null && contentType.startsWith("application/x-www-form-urlencoded"); + lazy val content = + if ((contentType != null && contentType.startsWith("text/")) || postParamsInBody) { + val reader = req.getReader(); + if (reader != null) + BetterFile.getReaderString(req.getReader()); + else + null; + } else { + val stream = req.getInputStream(); + if (stream != null) + BetterFile.getStreamBytes(req.getInputStream()); + else + null; + } + + // Depends on cos.jar; REMOVED_COS_OF_COS + def files(globalScope: Scriptable): Object = { +// if (! req.isInstanceOf[com.oreilly.servlet.MultipartWrapper]) { + new ScriptableAdapter(); +// } else { +// val r = req.asInstanceOf[com.oreilly.servlet.MultipartWrapper]; +// val fileScriptables = new HashMap[String, Scriptable](); +// val fileBytes = new HashMap[String, Array[byte]](); +// new ScriptableFromMapOfScriptableArrays(globalScope, +// r.getFileNames().asInstanceOf[Enumeration[String]].toList, +// name => { +// if (r.getFile(name) == null) +// None +// else +// Some(Array(fileScriptables.getOrElseUpdate(name, +// new ScriptableFromMapOfArrays[Object](globalScope, +// Set("contentType", "filesystemName", "bytes").toSeq, +// _ match { +// case "contentType" => Some(Array(r.getContentType(name))); +// case "filesystemName" => +// Some(Array(r.getFilesystemName(name))); +// case "bytes" => +// Some(Array(Context.javaToJS(fileBytes.getOrElseUpdate(name, +// BetterFile.getFileBytes(r.getFile(name))), globalScope))); +// case _ => None; +// }, +// true)))) +// }, +// true); +// } + } +} + +class ResponseWrapper(val res: HttpServletResponse) { + private lazy val outputStrings = new ListBuffer[String]; + private lazy val outputBytes = new ListBuffer[Array[byte]]; + private var statusCode = 200; + private var contentType = "text/html"; + private var redirect: String = null; + private lazy val headers = new LinkedHashSet[(String, String, HttpServletResponse => Unit)] { + def removeAll(k: String) { + this.foreach(x => if (x._1 == k) remove(x)); + } + } + + private[oui] def overwriteOutputWithError(code: Int, errorStr: String) { + statusCode = code; + outputStrings.clear(); + outputStrings += errorStr; + outputBytes.clear(); + headers.clear(); + Util.noCacheHeaders.foreach(x => headers += (x._1, x._2, res => res.setHeader(x._1, x._2))); + redirect = null; + contentType = "text/html; charset=utf-8"; + } + + def reset() { + outputStrings.clear(); + outputBytes.clear(); + redirect = null; + headers.clear(); + Util.noCacheHeaders.foreach(x => headers += (x._1, x._2, res => res.setHeader(x._1, x._2))); + statusCode = 200; + contentType = "text/html; charset=utf-8"; + } + def error(code: Int, errorStr: String) { + overwriteOutputWithError(code, errorStr); + stop(); + } + def stop() { + throw AppGeneratedStopException; + } + + def write(s: String) { + outputStrings += s; + } + def getOutput() = outputStrings.mkString(""); + def writeBytes(bytes: String) { + val a = new Array[byte](bytes.length()); + bytes.getBytes(0, bytes.length(), a, 0); + outputBytes += a; + } + def writeBytes(bytes: Array[Byte]) { + outputBytes += bytes; + } + def getOutputBytes() = outputBytes.flatMap(x => x).toArray + def setContentType(s: String) { + contentType = s; + } + def getCharacterEncoding() = { + res.setContentType(contentType); + res.getCharacterEncoding(); + } + def setStatusCode(sc: Int) { + statusCode = sc; + } + def getStatusCode() = statusCode; + def redirect(loc: String) { + statusCode = 302; + redirect = loc; + stop(); + } + def setHeader(name: String, value: String) { + headers += ((name, value, res => res.setHeader(name, value))); + } + def addHeader(name: String, value: String) { + headers += ((name, value, res => res.addHeader(name, value))); + } + def getHeader(name: String) = { + headers.filter(_._1 == name).map(_._2).toSeq.toArray; + } + def removeHeader(name: String) { + headers.removeAll(name); + } + + var gzipOutput = false; + def setGzip(gzip: Boolean) { + gzipOutput = gzip; + } + + def print() { + if (redirect != null && statusCode == 302) { + headers.foreach(_._3(res)); + res.sendRedirect(redirect); + } else { + res.setStatus(statusCode); + res.setContentType(contentType); + headers.foreach(_._3(res)); + if (gzipOutput) res.setHeader("Content-Encoding", "gzip"); + if (outputStrings.length > 0) { + var bytes: Seq[Array[Byte]] = outputStrings.map(_.getBytes(res.getCharacterEncoding())); + if (gzipOutput) bytes = List(Util.gzip(Array.concat(bytes:_*))); + res.setContentLength((bytes :\ 0) {_.length + _}); + bytes.foreach(res.getOutputStream.write(_)); + } else if (outputBytes.length > 0) { + var bytes: Seq[Array[Byte]] = outputBytes; + if (gzipOutput) bytes = List(Util.gzip(Array.concat(bytes:_*))); + res.setContentLength((bytes :\ 0) {_.length + _}); + bytes.foreach(res.getOutputStream.write(_)); + } + } + } +} + +class ScriptableAdapter extends Scriptable { + private def unsupported() = throw UnsupportedOperationException; + def delete(index: Int) { unsupported(); } + def delete(name: String) { unsupported(); } + def get(index: Int, start: Scriptable): Object = Context.getUndefinedValue(); + def get(name: String, start: Scriptable): Object = Context.getUndefinedValue(); + def getClassName() = getClass.getName(); + def getDefaultValue(hint: Class[_]) = "[ScriptableAdapter]"; + def getIds(): Array[Object] = Array[Object](); + def getParentScope: Scriptable = null; + def getPrototype: Scriptable = null; + def has(index: Int, start: Scriptable): Boolean = false; + def has(name: String, start: Scriptable): Boolean = false; + def hasInstance(instance: Scriptable): Boolean = false; + def put(index: Int, start: Scriptable, value: Object) { unsupported(); } + def put(name: String, start: Scriptable, value: Object) { unsupported(); } + def setParentScope(parent: Scriptable) { unsupported(); } + def setPrototype(prototype: Scriptable) { unsupported(); } +} + +class ScriptableFromMapOfStringArrays(globalScope: Scriptable, + keys: Seq[String], values: String => Option[Array[String]], + zeroMeansNone: Boolean) extends ScriptableFromMapOfArrays[String]( + globalScope, keys, values, zeroMeansNone); + +class ScriptableFromMapOfScriptableArrays(globalScope: Scriptable, + keys: Seq[String], values: String => Option[Array[Scriptable]], + zeroMeansNone: Boolean) extends ScriptableFromMapOfArrays[Scriptable]( + globalScope, keys, values, zeroMeansNone); + + +class ScriptableFromMapOfArrays[V <: Object](globalScope: Scriptable, + keys: Seq[String], values: String => Option[Array[V]], + zeroMeansNone: Boolean) extends ScriptableAdapter { + override def get(n: String, start: Scriptable): Object = { + val v = values(n); + if (v.isEmpty || (zeroMeansNone && v.get.length == 0)) { + Context.getUndefinedValue(); + } else if (v.get.length == 1) { + v.get.apply(0); + } else { + Context.getCurrentContext().newArray(globalScope, v.get.map(x => x.asInstanceOf[Object])); + } + } + override def getIds(): Array[Object] = keys.toArray[Object]; + override def getPrototype = ScriptableObject.getObjectPrototype(globalScope); + override def has(n: String, start: Scriptable): Boolean = ! (values(n).isEmpty || (zeroMeansNone && values(n).get.length == 0)); +} + +object AppGeneratedStopException extends JSRuntimeException("User-generated stop.", null); +class NoHandlerException(msg: String) extends JSRuntimeException(msg, null); +object UnsupportedOperationException extends JSRuntimeException("Unsupported operation.", null); + +object ExecutionContextUtils { + val uniqueIds = new AtomicLong(0); + + val ecVar = new NoninheritedDynamicVariable[ExecutionContext](null); + def withContext[E](ec: ExecutionContext)(block: => E): E = { + ecVar.withValue(ec)(block); + } + + def currentContext = ecVar.value; +} + +case class ExecutionContext( + val request: RequestWrapper, + val response: ResponseWrapper, + var runner: ScopeReuseManager.Runner) { + val asyncs = new ListBuffer[Function]; + lazy val attributes = new HashMap[String, Any]; + var completed = false; + lazy val executionId = ""+ExecutionContextUtils.uniqueIds.incrementAndGet(); + var result: AnyRef = null; +} + +object CometSupport { + trait CometHandler { + def handleCometRequest(req: HttpServletRequest, res: HttpServletResponse); + } + var cometHandler: CometHandler = null; +} + +class OuiServlet extends HttpServlet { + override def doGet(req: HttpServletRequest, res: HttpServletResponse) { + execute(req, res); + } + + override def doPost(req: HttpServletRequest, res: HttpServletResponse) { + execute(req, res); + } + + override def doHead(req: HttpServletRequest, res: HttpServletResponse) { + execute(req, res); + } + + override def doPut(req: HttpServletRequest, res: HttpServletResponse) { + execute(req, res); + } + + override def doDelete(req: HttpServletRequest, res: HttpServletResponse) { + execute(req, res); + } + + override def doTrace(req: HttpServletRequest, res: HttpServletResponse) { + execute(req, res); + } + + override def doOptions(req: HttpServletRequest, res: HttpServletResponse) { + execute(req, res); + } + + def execute(req: HttpServletRequest, res: HttpServletResponse) { + if (req.getProtocol() == "HTTP/1.1" && req.getHeader("Host") == null) { + res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid HTTP/1.1 request: No \"Host\" header found."); + } else if (config.transportPrefix != null && req.getRequestURI().startsWith(config.transportPrefix)) { + val runner = ScopeReuseManager.getRunner; + val ec = new ExecutionContext(new RequestWrapper(req), new ResponseWrapper(res), runner); + req.setAttribute("executionContext", ec); + req.setAttribute("isServerPushConnection", true); + try { + CometSupport.cometHandler.handleCometRequest(req, res); + } catch { + case e: RetryRequest => { + ec.runner = null; + ScopeReuseManager.freeRunner(runner); + throw e; + } + case _ => {}; + } + try { + ec.response.print(); + execution.onprint(ec, BodyLock.subScope(runner.mainScope)); + } finally { + ec.runner = null; + ScopeReuseManager.freeRunner(runner); + } + } else { + execution.execute(req, res); + } + } +} + +object execution { + // maybe find a better place for this? + { // initialize ajstdlib + val c = Class.forName("net.appjet.ajstdlib.ajstdlib$"); + val m = c.getDeclaredMethod("init"); + val o = c.getDeclaredField("MODULE$"); + m.invoke(o.get(null)); + } + + val requestLib = new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "onrequest.js")); + val errorLib = new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "onerror.js")); + val printLib = new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "onprint.js")); + val syntaxErrorLib = new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "syntaxerror.js")); + val onSyntaxErrorLib = new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "onsyntaxerror.js")); + val sarsLib = new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "onsars.js")); + val scheduledTaskLib = new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "onscheduledtask.js")); + def requestExecutable = requestLib.executable; + def errorExecutable = errorLib.executable; + def printExecutable = printLib.executable; + def syntaxErrorExecutable = syntaxErrorLib.executable; + def onSyntaxErrorExecutable = onSyntaxErrorLib.executable; + def sarsExecutable = sarsLib.executable; + def scheduledTaskExecutable = scheduledTaskLib.executable; + + def postSuccessfulRun(ec: ExecutionContext) { + try { + for (f <- ec.asyncs) { + BodyLock.runInContext({ cx => + f.call(cx, f.getParentScope(), ec.runner.mainScope, Array[Object]()); + }); + } + } catch { + case e => exceptionlog(e); + } + } + + def onprint(ec: ExecutionContext, scope: Scriptable) { + try { +// ec.runner.globalScope.put("_appjetcontext_", ec.runner.globalScope, ec); + printExecutable.execute(scope); + } catch { + case e => { exceptionlog(e); } // shrug. this was best-effort anyway. + } + } + + def execute(req: HttpServletRequest, res: HttpServletResponse) { + val runner = try { + ScopeReuseManager.getRunner; + } catch { + case e: JSCompileException => { + val r = ScopeReuseManager.getEmpty { r => + syntaxErrorExecutable.execute(r.globalScope) + } + val ec = ExecutionContext(new RequestWrapper(req), new ResponseWrapper(res), r); +// r.globalScope.put("_appjetcontext_", r.globalScope, ec); + ExecutionContextUtils.withContext(ec) { + ec.attributes("error") = e; + ec.result = onSyntaxErrorExecutable.execute(r.globalScope); + ec.response.print(); + } + return; + } + } + val ec = ExecutionContext(new RequestWrapper(req), new ResponseWrapper(res), runner); + val startTime = executionlatencies.time; + execute(ec, + (sc: Int, msg: String) => { + ec.response.overwriteOutputWithError(sc, msg); + }, + () => { executionlatencies.log(Map( + "time" -> (executionlatencies.time - startTime))); + ec.response.print() }, + () => { ScopeReuseManager.freeRunner(runner) }, + None); + } + + def errorToHTML(e: Throwable) = { + val trace = new java.io.StringWriter(); + e.printStackTrace(new java.io.PrintWriter(trace)); + trace.toString().split("\n").mkString("<br>\n"); + } + def execute(ec: ExecutionContext, + errorHandler: (Int, String) => Unit, + doneWritingHandler: () => Unit, + completedHandler: () => Unit, + customExecutable: Option[Executable]) = + ExecutionContextUtils.withContext(ec) { +// ec.runner.globalScope.put("_appjetcontext_", ec.runner.globalScope, ec); + val runScope = BodyLock.subScope(ec.runner.mainScope); + try { + ec.result = customExecutable.getOrElse(requestExecutable).execute(runScope); + ec.completed = true; + } catch { + case AppGeneratedStopException => { ec.completed = true; } + case e: NoHandlerException => errorHandler(500, "No request handler is defined."); + case e: RetryRequest => { completedHandler(); throw e; } + case e => { + ec.attributes("error") = e; + try { + ec.result = errorExecutable.execute(runScope); + } catch { + case AppGeneratedStopException => { } + case nhe: NoHandlerException => { + exceptionlog(e); + e.printStackTrace(); + errorHandler(500, "An error occurred and no error handler is defined."); + } + case e2 => { + exceptionlog(e); exceptionlog(e2); + val etext = e2 match { + case jse: JavaScriptException => { (jse.getValue() match { + case ne: org.mozilla.javascript.IdScriptableObject => ne.get("message", ne) + case e => e.getClass.getName + }) + "<br>\n" + errorToHTML(jse); } + case _ => errorToHTML(e2); + } + errorHandler( + 500, + "You like apples? An error occurred in the error handler while handling an error. How do you like <i>them</i> apples?<br>\n"+ + etext+"<br>\nCaused by:<br>\n"+errorToHTML(e)); + } + } + } + } + onprint(ec, runScope); + doneWritingHandler(); + if (ec.completed && ! ec.asyncs.isEmpty) { + main.server.getThreadPool().dispatch(new Runnable { + def run() { + postSuccessfulRun(ec); + completedHandler(); + } + }); + } else { + completedHandler(); + } + } + + def runOutOfBandSimply(executable: Executable, + props: Option[Map[String, Any]]) = { + // there must be a context already. + val currentContext = ExecutionContextUtils.currentContext; + val request = + if (currentContext != null) { + currentContext.request; + } else { + val fakeHeaders = scala.collection.jcl.Conversions.convertMap( + new java.util.HashMap[String, String]); + fakeHeaders("Host") = "unknown.local"; + new RequestWrapper(HttpServletRequestFactory.createRequest( + "/", fakeHeaders.underlying, "GET", null)) { + override val isFake = true; + } + } + val response = + if (currentContext != null && currentContext.response != null) { + currentContext.response; + } else { + new ResponseWrapper(null); + } + val runner = + if (currentContext != null) { + (false, currentContext.runner); + } else { + (true, ScopeReuseManager.getRunner); + } + val ec = new ExecutionContext(request, response, runner._2) + if (props.isDefined) { + for ((k, v) <- props.get) { + ec.attributes(k) = v; + } + } + try { + ExecutionContextUtils.withContext(ec) { + executable.execute(BodyLock.subScope(ec.runner.mainScope)); + } + } finally { + if (runner._1) { + ScopeReuseManager.freeRunner(runner._2); + } + } + } + + def runOutOfBand(executable: Executable, name: String, + props: Option[Map[String, Any]], + onFailure: Any => Unit) = { + var ec: ExecutionContext = null; + try { + val runner = ScopeReuseManager.getRunner; + val currentContext = ExecutionContextUtils.currentContext; + val request = + if (currentContext != null) { + currentContext.request; + } else { + val fakeHeaders = scala.collection.jcl.Conversions.convertMap( + new java.util.HashMap[String, String]); + fakeHeaders("Host") = "unknown.local"; + new RequestWrapper(HttpServletRequestFactory.createRequest( + "/", fakeHeaders.underlying, "GET", null)) { + override val isFake = true; + } + } + val response = + if (currentContext != null && currentContext.response != null) { + new ResponseWrapper(currentContext.response.res); + } else { + new ResponseWrapper(null); + } + ec = new ExecutionContext(request, response, runner); + if (props.isDefined) + for ((k, v) <- props.get) { + ec.attributes(k) = v; + } + execution.execute(ec, + (sc: Int, msg: String) => { println(name+" execution failed with error: "+sc+"\n"+msg); onFailure((sc, msg)); }, + () => { }, + () => { ScopeReuseManager.freeRunner(runner) }, + Some(executable)); + if (ec.response != null && ec.response.getStatusCode() != 200) { + println(name+" execution failed with non-200 response: "+ec.response.getStatusCode()); + onFailure((ec.response.getStatusCode, ec.response.getOutput())); + } + ec; + } catch { + case e: JSCompileException => { + val r = ScopeReuseManager.getEmpty { r => + execution.syntaxErrorExecutable.execute(r.globalScope); + } + val ec = ExecutionContext(null, null, r); +// r.globalScope.put("_appjetcontext_", r.globalScope, ec); + ExecutionContextUtils.withContext(ec) { + ec.attributes("error") = e; + ec.result = execution.onSyntaxErrorExecutable.execute(r.globalScope); + onFailure(e); + } + ec; + } + case e => { + println(name+" execution failed with error."); onFailure(e); ec; + } + } + } +} |