summaryrefslogtreecommitdiffstats
path: root/trunk/infrastructure/net.appjet.oui
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/infrastructure/net.appjet.oui')
-rw-r--r--trunk/infrastructure/net.appjet.oui/ConfigParam.java25
-rw-r--r--trunk/infrastructure/net.appjet.oui/FastJSON.scala171
-rw-r--r--trunk/infrastructure/net.appjet.oui/GeneratedConfigParam.java23
-rw-r--r--trunk/infrastructure/net.appjet.oui/config.scala240
-rw-r--r--trunk/infrastructure/net.appjet.oui/dynamicvar.scala49
-rw-r--r--trunk/infrastructure/net.appjet.oui/encryption.scala267
-rw-r--r--trunk/infrastructure/net.appjet.oui/execution.scala654
-rw-r--r--trunk/infrastructure/net.appjet.oui/files.scala355
-rw-r--r--trunk/infrastructure/net.appjet.oui/logging.scala530
-rw-r--r--trunk/infrastructure/net.appjet.oui/main.scala386
-rw-r--r--trunk/infrastructure/net.appjet.oui/monitoring.scala125
-rw-r--r--trunk/infrastructure/net.appjet.oui/network.scala50
-rw-r--r--trunk/infrastructure/net.appjet.oui/servermodel.scala209
-rw-r--r--trunk/infrastructure/net.appjet.oui/stats.scala220
-rw-r--r--trunk/infrastructure/net.appjet.oui/synchronizer.scala69
-rw-r--r--trunk/infrastructure/net.appjet.oui/util.scala153
16 files changed, 3526 insertions, 0 deletions
diff --git a/trunk/infrastructure/net.appjet.oui/ConfigParam.java b/trunk/infrastructure/net.appjet.oui/ConfigParam.java
new file mode 100644
index 0000000..5029f28
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/ConfigParam.java
@@ -0,0 +1,25 @@
+/**
+ * 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.lang.annotation.*;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ConfigParam {
+ public String value(); // a description of the config parameter.
+ public String argName() default "";
+} \ No newline at end of file
diff --git a/trunk/infrastructure/net.appjet.oui/FastJSON.scala b/trunk/infrastructure/net.appjet.oui/FastJSON.scala
new file mode 100644
index 0000000..60cfc48
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/FastJSON.scala
@@ -0,0 +1,171 @@
+/**
+ * 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 org.mozilla.javascript.{Context,Scriptable,ScriptableObject};
+import org.json.{JSONStringer,JSONObject,JSONArray};
+
+object FastJSON {
+ def stringify(rhinoObj: Scriptable): String = {
+ return FastJSONStringify.stringify(rhinoObj);
+ }
+ def parse(exctx: ExecutionContext, source: String): Scriptable = {
+ return (new FastJSONParser(exctx)).parse(source);
+ }
+}
+
+//----------------------------------------------------------------
+// FastJSONStringify
+//----------------------------------------------------------------
+object FastJSONStringify {
+
+ def stringify(rhinoObj: Scriptable): String = {
+ val stringer = new JSONStringer();
+ stringerizeScriptable(stringer, rhinoObj);
+ return stringer.toString();
+ }
+
+ private def stringerize(s: JSONStringer, v: Object) {
+ if (v == Context.getUndefinedValue) {
+ return;
+ }
+ v match {
+ case (o:Scriptable) => stringerizeScriptable(s, o);
+ case (o:Number) => {
+ val d = o.doubleValue;
+ if (d.toLong.toDouble == d) {
+ s.value(d.toLong);
+ }
+ else {
+ s.value(o);
+ }
+ }
+ case o => s.value(o);
+ }
+ }
+
+ private def stringerizeScriptable(stringer: JSONStringer, rhinoObj: Scriptable) {
+ if (rhinoObj.getClassName() == "Array") {
+ stringerizeArray(stringer, rhinoObj);
+ } else {
+ stringerizeObj(stringer, rhinoObj);
+ }
+ }
+
+ private def stringerizeObj(stringer: JSONStringer, rhinoObj: Scriptable) {
+ stringer.`object`();
+
+ for (id <- rhinoObj.getIds()) {
+ val k = id.toString();
+ var v:Object = null;
+ id match {
+ case (s:String) => { v = rhinoObj.get(s, rhinoObj); }
+ case (n:Number) => { v = rhinoObj.get(n.intValue, rhinoObj); }
+ case _ => {}
+ }
+
+ if (v != null && v != Scriptable.NOT_FOUND && v != Context.getUndefinedValue) {
+ stringer.key(k);
+ stringerize(stringer, v);
+ }
+ }
+
+ stringer.endObject();
+ }
+
+ private def stringerizeArray(stringer: JSONStringer, rhinoArray: Scriptable) {
+ stringer.`array`();
+
+ val ids:Array[Object] = rhinoArray.getIds();
+ var x = 0;
+ for (i <- 0 until ids.length) {
+ // we ignore string keys on js arrays. crockford's "offical"
+ // json library does this as well.
+ if (ids(i).isInstanceOf[Number]) {
+ val id:Int = ids(i).asInstanceOf[Number].intValue;
+ while (x < id) {
+ stringer.value(null);
+ x += 1;
+ }
+ val v:Object = rhinoArray.get(id, rhinoArray);
+ stringerize(stringer, v);
+ x += 1;
+ }
+ }
+
+ stringer.endArray();
+ }
+}
+
+//----------------------------------------------------------------
+// FastJSONParse
+//----------------------------------------------------------------
+class FastJSONParser(val ctx:ExecutionContext) {
+
+ def parse(source: String): Scriptable = {
+ if (source(0) == '[') {
+ jsonToRhino(new JSONArray(source)).asInstanceOf[Scriptable];
+ } else {
+ jsonToRhino(new JSONObject(source)).asInstanceOf[Scriptable];
+ }
+ }
+
+ private def newObj(): Scriptable = {
+ Context.getCurrentContext().newObject(ctx.runner.globalScope);
+ }
+
+ private def newArray(): Scriptable = {
+ Context.getCurrentContext().newArray(ctx.runner.globalScope, 0);
+ }
+
+ private def jsonToRhino(json: Object): Object = {
+ json match {
+ case (o:JSONArray) => jsonArrayToRhino(o);
+ case (o:JSONObject) => jsonObjectToRhino(o);
+ case o if (o == JSONObject.NULL) => null;
+ case o => o;
+ }
+ }
+
+ private def jsonArrayToRhino(json: JSONArray): Scriptable = {
+ val o:Scriptable = newArray();
+ for (i <- 0 until json.length()) {
+ o.put(i, o, jsonToRhino(json.get(i)));
+ }
+ return o;
+ }
+
+ private def jsonObjectToRhino(json: JSONObject): Scriptable = {
+ val o:Scriptable = newObj();
+ val names:Array[String] = JSONObject.getNames(json);
+ if (names != null) {
+ for (n <- names) {
+ val i = try { Some(n.toInt); } catch { case (e:NumberFormatException) => None };
+ if (i.isDefined) {
+ o.put(i.get, o, jsonToRhino(json.get(n)));
+ }
+ else {
+ o.put(n, o, jsonToRhino(json.get(n)));
+ }
+ }
+ }
+ return o;
+ }
+
+}
+
+
diff --git a/trunk/infrastructure/net.appjet.oui/GeneratedConfigParam.java b/trunk/infrastructure/net.appjet.oui/GeneratedConfigParam.java
new file mode 100644
index 0000000..0986015
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/GeneratedConfigParam.java
@@ -0,0 +1,23 @@
+/**
+ * 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.lang.annotation.*;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface GeneratedConfigParam {
+} \ No newline at end of file
diff --git a/trunk/infrastructure/net.appjet.oui/config.scala b/trunk/infrastructure/net.appjet.oui/config.scala
new file mode 100644
index 0000000..46e73cf
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/config.scala
@@ -0,0 +1,240 @@
+/**
+ * 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 scala.collection.mutable.HashMap;
+import java.util.regex.Pattern;
+import java.net.URL;
+import org.mortbay.jetty.servlet.Context;
+import org.mozilla.javascript.{Scriptable, ScriptableObject, Context => JSContext};
+
+import net.appjet.common.util.BetterFile;
+
+
+object config {
+ val values = new HashMap[String, String];
+ def stringOrElse(name: String, default: String): String = {
+ val v = values.getOrElse(name, default);
+ if (v != null) {
+ val m = propertiesPattern.matcher(v);
+ val sb = new StringBuffer();
+ while (m.find()) {
+ m.appendReplacement(sb, getClass.getDeclaredMethod(m.group(1), Array[Class[_]](): _*).invoke(this, Array[Class[_]](): _*).asInstanceOf[String]);
+ }
+ m.appendTail(sb);
+ sb.toString();
+ } else {
+ null;
+ }
+ }
+ def boolOrElse(name: String, default: Boolean) = values.get(name).map(_.equals("true")).getOrElse(default);
+ def intOrElse(name: String, default: Int) = values.get(name).map(Integer.parseInt(_)).getOrElse(default);
+ def longOrElse(name: String, default: Long) = values.get(name).map(java.lang.Long.parseLong(_)).getOrElse(default);
+
+ @ConfigParam("Read configuration options from this file before processing any command-line flags.")
+ { val argName = "file" }
+ def configFile = stringOrElse("configFile", null);
+
+ // configuation parameters
+ var specialDebug = false;
+
+ @ConfigParam("Enable additional logging output.")
+ def verbose = boolOrElse("verbose", false);
+
+ @ConfigParam("Activate \"developer\" mode.")
+ def devMode = boolOrElse("devMode", false);
+
+ @ConfigParam("Activate \"profiling\" mode.")
+ def profile = boolOrElse("profile", false);
+
+ @ConfigParam("Directory to use for storing appjet support files, logs, etc. This directory will be created if it does not exist and must be writeable by the user who runs appjet.jar. Defaults to current working directory.")
+ { val argName = "directory" }
+ def appjetHome = stringOrElse("appjetHome", "appjet");
+
+ @ConfigParam("Directory to use for storing built-in database (Apache Derby) files. Will be created if it doesn't exist. Defaults to [appjetHome]/db")
+ def derbyHome = stringOrElse("derbyHome", "[appjetHome]/derbydb");
+
+ @ConfigParam("Directory to use for storing appserver logs. Defaults to [appjetHome]/log/appserver")
+ { val argName = "directory" }
+ def logDir = stringOrElse("logDir", "[appjetHome]/log/appserver");
+
+ @ConfigParam("Optional alternative directory to load built-in libraries from. Used by AppJet platform hackers to develop and debug built-in libraries. Default: use built-in libraries.")
+ { val argName = "directory" }
+ def ajstdlibHome = stringOrElse("ajstdlibHome", null);
+
+ @ConfigParam("Optional directory to specify as the \"app home\".")
+ { val argName = "directory" }
+ def appHome = stringOrElse("appHome", "");
+
+
+ @ConfigParam("Search path for modules imported via \"import\". Defaults to current working directory.")
+ { val argName = "dir1:dir2:..." }
+ def modulePath = stringOrElse("modulePath", null);
+ def moduleRoots =
+ Array.concat(Array("."), if (modulePath != null) modulePath.split(":") else Array[String](), Array(ajstdlibHome));
+
+ @ConfigParam("Where to read the static files from on the local filesystem. Don't specify this to read static files from the classpath/JAR.")
+ { val argName = "directory" }
+ def useVirtualFileRoot = stringOrElse("useVirtualFileRoot", null);
+
+ @ConfigParam("Directory to use for storing the temporary sessions file on shutdown. Will be created if it does not exist.")
+ { val argName = "directory" }
+ def sessionStoreDir = stringOrElse("sessionStoreDir", "[appjetHome]/sessions");
+
+ // performance tuning
+ @ConfigParam("Create this many runners before opening up the server.")
+ { val argName = "count" }
+ def preloadRunners = intOrElse("preloadRunners", 0);
+
+ @ConfigParam("Have this many JDBC connections available in the pool.")
+ { val argName = "count" }
+ def jdbcPoolSize = intOrElse("jdbcPoolSize", 10);
+ @ConfigParam("Max count of worker threads.")
+ { val argName = "num" }
+ def maxThreads = intOrElse("maxThreads", 250);
+
+ // specifying ports and such
+ def extractHostAndPort(s: String): (String, Int) =
+ if (s.indexOf(":") >= 0)
+ (s.split(":")(0), Integer.parseInt(s.split(":")(1)))
+ else
+ ("", Integer.parseInt(s))
+
+ @ConfigParam("[host:]port on which to serve the app. Default: 8080.")
+ { val argName = "[host:]port" }
+ def listen = stringOrElse("listen", "8080");
+ @GeneratedConfigParam
+ def listenHost = extractHostAndPort(listen)._1;
+ @GeneratedConfigParam
+ def listenPort = extractHostAndPort(listen)._2;
+
+ @ConfigParam("[host:]port on which to serve the app using SSL. Default: none.")
+ { val argName = "[host:]port" }
+ def listenSecure = stringOrElse("listenSecure", "0");
+ @GeneratedConfigParam
+ def listenSecureHost = extractHostAndPort(listenSecure)._1;
+ @GeneratedConfigParam
+ def listenSecurePort = extractHostAndPort(listenSecure)._2;
+
+ @ConfigParam("[host:]port:port on which to listen for monitoring. Default: none.")
+ { val argName = "[host:]primaryPort:secondaryPort" }
+ def listenMonitoring = stringOrElse("listenMonitoring", "0:0");
+ def extractHostAndPortPort(s: String): (String, Int, Int) = {
+ val spl = s.split(":", 3);
+ if (spl.length > 2)
+ (spl(0), Integer.parseInt(spl(1)), Integer.parseInt(spl(2)))
+ else
+ ("", Integer.parseInt(spl(0)), Integer.parseInt(spl(1)));
+ }
+ @GeneratedConfigParam
+ def listenMonitoringHost = extractHostAndPortPort(listenMonitoring)._1;
+ @GeneratedConfigParam
+ def listenMonitoringPrimaryPort = extractHostAndPortPort(listenMonitoring)._2;
+ @GeneratedConfigParam
+ def listenMonitoringSecondaryPort = extractHostAndPortPort(listenMonitoring)._3;
+
+ @ConfigParam("[host:]port on which to listen for RPCs (via SARS). Default: none.")
+ { val argName = "[host:]port" }
+ def listenSars = stringOrElse("listenSars", "0");
+ @GeneratedConfigParam
+ def listenSarsHost = extractHostAndPort(listenSars)._1;
+ @GeneratedConfigParam
+ def listenSarsPort = extractHostAndPort(listenSars)._2;
+
+ // Licensing
+ @ConfigParam("Private key for generating license keys.")
+ { val argName = "pathToKey" }
+ def licenseGeneratorKey = stringOrElse("licenseGeneratorKey", null);
+
+ // SARS
+ @ConfigParam("SARS auth key. Default: \"appjet\".")
+ { val argName = "authkey" }
+ def sarsAuthKey = stringOrElse("sarsAuthKey", "appjet");
+
+ // SSL
+ @ConfigParam("[SSL] Keystore location. Default: appjetHome/sslkeystore.")
+ { val argName = "keystore" }
+ def sslKeyStore = stringOrElse("sslKeyStore", appjetHome+"/sslkeystore");
+ def sslKeyStore_isSet = values.contains("sslKeyStore");
+ @ConfigParam("[SSL] Key password. Default: same as store password.")
+ { val argName = "password" }
+ def sslKeyPassword = stringOrElse("sslKeyPassword", "[sslStorePassword]");
+ @ConfigParam("[SSL] Store password. Default: 'appjet'.")
+ { val argName = "password" }
+ def sslStorePassword = stringOrElse("sslStorePassword", "appjet");
+
+ // email
+ @ConfigParam("host:port of mail server to use for sending email. Default: localhost:25.")
+ { val argName = "host:port" }
+ def smtpServer = stringOrElse("smtpServer", "localhost:25");
+ def smtpServerHost = extractHostAndPort(smtpServer)._1;
+ def smtpServerPort = extractHostAndPort(smtpServer)._2;
+ @ConfigParam("username for authentication to mail server. Default: no authentication.")
+ { val argName = "username" }
+ def smtpUser = stringOrElse("smtpUser", "");
+ @ConfigParam("password for authentication to mail server. Default: no authentication.")
+ { val argName = "password" }
+ def smtpPass = stringOrElse("smtpPass", "");
+
+ // comet
+ @ConfigParam("prefix for all comet requests. Required to use Comet system.")
+ { val argName = "path" }
+ def transportPrefix = stringOrElse("transportPrefix", null);
+ @ConfigParam("Use a subdomain for all comet requests.")
+ def transportUseWildcardSubdomains = boolOrElse("transportUseWildcardSubdomains", false);
+ @ConfigParam("Don't use short polling, ever.")
+ def disableShortPolling = boolOrElse("disableShortPolling", false);
+
+ // helpers
+ val allProperties =
+ for (m <- getClass.getDeclaredMethods() if (m.getAnnotation(classOf[ConfigParam]) != null || m.getAnnotation(classOf[GeneratedConfigParam]) != null))
+ yield m;
+ val configParamNames =
+ for (m <- allProperties if m.getAnnotation(classOf[ConfigParam]) != null) yield m.getName
+ lazy val allPropertiesMap =
+ Map((for (m <- allProperties) yield ((m.getName, () => m.invoke(this)))): _*);
+ val propertiesPattern = Pattern.compile("\\[("+allProperties.map(x => "(?:"+x.getName()+")").mkString("|")+")\\]");
+
+ override def toString() =
+ (allProperties.map(m => m.getName()+" -> "+m.invoke(this)) ++
+ values.keys.toList.filter(! allPropertiesMap.contains(_)).map(k => k+" -> "+values(k))).mkString("[Config ", ", ", "]");
+ def print {
+ for (m <- allProperties) {
+ println(m.getName() + " -> " + m.invoke(this));
+ }
+ for ((k, v) <- values if (! allPropertiesMap.contains(k))) {
+ println(k + " -> " + v);
+ }
+ }
+ def configObject(globalScope: Scriptable) =
+ new ScriptableAdapter {
+ val keys = (Set.empty[Object] ++ allProperties.map(m => m.getName) ++ values.keySet).toList.toArray;
+ override def get(n: String, start: Scriptable) =
+ allPropertiesMap.getOrElse(n, () => values.getOrElse(n, JSContext.getUndefinedValue()))();
+ override def put(n: String, start: Scriptable, value: Object) =
+ values(n) = value.toString();
+ override def getIds() = keys;
+ override def getPrototype() = ScriptableObject.getObjectPrototype(globalScope);
+ override def has(n: String, start: Scriptable) =
+ allPropertiesMap.contains(n) || values.contains(n);
+ override def getDefaultValue(hint: Class[_]) = config.toString();
+ }
+}
+
+object global {
+ var context: Context = null;
+}
diff --git a/trunk/infrastructure/net.appjet.oui/dynamicvar.scala b/trunk/infrastructure/net.appjet.oui/dynamicvar.scala
new file mode 100644
index 0000000..b1f8c2e
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/dynamicvar.scala
@@ -0,0 +1,49 @@
+/**
+ * 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;
+
+class NoninheritedDynamicVariable[T](init: T) {
+ private val tl = new ThreadLocal[T] {
+ override def initialValue = init.asInstanceOf[T with AnyRef]
+ }
+
+ /** Retrieve the current value */
+ def value: T = tl.get.asInstanceOf[T]
+
+
+ /** Set the value of the variable while executing the specified
+ * thunk.
+ *
+ * @param newval The value to which to set the fluid
+ * @param thunk The code to evaluate under the new setting
+ */
+ def withValue[S](newval: T)(thunk: =>S): S = {
+ val oldval = value
+ tl.set(newval)
+
+ try { thunk } finally {
+ tl.set(oldval)
+ }
+ }
+
+ /** Change the currently bound value, discarding the old value.
+ * Usually <code>withValue()</code> gives better semantics.
+ */
+ def value_=(newval: T) = { tl.set(newval) }
+
+ override def toString: String = "NoninheritedDynamicVariable(" + value +")"
+}
diff --git a/trunk/infrastructure/net.appjet.oui/encryption.scala b/trunk/infrastructure/net.appjet.oui/encryption.scala
new file mode 100644
index 0000000..92d463b
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/encryption.scala
@@ -0,0 +1,267 @@
+/**
+ * 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 scala.collection.mutable.ArrayBuffer;
+
+import javax.crypto.Cipher;
+import java.security._;
+import java.security.spec._;
+import java.math.BigInteger;
+import java.io.{ObjectInputStream, ObjectOutputStream, FileInputStream, FileOutputStream, PrintWriter, OutputStreamWriter, ByteArrayOutputStream, ByteArrayInputStream, InputStream, InputStreamReader, BufferedReader, DataOutputStream, DataInputStream};
+
+import net.appjet.common.util.BetterFile;
+
+// object EncryptomaticTest {
+// def main(args: Array[String]) {
+// args(0) match {
+// case "genkeys" => {
+// val keyPair = Encryptomatic.generateKeyPair;
+// println("made key pair.")
+// Encryptomatic.writeKeyPair(keyPair, args(1), args(2));
+// println("done.");
+// }
+// case "printkeys" => {
+// val keyPair = Encryptomatic.generateKeyPair;
+// val Pair(pubBytes, privBytes) = Encryptomatic.keyPairBytes(keyPair);
+// println("Public key: "+Encryptomatic.bytesToAscii(pubBytes))
+// println("Private key: "+Encryptomatic.bytesToAscii(privBytes));
+// }
+// case "sign" => {
+// println(Encryptomatic.sign(java.lang.System.in, Encryptomatic.readPrivateKey(new FileInputStream(args(1)))));
+// }
+// case "verify" => {
+// if (Encryptomatic.verify(java.lang.System.in, Encryptomatic.readPublicKey(new FileInputStream(args(1))), args(2))) {
+// println("Verification succeeded.");
+// } else {
+// println("Verification failed.");
+// }
+// }
+// case "test" => {
+// val out = new PrintWriter(new OutputStreamWriter(System.out, "UTF-8"), true);
+// val src = "Hey dudes, this is a test of この魚は築地からのですか?";
+// out.println(src);
+// val bytes = Encryptomatic.bytesToAscii(src.getBytes("UTF-8"));
+// out.println("bytes: "+bytes);
+// val done = new String(Encryptomatic.asciiToBytes(bytes), "UTF-8");
+// out.println(done);
+// out.println("Match? "+(done == src));
+// }
+// case "keytest" => {
+// val keyPair = Encryptomatic.generateKeyPair;
+// val bytes = Encryptomatic.keyPairBytes(keyPair);
+// try {
+// val newKeyPair = Encryptomatic.readKeyPair(new ByteArrayInputStream(Encryptomatic.bytesToAscii(bytes._1).getBytes()),
+// new ByteArrayInputStream(Encryptomatic.bytesToAscii(bytes._2).getBytes()));
+// println("equal? "+(keyPair.getPublic.getEncoded.deepEquals(newKeyPair.getPublic.getEncoded) && keyPair.getPrivate.getEncoded.deepEquals(newKeyPair.getPrivate.getEncoded)));
+// } catch {
+// case e: InvalidKeySpecException => {
+// println("equality failed.")
+// println("public key 1 is: "+bytes._1.mkString("(", ",", ")"));
+// println("public key 2 is: "+BetterFile.getStreamBytes(new Encryptomatic.AsciiToBytesInputStream(new ByteArrayInputStream(Encryptomatic.bytesToAscii(bytes._1).getBytes()))).mkString("(", ",", ")"));
+// println("pk1 enc to: "+Encryptomatic.bytesToAscii(bytes._1));
+// }
+// }
+// }
+// }
+// }
+// }
+
+object Encryptomatic {
+ private val chars = "0123456789abcdefghijlkmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ def bytesToAscii(bytes: Array[Byte]) = {
+ var i = BigInt(bytes);
+ val neg = i < 0;
+ if (neg)
+ i = BigInt(0)-i;
+ val sb = new StringBuffer();
+ while (i > BigInt(chars.length-1)) {
+ val Pair(div, mod) = i /% BigInt(chars.length);
+ sb.append(chars(mod.intValue));
+ i = div;
+ }
+ sb.append(chars(i.intValue));
+ (if (neg) "-" else "")+sb.toString.reverse;
+ }
+ def asciiToBytes(src: String) = {
+ var i = BigInt(0);
+ val Pair(isNegative, b) =
+ if (src.startsWith("-"))
+ (true, src.substring(1))
+ else
+ (false, src);
+ for (c <- b) {
+ i = i * chars.length + chars.indexOf(c);
+ }
+ if (isNegative)
+ i = BigInt(0)-i;
+ i.toByteArray
+ }
+
+ def generateKeyPair(keyType: String) = {
+ val keyGen = KeyPairGenerator.getInstance(keyType);
+ val random = SecureRandom.getInstance("SHA1PRNG", "SUN");
+ keyGen.initialize(1024, random);
+ keyGen.generateKeyPair();
+ }
+
+ def keyPairBytes(keyPair: KeyPair) = {
+ val pubKey = keyPair.getPublic();
+ if (pubKey.getFormat != "X.509")
+ throw new RuntimeException("Can't produce public key in format: "+pubKey.getFormat);
+
+ val privKey = keyPair.getPrivate();
+ if (privKey.getFormat != "PKCS#8")
+ throw new RuntimeException("Can't produce private key in format: "+privKey.getFormat);
+
+ (pubKey.getEncoded, privKey.getEncoded)
+ }
+
+ def writeKeyPair(keyPair: KeyPair, publicKey: String, privateKey: String) {
+ val pubOutputStream = new PrintWriter(new FileOutputStream(publicKey));
+ val privOutputStream = new PrintWriter(new FileOutputStream(privateKey));
+ val Pair(pubBytes, privBytes) = keyPairBytes(keyPair);
+ pubOutputStream.print(bytesToAscii(pubBytes));
+ privOutputStream.print(bytesToAscii(privBytes));
+ List(pubOutputStream, privOutputStream).foreach(x => {x.flush(); x.close()});
+ }
+
+ class AsciiToBytesInputStream(in: InputStream) extends InputStream {
+ val reader = new BufferedReader(new InputStreamReader(in));
+ val bytes = new ByteArrayInputStream(asciiToBytes(reader.readLine()));
+ def read(): Int = bytes.read();
+ }
+
+ def readPublicKey(keyType: String, publicKey: InputStream) = {
+ val pubKeySpec = new X509EncodedKeySpec(BetterFile.getStreamBytes(new AsciiToBytesInputStream(publicKey)));
+ KeyFactory.getInstance(keyType).generatePublic(pubKeySpec);
+ }
+ def readPrivateKey(keyType: String, privateKey: InputStream) = {
+ val privKeySpec = new PKCS8EncodedKeySpec(BetterFile.getStreamBytes(new AsciiToBytesInputStream(privateKey)));
+ KeyFactory.getInstance(keyType).generatePrivate(privKeySpec);
+ }
+
+ def readKeyPair(keyType: String, publicKey: InputStream, privateKey: InputStream) = {
+ new KeyPair(readPublicKey(keyType, publicKey),
+ readPrivateKey(keyType, privateKey));
+ }
+
+ def sign(source: InputStream, key: PrivateKey): Array[byte] = {
+ val dsa = Signature.getInstance("SHA1withDSA");
+ dsa.initSign(key);
+ val inBytes = new Array[Byte](4096);
+ var count = source.read(inBytes);
+ while (count > 0) {
+ dsa.update(inBytes, 0, count);
+ count = source.read(inBytes);
+ }
+ dsa.sign();
+ }
+
+ def verify(source: InputStream, key: PublicKey, sig: Array[byte]): Boolean = {
+ val dsa = Signature.getInstance("SHA1withDSA");
+ dsa.initVerify(key);
+ val inBytes = new Array[Byte](4096);
+ var count = source.read(inBytes);
+ while (count > 0) {
+ dsa.update(inBytes, 0, count);
+ count = source.read(inBytes);
+ }
+ dsa.verify(sig)
+ }
+
+ def encrypt(source: InputStream, key: PublicKey): Array[byte] = {
+ val cipher = Cipher.getInstance("RSA");
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ val inBytes = new Array[Byte](100);
+ val outBytesStream = new ByteArrayOutputStream();
+ val dataOut = new DataOutputStream(outBytesStream);
+
+ var count = source.read(inBytes);
+ while (count > 0) {
+ val arr = cipher.doFinal(inBytes, 0, count);
+ dataOut.writeShort(arr.length);
+ dataOut.write(arr, 0, arr.length);
+ count = source.read(inBytes);
+ }
+ dataOut.writeShort(0);
+ outBytesStream.toByteArray();
+ }
+
+ def decrypt(source: InputStream, key: PrivateKey): Array[byte] = {
+ val in = new DataInputStream(source);
+ def readBlock() = {
+ val length = in.readShort();
+ if (length > 0) {
+ val bytes = new Array[Byte](length);
+ in.readFully(bytes);
+ Some(bytes);
+ } else {
+ None;
+ }
+ }
+ val outBytes = new ArrayBuffer[Byte];
+ val cipher = Cipher.getInstance("RSA");
+ cipher.init(Cipher.DECRYPT_MODE, key);
+ var block = readBlock();
+ while (block.isDefined) {
+ outBytes ++= cipher.doFinal(block.get);
+ block = readBlock();
+ }
+ outBytes.toArray;
+ }
+}
+
+object Encryptor {
+ def main(args: Array[String]) {
+ args(0) match {
+ case "genkeys" => {
+ println("generating keys...");
+ val keyPair = Encryptomatic.generateKeyPair(args(1));
+ println("saving public key to: "+args(2)+"; private key to: "+args(3));
+ Encryptomatic.writeKeyPair(keyPair, args(2), args(3));
+ println("done.");
+ }
+ case "test" => {
+ val plaintext = "This is a test of some data that's actually pretty long once you really start thinking about it. I mean, it needs to be more than 117 bytes for it to be a reasonable test, and I suppose it's pretty close to that now. OK, let's just go for it and see what happens.".getBytes("UTF-8");
+ val keys = Encryptomatic.generateKeyPair("RSA");
+ val ciphertext = Encryptomatic.bytesToAscii(Encryptomatic.encrypt(new ByteArrayInputStream(plaintext), keys.getPublic()));
+ println(ciphertext);
+ println(new String(Encryptomatic.decrypt(new ByteArrayInputStream(Encryptomatic.asciiToBytes(ciphertext)), keys.getPrivate()), "UTF-8"));
+ }
+ case "decode" => {
+ val key = Encryptomatic.readPrivateKey(args(1), new FileInputStream(args(2)));
+ val plaintext = Encryptomatic.decrypt(new ByteArrayInputStream(Encryptomatic.asciiToBytes(args(3))), key);
+ println(new String(plaintext, "UTF-8"));
+ }
+ case "decodeFile" => {
+ println("Enter private key (assuming type RSA):");
+ val key = Encryptomatic.readPrivateKey("RSA", java.lang.System.in);
+ val file = new java.io.File(args(1));
+ println("Reading "+file.getName()+"...");
+ val reader = new java.io.BufferedReader(new java.io.InputStreamReader(new FileInputStream(file)));
+ var line = reader.readLine();
+ while (line != null) {
+ val bytes = Encryptomatic.decrypt(new ByteArrayInputStream(Encryptomatic.asciiToBytes(line)), key);
+ println(new String(bytes, "UTF-8"));
+ line = reader.readLine();
+ }
+ }
+ }
+ }
+}
diff --git a/trunk/infrastructure/net.appjet.oui/execution.scala b/trunk/infrastructure/net.appjet.oui/execution.scala
new file mode 100644
index 0000000..63749b1
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/execution.scala
@@ -0,0 +1,654 @@
+/**
+ * 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;
+
+class RequestWrapper(val req: HttpServletRequest) {
+ req.setCharacterEncoding("UTF-8");
+// 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;
+ }
+ 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;
+ }
+ }
+ }
+}
diff --git a/trunk/infrastructure/net.appjet.oui/files.scala b/trunk/infrastructure/net.appjet.oui/files.scala
new file mode 100644
index 0000000..3df5c1c
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/files.scala
@@ -0,0 +1,355 @@
+/**
+ * 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 net.appjet.bodylock.{BodyLock, Executable};
+import net.appjet.common.util.BetterFile;
+
+import java.io.{File, FileNotFoundException, FileInputStream, IOException, ByteArrayInputStream};
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.WeakHashMap;
+
+import scala.collection.mutable.{Subscriber, Message, Reset=>SReset};
+import scala.collection.jcl.Conversions._;
+
+trait WeakPublisher[A, This <: WeakPublisher[A, This]] { self: This =>
+ val subscribers = new WeakHashMap[Subscriber[A, This], Unit];
+
+ protected def publish(event: A): Unit = {
+ subscribers.synchronized {
+ val subsCopy = for (sub <- subscribers.keySet()) yield sub;
+ for (sub <- subsCopy) {
+ sub.notify(this, event);
+ }
+ }
+ }
+
+ def subscribe(sub: Subscriber[A, This]): Unit = {
+ subscribers.synchronized {
+ subscribers.put(sub, ());
+ }
+ }
+
+ def removeSubscription(sub: Subscriber[A, This]): Unit = {
+ subscribers.synchronized {
+ subscribers.remove(sub);
+ }
+ }
+}
+
+object Reset extends SReset[Unit];
+
+object FileCache {
+ val files = new ConcurrentHashMap[String, CachedFile];
+
+ def file(path: String): CachedFile = {
+ if (files.containsKey(path)) {
+ files.get(path);
+ } else {
+ val f = new CachedFile(new File(path));
+ val oldFile = files.putIfAbsent(path, f);
+ if (oldFile != null) {
+ oldFile
+ } else {
+ f
+ }
+ }
+ }
+
+ def file(path: String, subscriber: Subscriber[Message[Unit], CachedFile]): CachedFile = {
+ val f = file(path);
+ f.subscribe(subscriber);
+ f;
+ }
+
+ def testFiles() = {
+ val iter = files.values().iterator();
+ var filesHaveChanged = false;
+ while (iter.hasNext()) {
+ if (iter.next().test()) {
+ filesHaveChanged = true;
+ }
+ }
+ filesHaveChanged;
+ }
+}
+
+class CachedFile(f: File) extends WeakPublisher[Message[Unit], CachedFile] {
+ var cachedContent: Option[Array[Byte]] = None;
+ def content = synchronized {
+ if (cachedContent.isEmpty) {
+ cachedContent = Some(BetterFile.getFileBytes(f));
+ }
+ cachedContent.get;
+ }
+ def stream = new ByteArrayInputStream(content);
+
+ var cachedExistence: Option[Boolean] = None;
+ def exists = synchronized {
+ if (cachedExistence.isEmpty) {
+ cachedExistence = Some(f.exists());
+ }
+ cachedExistence.get;
+ }
+
+ var cachedDirectory: Option[Boolean] = None;
+ def isDirectory = synchronized {
+ if (cachedDirectory.isEmpty) {
+ cachedDirectory = Some(f.isDirectory());
+ }
+ cachedDirectory.get;
+ }
+
+ def underlyingLastModified = f.lastModified;
+ var lastModified = underlyingLastModified;
+
+ def hasBeenModified = underlyingLastModified != lastModified;
+
+ def test() = synchronized {
+ if (hasBeenModified) {
+ reset;
+ true;
+ } else {
+ false;
+ }
+ }
+
+ def reset = synchronized {
+ lastModified = underlyingLastModified;
+ cachedContent = None;
+ cachedExistence = None;
+ cachedDirectory = None;
+ publish(Reset);
+ }
+}
+
+class SpecialJarOrNotFile(root: String, fname: String) extends JarOrNotFile(root, fname) {
+ override val classBase = "/net/appjet/ajstdlib/";
+ override val fileSep = "/../";
+
+ override def clone(fname: String) = new SpecialJarOrNotFile(root, fname);
+}
+
+// A JarOrNotFile that reads from the /mirror directory in the classpath.
+class MirroredJarOrNotFile(root: String, fname: String) extends JarOrNotFile(root, fname) {
+ override val classBase = "/mirror/";
+ override def clone(fname: String) = new MirroredJarOrNotFile(root, fname);
+}
+
+class JarVirtualFile(fname: String) extends MirroredJarOrNotFile(config.useVirtualFileRoot, fname);
+
+class JarOrNotFile(root: String, fname: String) extends Subscriber[Message[Unit], CachedFile] with WeakPublisher[Message[Unit], JarOrNotFile] {
+ val classBase = "/net/appjet/ajstdlib/modules/";
+ val fileSep = "/";
+ val isJar = (root == null);
+ val streamBase = if (isJar) getClass().getResource((classBase+fname).replaceAll("/+", "/")) else null;
+ val file = if (! isJar) FileCache.file(root+fileSep+fname, this) else null;
+
+ def openStream() = {
+ if (isJar) streamBase.openStream;
+ else file.stream;
+ }
+
+ def exists = {
+ if (isJar) streamBase != null;
+ else file.exists;
+ }
+
+ def isDirectory = if (isJar) false else file.isDirectory;
+
+ lazy val streamModified = streamBase.openConnection().getLastModified();
+ def lastModified = {
+ if (isJar) streamModified;
+ else file.lastModified;
+ }
+
+ def name = fname;
+
+ override def toString() =
+ getClass.getName+": "+hashCode()+"; fname: "+fname+"; streambase: "+streamBase+"; file: "+file+(if (isJar) " from: "+classBase+fname else "");
+// override def equals(o: AnyRef) =
+// o match {
+// case jf: JarOrNotFile => {
+// classBase == jf.classBase &&
+// fileSep == jf.fileSep &&
+// root == jf.root &&
+// fname == jf.fname
+// }
+// case _ => false
+// }
+// override def hashCode() =
+// classBase.hashCode + fileSep.hashCode + root.hashCode + fname.hashCode
+
+ def notify(pub: CachedFile, event: Message[Unit]) = synchronized {
+ publish(event);
+ }
+
+ def clone(fname: String) = new JarOrNotFile(root, fname);
+}
+
+abstract class AutoUpdateFile(val fname: String) extends Subscriber[Message[Unit], JarOrNotFile] {
+ def files: Array[JarOrNotFile]; // = config.moduleRoots.map(f => new JarOrNotFile(f, libName));
+
+ def exists = files.exists(_.exists);
+ def file = files.find(_.exists).getOrElse(null);
+ def fileLastModified = if (exists) file.lastModified else 0L;
+
+ // var lastModified = fileLastModified;
+ // var cachedContents: Option[String] = None;
+
+ def fail(): Nothing = {
+ throw new FileNotFoundException("No such module: "+fname);
+ }
+
+ // def hasBeenModified = {
+ // if (exists) {
+ // val newModTime = try {
+ // fileLastModified
+ // } catch {
+ // case e: NoSuchElementException => fail();
+ // case e: NullPointerException => fail();
+ // }
+ // newModTime > lastModified;
+ // } else {
+ // false;
+ // }
+ // }
+
+ // def update() = synchronized {
+ // try {
+ // lastModified = fileLastModified;
+ // val contents = BetterFile.getStreamContents(file.openStream()).replace("\r\n", "\n").replace("\r", "\n");
+ // if (contents == null) {
+ // fail();
+ // }
+ // cachedContents = Some(contents);
+ // } catch {
+ // case e: IOException => {
+ // exceptionlog(e);
+ // e.printStackTrace();
+ // fail();
+ // }
+ // }
+ // }
+
+ def notify(pub: JarOrNotFile, event: Message[Unit]) {
+ event match {
+ case Reset => cachedContents = None;
+ }
+ }
+
+ var cachedContents: Option[String] = None;
+ def update() = synchronized {
+ if (cachedContents.isEmpty) {
+ cachedContents = Some(BetterFile.getStreamContents(file.openStream()).replace("\r\n", "\n").replace("\r", "\n"));
+ }
+ }
+
+ def contents = synchronized {
+ update();
+ cachedContents.get;
+ }
+
+ override def toString() = "[AutoUpdateFile: "+fname+"]";
+}
+
+class FixedDiskResource(srcfile: JarOrNotFile) extends AutoUpdateFile(srcfile.name) {
+ lazy val files0 = Array(srcfile);
+ files0.foreach(_.subscribe(this));
+
+ override def files = files0;
+}
+
+abstract class DiskLibrary(fname: String) extends AutoUpdateFile(fname) {
+ var cachedExecutable: Option[Executable] = None;
+
+ lazy val classFiles = files.map({ f =>
+ val parts = f.name.split("/");
+ val pathIfAny = parts.reverse.drop(1).reverse.mkString("/");
+ val newFname =
+ if (pathIfAny == "")
+ className(f.name);
+ else
+ pathIfAny+"/"+className(parts.last);
+ val newFile = f.clone(newFname+".class");
+ newFile.subscribe(this);
+ newFile;
+ });
+ def className(fname: String): String = "JS$"+fname.split("\\.").reverse.drop(1).reverse.mkString(".").replaceAll("[^A-Za-z0-9]", "\\$");
+ def className: String = classFile.name.split("\\.").reverse.drop(1).reverse.mkString(".");
+
+ override def exists = super.exists || classFiles.exists(_.exists);
+ override def file = if (super.exists) super.file else classFile;
+ def classFile = classFiles.find(_.exists).getOrElse(null);
+
+// println("Made DiskLibrary on "+fname+", with classFile: "+classFile);
+
+ def updateExecutable() = synchronized {
+ if (classFile == null)
+ super.update();
+ if (cachedExecutable.isEmpty) {
+ try {
+ if (classFile != null) {
+ cachedExecutable = Some(BodyLock.executableFromBytes(BetterFile.getStreamBytes(classFile.openStream()), className.split("/").last));
+ } else {
+ cachedExecutable = Some(BodyLock.compileString(contents, "module "+fname, 1));
+ }
+ } catch {
+ case e => { cachedExecutable = None; throw e; }
+ }
+ }
+ }
+
+ def executable = synchronized {
+ updateExecutable();
+ cachedExecutable.get
+ }
+
+ override def notify(pub: JarOrNotFile, event: Message[Unit]) = synchronized {
+ super.notify(pub, event);
+ event match {
+ case Reset => cachedExecutable = None;
+ }
+ }
+
+ override def equals(o: Any) =
+ o match {
+ case dl: DiskLibrary => {
+ getClass.getName == dl.getClass.getName &&
+ fname == dl.fname
+ }
+ case _ => false;
+ }
+ override def hashCode() =
+ getClass.getName.hashCode + fname.hashCode
+}
+
+class FixedDiskLibrary(srcfile: JarOrNotFile) extends DiskLibrary(srcfile.name) {
+ lazy val files0 = Array(srcfile);
+ files0.foreach(_.subscribe(this));
+
+ override def files = files0;
+}
+
+class VariableDiskLibrary(libName: String) extends DiskLibrary(libName) {
+ lazy val files0 =
+ Array.concat(Array(new MirroredJarOrNotFile(null, libName)),
+ config.moduleRoots.map(f => new JarOrNotFile(f, libName)))
+ files0.foreach(_.subscribe(this));
+
+ override def files = files0;
+}
diff --git a/trunk/infrastructure/net.appjet.oui/logging.scala b/trunk/infrastructure/net.appjet.oui/logging.scala
new file mode 100644
index 0000000..9c384d2
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/logging.scala
@@ -0,0 +1,530 @@
+/**
+ * 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.text.SimpleDateFormat;
+import java.io.{File, FileWriter, StringWriter, PrintWriter};
+import java.util.Date;
+import java.util.concurrent.{ConcurrentLinkedQueue, ConcurrentHashMap, CopyOnWriteArraySet};
+import java.util.concurrent.atomic.AtomicInteger;
+
+import scala.util.Sorting;
+import scala.ref.WeakReference;
+import scala.collection.mutable.{Map, HashMap};
+import scala.collection.jcl.{SetWrapper, Conversions};
+
+import org.json.{JSONObject, JSONArray};
+import org.mozilla.javascript.{Scriptable, Context};
+
+import Util.iteratorToRichIterator;
+import scala.collection.jcl.Conversions._;
+
+trait LoggablePropertyBag {
+ def date: Date;
+ def `type`: String = value("type").asInstanceOf[String];
+ def json: String;
+ def tabDelimited: String;
+ def keys: Array[String];
+ def value(k: String): Any;
+}
+
+class LoggableFromScriptable(
+ scr: Scriptable,
+ extra: Option[scala.collection.Map[String, String]])
+ extends LoggablePropertyBag {
+ def this(scr: Scriptable) = this(scr, None);
+ if (extra.isDefined) {
+ for ((k, v) <- extra.get if (! scr.has(k, scr))) {
+ scr.put(k, scr, v);
+ }
+ }
+
+ val keys =
+ scr.getIds()
+ .map(_.asInstanceOf[String])
+ .filter(scr.get(_, scr) != Context.getUndefinedValue());
+ Sorting.quickSort(keys);
+ if (! scr.has("date", scr)) {
+ scr.put("date", scr, System.currentTimeMillis());
+ }
+ val date = new Date(scr.get("date", scr).asInstanceOf[Number].longValue);
+ val json = FastJSON.stringify(scr);
+ val tabDelimited = GenericLoggerUtils.dateString(date) + "\t" +
+ keys.filter("date" != _).map(value(_)).mkString("\t");
+
+ def value(k: String) = {
+ scr.get(k, scr);
+ }
+}
+
+class LoggableFromMap[T](
+ map: scala.collection.Map[String, T],
+ extra: Option[scala.collection.Map[String, String]])
+ extends LoggablePropertyBag {
+ def this(map: scala.collection.Map[String, T]) = this(map, None);
+ val keys = map.keys.collect.toArray ++
+ extra.map(_.keys.collect.toArray).getOrElse(Array[String]());
+ Sorting.quickSort(keys);
+
+ def fillJson(json: JSONObject,
+ map: scala.collection.Map[String, T]): JSONObject = {
+ for ((k, v) <- map) {
+ v match {
+ case b: Boolean => json.put(k, b);
+ case d: Double => json.put(k, d);
+ case i: Int => json.put(k, i);
+ case l: Long => json.put(k, l);
+ case m: java.util.Map[_,_] => json.put(k, m);
+ case m: scala.collection.Map[String,T] =>
+ json.put(k, fillJson(new JSONObject(), m));
+ case c: java.util.Collection[_] => json.put(k, c);
+ case o: Object => json.put(k, o);
+ case _ => {};
+ }
+ }
+ json;
+ }
+ val json0 = fillJson(new JSONObject(), map);
+ if (extra.isDefined) {
+ for ((k, v) <- extra.get if (! json0.has(k))) {
+ json0.put(k, v);
+ }
+ }
+ if (! json0.has("date")) {
+ json0.put("date", System.currentTimeMillis());
+ }
+ val date = new Date(json0.getLong("date"));
+ val json = json0.toString;
+ val tabDelimited =
+ GenericLoggerUtils.dateString(date) + "\t" +
+ keys.filter("date" != _).map(value(_)).mkString("\t");
+
+ def value(k: String) = {
+ map.orElse(extra.getOrElse(Map[String, Any]()))(k);
+ }
+}
+
+class LoggableFromJson(val json: String) extends LoggablePropertyBag {
+ val obj = new JSONObject(json);
+ val date = new Date(obj.getLong("date"));
+ val keys = obj.sortedKeys().map(String.valueOf(_)).collect.toArray;
+ def value(k: String) = obj.get(k);
+ val tabDelimited =
+ GenericLoggerUtils.dateString(date) + "\t"+
+ keys.filter("date" != _).map(value(_)).mkString("\t");
+}
+
+object GenericLoggerUtils {
+ lazy val df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ");
+ def dateString(date: Date) = df.format(date);
+ var extraPropertiesFunction: Option[() => Map[String, String]] = None;
+ def setExtraPropertiesFunction(f: () => Map[String, String]) {
+ extraPropertiesFunction = Some(() => {
+ try {
+ f();
+ } catch {
+ case e => withoutExtraProperties {
+ exceptionlog(e);
+ Map[String, String]();
+ }
+ }
+ });
+ }
+ def getExtraProperties: Option[Map[String, String]] = {
+ if (shouldGetExtraProperties) {
+ withoutExtraProperties(extraPropertiesFunction.map(_()));
+ } else {
+ None;
+ }
+ }
+
+ val registeredWranglers =
+ new ConcurrentHashMap[String, SetWrapper[WeakReference[LogWrangler]]];
+ def registerWrangler(name: String, wrangler: LogWrangler) {
+ wranglers(name) += wrangler.ref;
+ }
+ def clearWrangler(name: String, wrangler: LogWrangler) {
+ wranglers(name) -= wrangler.ref;
+ }
+ def wranglers(name: String) = {
+ if (! registeredWranglers.containsKey(name)) {
+ val set1 = Conversions.convertSet(
+ new CopyOnWriteArraySet[WeakReference[LogWrangler]]);
+ val set2 = registeredWranglers.putIfAbsent(
+ name, set1);
+ if (set2 == null) {
+ set1
+ } else {
+ set2
+ }
+ } else {
+ registeredWranglers.get(name);
+ }
+ }
+ def tellWranglers(name: String, lpb: LoggablePropertyBag) {
+ for (w <- wranglers(name)) {
+ w.get.foreach(_.tell(lpb));
+ if (! w.isValid) {
+ wranglers(name) -= w;
+ }
+ }
+ }
+
+ val shouldGetExtraProperties_var =
+ new NoninheritedDynamicVariable[Boolean](true);
+ def withoutExtraProperties[E](block: => E): E = {
+ shouldGetExtraProperties_var.withValue(false)(block);
+ }
+ def shouldGetExtraProperties = shouldGetExtraProperties_var.value;
+}
+
+class GenericLogger(path: String, logName: String, rotateDaily: Boolean) {
+ val queue = new ConcurrentLinkedQueue[LoggablePropertyBag];
+
+ var loggerThread: Thread = null;
+ var currentLogDay:Date = null;
+ var logWriter: FileWriter = null;
+ var logBase = config.logDir;
+ def setLogBase(p: String) { logBase = p }
+
+ var echoToStdOut = false;
+ def setEchoToStdOut(e: Boolean) {
+ echoToStdOut = e;
+ }
+ def stdOutPrefix = logName+": "
+
+ def initLogWriter(logDay: Date) {
+ currentLogDay = logDay;
+
+ // if rotating, log filename is logBase/[path/]logName/logName-<date>.jslog
+ // otherwise, log filename is logBase/[path/]logName.jslog
+ var fileName =
+ if (rotateDaily) {
+ val df = new SimpleDateFormat("yyyy-MM-dd");
+ logName + "/" + logName + "-" + df.format(logDay) + ".jslog";
+ } else {
+ logName + ".jslog";
+ }
+ if (path != null && path.length > 0) {
+ fileName = path + "/" + fileName;
+ }
+ val f = new File(logBase+"/"+fileName);
+ if (! f.getParentFile.exists) {
+ f.getParentFile().mkdirs();
+ }
+ logWriter = new FileWriter(f, true);
+ }
+
+ def rotateIfNecessary(messageDate: Date) {
+ if (rotateDaily) {
+ if (!((messageDate.getYear == currentLogDay.getYear) &&
+ (messageDate.getMonth == currentLogDay.getMonth) &&
+ (messageDate.getDate == currentLogDay.getDate))) {
+ logWriter.flush();
+ logWriter.close();
+ initLogWriter(messageDate);
+ }
+ }
+ }
+
+ def flush() {
+ flush(java.lang.Integer.MAX_VALUE);
+ }
+ def close() {
+ logWriter.close();
+ }
+
+ def flush(n: Int) = synchronized {
+ var count = 0;
+ while (count < n && ! queue.isEmpty()) {
+ val lpb = queue.poll();
+ rotateIfNecessary(lpb.date);
+ logWriter.write(lpb.json+"\n");
+ if (echoToStdOut)
+ print(lpb.tabDelimited.split("\n").mkString(stdOutPrefix, "\n"+stdOutPrefix, "\n"));
+ count += 1;
+ }
+ if (count > 0) {
+ logWriter.flush();
+ }
+ count;
+ }
+
+ def start() {
+ initLogWriter(new Date());
+
+ loggerThread = new Thread("GenericLogger "+logName) {
+ this.setDaemon(true);
+ override def run() {
+ while (true) {
+ if (queue.isEmpty()) {
+ Thread.sleep(500);
+ } else {
+ flush(1000);
+ }
+ }
+ }
+ }
+ main.loggers += this;
+ loggerThread.start();
+ }
+
+ def log(lpb: LoggablePropertyBag) {
+ if (loggerThread != null) {
+ queue.offer(lpb);
+ GenericLoggerUtils.tellWranglers(logName, lpb);
+ }
+ }
+ def logObject(scr: Scriptable) {
+ log(new LoggableFromScriptable(
+ scr, GenericLoggerUtils.getExtraProperties));
+ }
+ def log[T](m: scala.collection.Map[String, T]) {
+ log(new LoggableFromMap(
+ m, GenericLoggerUtils.getExtraProperties));
+ }
+ def log(s: String) {
+ log(Map("message" -> s));
+ }
+ def apply(s: String) {
+ log(s);
+ }
+ def apply(scr: Scriptable) {
+ logObject(scr);
+ }
+ def apply[T](m: scala.collection.Map[String, T]) {
+ log(m);
+ }
+}
+
+object profiler extends GenericLogger("backend", "profile", false) {
+ def apply(id: String, op: String, method: String, path: String, countAndNanos: (Long, Long)) {
+ if (loggerThread != null)
+ log(id+":"+op+":"+method+":"+path+":"+
+ Math.round(countAndNanos._2/1000)+
+ (if (countAndNanos._1 > 1) ":"+countAndNanos._1 else ""));
+ }
+// def apply(state: RequestState, op: String, nanos: long) {
+// apply(state.requestId, op, state.req.getMethod(), state.req.getRequestURI(), nanos);
+// }
+
+ def time =
+ System.nanoTime();
+
+ // thread-specific stuff.
+ val map = new ThreadLocal[HashMap[String, Any]] {
+ override def initialValue = new HashMap[String, Any];
+ }
+ val idGen = new java.util.concurrent.atomic.AtomicLong(0);
+ val id = new ThreadLocal[Long] {
+ override def initialValue = idGen.getAndIncrement();
+ }
+ def reset() = {
+ map.remove();
+ id.remove();
+ }
+
+ def record(key: String, time: Long) {
+ map.get()(key) = (1L, time);
+ }
+ def recordCumulative(key: String, time: Long) {
+ map.get()(key) = map.get().getOrElse(key, (0L, 0L)) match {
+ case (count: Long, time0: Long) => (count+1, time0+time);
+ case _ => { } // do nothing, but maybe shoud error.
+ }
+ }
+ def print() {
+ for ((k, t) <- map.get()) {
+ profiler(""+id.get(), k, "/", "/", t match {
+ case (count: Long, time0: Long) => (count, time0);
+ case _ => (-1L, -1L);
+ });
+ }
+ }
+
+ def printTiming[E](name: String)(block: => E): E = {
+ val startTime = time;
+ val r = block;
+ val endTime = time;
+ println(name+": "+((endTime - startTime)/1000)+" us.");
+ r;
+ }
+}
+
+object eventlog extends GenericLogger("backend", "server-events", true) {
+ start();
+}
+
+object streaminglog extends GenericLogger("backend", "streaming-events", true) {
+ start();
+}
+
+object exceptionlog extends GenericLogger("backend", "exceptions", true) {
+ def apply(e: Throwable) {
+ val s = new StringWriter;
+ e.printStackTrace(new PrintWriter(s));
+ log(Map(
+ "description" -> e.toString(),
+ "trace" -> s.toString()));
+ }
+
+ echoToStdOut = config.devMode
+ override def stdOutPrefix = "(exlog): ";
+
+ start();
+}
+
+// object dprintln extends GenericLogger("backend", "debug", true) {
+// echoToStdOut = config.devMode;
+// }
+
+class STFULogger extends org.mortbay.log.Logger {
+ def debug(m: String, a0: Object, a1: Object) { }
+ def debug(m: String, t: Throwable) { }
+ def getLogger(m: String) = { this }
+ def info(m: String, a0: Object, a2: Object) { }
+ def isDebugEnabled() = { false }
+ def setDebugEnabled(t: Boolean) { }
+ def warn(m: String, a0: Object, a1: Object) { }
+ def warn(m: String, t: Throwable) { }
+}
+
+case class Percentile(count: Int, p50: Int, p90: Int, p95: Int, p99: Int, max: Int);
+
+object cometlatencies {
+ var latencies = new java.util.concurrent.ConcurrentLinkedQueue[Int];
+ def register(t: Int) = latencies.offer(t);
+
+ var loggerThread: Thread = null;
+ var lastCount: Option[Map[String, Int]] = None;
+ var lastStats: Option[Percentile] = None;
+ def start() {
+ loggerThread = new Thread("latencies logger") {
+ this.setDaemon(true);
+ override def run() {
+ while(true) {
+ Thread.sleep(60*1000); // every minute
+ try {
+ val oldLatencies = latencies;
+ latencies = new java.util.concurrent.ConcurrentLinkedQueue[Int];
+ val latArray = oldLatencies.toArray().map(_.asInstanceOf[int]);
+ Sorting.quickSort(latArray);
+ def pct(p: Int) =
+ if (latArray.length > 0)
+ latArray(Math.floor((p/100.0)*latArray.length).toInt);
+ else
+ 0;
+ def s(a: Any) = String.valueOf(a);
+ lastStats = Some(Percentile(latArray.length,
+ pct(50), pct(90), pct(95), pct(99),
+ if (latArray.length > 0) latArray.last else 0));
+ eventlog.log(Map(
+ "type" -> "streaming-message-latencies",
+ "count" -> s(lastStats.get.count),
+ "p50" -> s(lastStats.get.p50),
+ "p90" -> s(lastStats.get.p90),
+ "p95" -> s(lastStats.get.p95),
+ "p99" -> s(lastStats.get.p99),
+ "max" -> s(lastStats.get.max)));
+ lastCount = Some({
+ val c = Class.forName("net.appjet.ajstdlib.Comet$");
+ c.getDeclaredMethod("connectionStatus")
+ .invoke(c.getDeclaredField("MODULE$").get(null))
+ }.asInstanceOf[Map[String, Int]]);
+ eventlog.log(
+ Map("type" -> "streaming-connection-count") ++
+ lastCount.get.elements.map(p => (p._1, String.valueOf(p._2))));
+ } catch {
+ case e: Exception => {
+ exceptionlog(e);
+ }
+ }
+ }
+ }
+ }
+ loggerThread.start();
+ }
+
+ start();
+}
+
+object executionlatencies extends GenericLogger("backend", "latency", true) {
+ start();
+
+ def time = System.currentTimeMillis();
+}
+
+abstract class LogWrangler {
+ def tell(lpb: LoggablePropertyBag);
+ def tell(json: String) { tell(new LoggableFromJson(json)); }
+ lazy val ref = new WeakReference(this);
+
+ def watch(logName: String) {
+ GenericLoggerUtils.registerWrangler(logName, this);
+ }
+}
+
+// you probably want to subclass this, or at least set data.
+class FilterWrangler(
+ `type`: String,
+ filter: LoggablePropertyBag => Boolean,
+ field: String) extends LogWrangler {
+ def tell(lpb: LoggablePropertyBag) {
+ if ((`type` == null || lpb.`type` == `type`) &&
+ (filter == null || filter(lpb))) {
+ val entry = lpb.value(field);
+ data(lpb.date, entry);
+ }
+ }
+ var data: (Date, Any) => Unit = null;
+ def setData(data0: (Date, Any) => Unit) {
+ data = data0;
+ }
+}
+
+class TopNWrangler(n: Int, `type`: String,
+ filter: LoggablePropertyBag => Boolean,
+ field: String)
+ extends FilterWrangler(`type`, filter, field) {
+ val entries = new ConcurrentHashMap[String, AtomicInteger]();
+ def sortedEntries = {
+ Sorting.stableSort(
+ convertMap(entries).toSeq,
+ (p1: (String, AtomicInteger), p2: (String, AtomicInteger)) =>
+ p1._2.get() > p2._2.get());
+ }
+ def count = {
+ (convertMap(entries) :\ 0) { (x, y) => x._2.get() + y }
+ }
+
+ def topNItems(n: Int): Array[(String, Int)] =
+ sortedEntries.take(n).map(p => (p._1, p._2.get())).toArray;
+ def topNItems: Array[(String, Int)] = topNItems(n);
+
+ data = (date: Date, value: Any) => {
+ val entry = value.asInstanceOf[String];
+ val i =
+ if (! entries.containsKey(entry)) {
+ val newInt = new AtomicInteger(0);
+ val oldInt = entries.putIfAbsent(entry, newInt);
+ if (oldInt == null) { newInt } else { oldInt }
+ } else {
+ entries.get(entry);
+ }
+ i.incrementAndGet();
+ }
+}
diff --git a/trunk/infrastructure/net.appjet.oui/main.scala b/trunk/infrastructure/net.appjet.oui/main.scala
new file mode 100644
index 0000000..42cd268
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/main.scala
@@ -0,0 +1,386 @@
+/**
+ * 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 net.appjet.bodylock.{BodyLock, Executable};
+
+import java.io.File;
+import java.util.{Properties, Date};
+import java.lang.annotation.Annotation;
+import java.text.SimpleDateFormat;
+
+import scala.collection.mutable.{HashMap, SynchronizedMap, HashSet};
+import scala.collection.jcl.{IterableWrapper, Conversions};
+
+import org.mortbay.thread.QueuedThreadPool;
+import org.mortbay.jetty.servlet.{Context, HashSessionIdManager, FilterHolder, ServletHolder};
+import org.mortbay.jetty.handler.{HandlerCollection, RequestLogHandler, HandlerList};
+import org.mortbay.jetty.{Server, NCSARequestLog, Request, Response};
+import org.mortbay.servlet.GzipFilter;
+
+// removed due to license restrictions
+// import com.oreilly.servlet.MultipartFilter;
+
+import net.appjet.common.util.{BetterFile, HttpServletRequestFactory};
+import net.appjet.common.cli._;
+import net.appjet.bodylock.JSCompileException;
+
+import Util.enumerationToRichEnumeration;
+
+object main {
+ val startTime = new java.util.Date();
+
+ def quit(status: Int) {
+ java.lang.Runtime.getRuntime().halt(status);
+ }
+
+ def setupFilesystem() {
+ val logdir = new File(config.logDir+"/backend/access");
+ if (! logdir.isDirectory())
+ if (! logdir.mkdirs())
+ quit(1);
+ }
+
+ val options =
+ for (m <- config.allProperties if (m.getAnnotation(classOf[ConfigParam]) != null)) yield {
+ val cp = m.getAnnotation(classOf[ConfigParam])
+ new CliOption(m.getName(), cp.value(), if (cp.argName().length > 0) Some(cp.argName()) else None);
+ }
+
+ def printUsage() {
+ println("\n--------------------------------------------------------------------------------");
+ println("usage:");
+ println((new CliParser(options)).usage);
+ println("--------------------------------------------------------------------------------\n");
+ }
+
+ def extractOptions(args: Array[String]) {
+ val parser = new CliParser(options);
+ val opts =
+ try {
+ parser.parseOptions(args)._1;
+ } catch {
+ case e: ParseException => {
+ println("error: "+e.getMessage());
+ printUsage();
+ System.exit(1);
+ null;
+ }
+ }
+ if (opts.contains("configFile")) {
+ val p = new Properties();
+ p.load(new java.io.FileInputStream(opts("configFile")));
+ extractOptions(p);
+ }
+ for ((k, v) <- opts) {
+ config.values(k) = v;
+ }
+ }
+
+ def extractOptions(props: Properties) {
+ for (k <- for (o <- props.propertyNames()) yield o.asInstanceOf[String]) {
+ config.values(k) = props.getProperty(k);
+ }
+ }
+
+ val startupExecutable = (new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "onstartup.js"))).executable;
+ def runOnStartup() {
+ execution.runOutOfBand(startupExecutable, "Startup", None, { error =>
+ error match {
+ case e: JSCompileException => { }
+ case e: Throwable => { e.printStackTrace(); }
+ case (sc: Int, msg: String) => { println(msg); }
+ case x => println(x);
+ }
+ System.exit(1);
+ });
+ }
+
+ lazy val shutdownExecutable = (new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "onshutdown.js"))).executable;
+ def runOnShutdown() {
+ execution.runOutOfBand(shutdownExecutable, "Shutdown", None, { error =>
+ error match {
+ case e: JSCompileException => { }
+ case e: Throwable => { }
+ case (sc: Int, msg: String) => { println(msg); }
+ case x => println(x);
+ }
+ });
+ }
+
+ def runOnSars(q: String) = {
+ val ec = execution.runOutOfBand(execution.sarsExecutable, "SARS", Some(Map("sarsRequest" -> q)), { error =>
+ error match {
+ case e: JSCompileException => { throw e; }
+ case e: Throwable => { exceptionlog(e); throw e; }
+ case (sc: Int, msg: String) => { println(msg); throw new RuntimeException(""+sc+": "+msg) }
+ case x => { println(x); throw new RuntimeException(x.toString()) }
+ }
+ });
+ ec.attributes.get("sarsResponse").map(_.toString());
+ }
+
+ def stfu() {
+ System.setProperty("org.mortbay.log.class", "net.appjet.oui.STFULogger");
+ System.setProperty("com.mchange.v2.log.MLog", "com.mchange.v2.log.FallbackMLog");
+ System.setProperty("com.mchange.v2.log.FallbackMLog.DEFAULT_CUTOFF_LEVEL", "OFF");
+ }
+ var server: Server = null;
+ var sarsServer: net.appjet.common.sars.SarsServer = null;
+
+ var loggers = new HashSet[GenericLogger];
+ def main(args: Array[String]) {
+ val etherpadProperties = getClass.getResource("/etherpad.properties");
+ if (etherpadProperties != null) {
+ val p = new Properties();
+ p.load(etherpadProperties.openStream);
+ extractOptions(p);
+ }
+ extractOptions(args);
+
+ if (! config.verbose)
+ stfu();
+ setupFilesystem();
+ if (config.devMode)
+ config.print;
+ if (config.profile)
+ profiler.start();
+ if (config.listenMonitoring != "0:0")
+ monitoring.startMonitoringServer();
+
+ // this needs a better place.
+ if (config.devMode)
+ BodyLock.map = Some(new HashMap[String, String] with SynchronizedMap[String, String]);
+
+ server = new Server();
+ if (config.maxThreads > 0)
+ server.setThreadPool(new QueuedThreadPool(config.maxThreads));
+ else
+ server.setThreadPool(new QueuedThreadPool());
+ // set up socket connectors
+ val nioconnector = new CometSelectChannelConnector;
+ var sslconnector: CometSslSelectChannelConnector = null;
+ nioconnector.setPort(config.listenPort);
+ if (config.listenHost.length > 0)
+ nioconnector.setHost(config.listenHost);
+ if (config.listenSecurePort == 0) {
+ server.setConnectors(Array(nioconnector));
+ } else {
+ sslconnector = new CometSslSelectChannelConnector;
+ sslconnector.setPort(config.listenSecurePort);
+ if (config.listenSecureHost.length > 0)
+ sslconnector.setHost(config.listenSecureHost);
+ if (! config.sslKeyStore_isSet) {
+ val url = getClass.getResource("/mirror/snakeoil-ssl-cert");
+ if (url != null)
+ sslconnector.setKeystore(url.toString());
+ else
+ sslconnector.setKeystore(config.sslKeyStore);
+ } else {
+ sslconnector.setKeystore(config.sslKeyStore);
+ }
+ sslconnector.setPassword(config.sslStorePassword);
+ sslconnector.setKeyPassword(config.sslKeyPassword);
+ sslconnector.setTrustPassword(config.sslStorePassword);
+ sslconnector.setExcludeCipherSuites(Array[String](
+ "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
+ "SSL_DHE_RSA_WITH_DES_CBC_SHA",
+ "SSL_DHE_DSS_WITH_DES_CBC_SHA",
+ "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA",
+ "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA",
+ "SSL_RSA_WITH_DES_CBC_SHA",
+ "SSL_RSA_EXPORT_WITH_RC4_40_MD5",
+ "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA",
+ "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA",
+ "SSL_RSA_WITH_NULL_MD5",
+ "SSL_RSA_WITH_NULL_SHA",
+ "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA",
+ "SSL_DH_anon_WITH_DES_CBC_SHA",
+ "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5",
+ "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"));
+ server.setConnectors(Array(nioconnector, sslconnector));
+ }
+
+ // set up Context and Servlet
+ val handler = new Context(server, "/", Context.NO_SESSIONS | Context.NO_SECURITY);
+ handler.addServlet(new ServletHolder(new OuiServlet), "/");
+
+// val filterHolder = new FilterHolder(new MultipartFilter());
+// filterHolder.setInitParameter("uploadDir", System.getProperty("java.io.tmpdir"));
+// handler.addFilter(filterHolder, "/*", 1);
+
+ global.context = handler;
+
+ // set up apache-style logging
+ val requestLogHandler = new RequestLogHandler();
+ val requestLog = new NCSARequestLog(config.logDir+"/backend/access/access-yyyy_mm_dd.request.log") {
+ override def log(req: Request, res: Response) {
+ try {
+ if (config.devMode || config.specialDebug)
+ super.log(req, res);
+ else if (res.getStatus() != 200 || config.transportPrefix == null || ! req.getRequestURI().startsWith(config.transportPrefix))
+ super.log(req, res);
+ val d = new Date();
+ appstats.stati.foreach(_(if (res.getStatus() < 0) 404 else res.getStatus()).hit(d));
+ } catch {
+ case e => { exceptionlog("Error writing to log?"); exceptionlog(e); }
+ }
+ }
+ };
+ requestLog.setRetainDays(365);
+ requestLog.setAppend(true);
+ requestLog.setExtended(true);
+ requestLog.setLogServer(true);
+ requestLog.setLogLatency(true);
+ requestLog.setLogTimeZone("PST");
+ requestLogHandler.setRequestLog(requestLog);
+
+ // set handlers with server
+ val businessHandlers = new HandlerList();
+ businessHandlers.setHandlers(Array(handler));
+ val allHandlers = new HandlerCollection();
+ allHandlers.setHandlers(Array(businessHandlers, requestLogHandler));
+ server.setHandler(allHandlers);
+
+ // fix slow startup bug
+ server.setSessionIdManager(new HashSessionIdManager(new java.util.Random()));
+
+ // run the onStartup script.
+ runOnStartup();
+
+ // preload some runners, if necessary.
+ if (config.preloadRunners > 0) {
+ val b = new java.util.concurrent.CountDownLatch(config.preloadRunners);
+ for (i <- 0 until config.preloadRunners)
+ (new Thread {
+ ScopeReuseManager.freeRunner(ScopeReuseManager.newRunner);
+ b.countDown();
+ }).start();
+ while (b.getCount() > 0) {
+ b.await();
+ }
+ println("Preloaded "+config.preloadRunners+" runners.");
+ }
+
+ // start SARS server.
+ if (config.listenSarsPort > 0) {
+ try {
+ import net.appjet.common.sars._;
+ sarsServer = new SarsServer(config.sarsAuthKey,
+ new SarsMessageHandler { override def handle(q: String) = runOnSars(q) },
+ if (config.listenSarsHost.length > 0) Some(config.listenSarsHost) else None,
+ config.listenSarsPort);
+ sarsServer.daemon = true;
+ sarsServer.start();
+ } catch {
+ case e: java.net.SocketException => {
+ println("SARS: A socket exception occurred: "+e.getMessage()+" on SARS server at "+config.listenSarsHost+":"+config.listenSarsPort);
+ java.lang.Runtime.getRuntime().halt(1);
+ }
+ }
+ }
+
+ // start server
+ java.lang.Runtime.getRuntime().addShutdownHook(new Thread() {
+ override def run() {
+ val df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ");
+ def printts(str: String) {
+ println("["+df.format(new Date())+"]: "+str);
+ }
+ printts("Shutting down...");
+ handler.setShutdown(true);
+ Thread.sleep(if (config.devMode) 500 else 3000);
+ printts("...done, running onshutdown.");
+ runOnShutdown();
+ printts("...done, stopping server.");
+ server.stop();
+ server.join();
+ printts("...done, flushing logs.");
+ for (l <- loggers) { l.flush(); l.close(); }
+ printts("...done.");
+ }
+ });
+
+ def socketError(c: org.mortbay.jetty.Connector, e: java.net.SocketException) {
+ var msg = e.getMessage();
+ println("SOCKET ERROR: "+msg+" - "+(c match {
+ case null => "(unknown socket)";
+ case x => {
+ (x.getHost() match {
+ case null => "localhost";
+ case y => y;
+ })+":"+x.getPort();
+ }
+ }));
+ if (msg.contains("Address already in use")) {
+ println("Did you make sure that ports "+config.listenPort+" and "+config.listenSecurePort+" are not in use?");
+ }
+ if (msg.contains("Permission denied")) {
+ println("Perhaps you need to run as the root user or as an Administrator?");
+ }
+ }
+
+ var c: org.mortbay.jetty.Connector = null;
+
+ try {
+ c = nioconnector;
+ c.open();
+ if (sslconnector != null) {
+ c = sslconnector;
+ c.open();
+ }
+ c = null;
+ allHandlers.start();
+ server.start();
+ } catch {
+ case e: java.net.SocketException => {
+ socketError(c, e);
+ java.lang.Runtime.getRuntime().halt(1);
+ }
+ case e: org.mortbay.util.MultiException => {
+ println("SERVER ERROR: Couldn't start server; multiple errors.");
+ for (i <- new IterableWrapper[Throwable] { override val underlying = e.getThrowables.asInstanceOf[java.util.List[Throwable]] }) {
+ i match {
+ case se: java.net.SocketException => {
+ socketError(c, se);
+ }
+ case e =>
+ println("SERVER ERROR: Couldn't start server: "+i.getMessage());
+ }
+ }
+ java.lang.Runtime.getRuntime().halt(1);
+ }
+ case e => {
+ println("SERVER ERROR: Couldn't start server: "+e.getMessage());
+ java.lang.Runtime.getRuntime().halt(1);
+ }
+ }
+
+ println("HTTP server listening on http://"+
+ (if (config.listenHost.length > 0) config.listenHost else "localhost")+
+ ":"+config.listenPort+"/");
+ if (config.listenSecurePort > 0)
+ println("HTTPS server listening on https://"+
+ (if (config.listenSecureHost.length > 0) config.listenSecureHost else "localhost")+
+ ":"+config.listenSecurePort+"/");
+ if (config.listenSarsPort > 0)
+ println("SARS server listening on "+
+ (if (config.listenSarsHost.length > 0) config.listenSarsHost else "localhost")+
+ ":"+config.listenSarsPort);
+ }
+}
diff --git a/trunk/infrastructure/net.appjet.oui/monitoring.scala b/trunk/infrastructure/net.appjet.oui/monitoring.scala
new file mode 100644
index 0000000..e380b84
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/monitoring.scala
@@ -0,0 +1,125 @@
+/**
+ * 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;
+
+object monitoring {
+
+ def startMonitoringServer() {
+ // remote JMX monitoring
+ // see: http://java.sun.com/javase/6/docs/technotes/guides/management/agent.html
+
+ import java.rmi.registry.LocateRegistry;
+ import java.lang.management.ManagementFactory;
+ import javax.management.ObjectName;
+ import javax.management.remote.{JMXServiceURL, JMXConnectorServerFactory};
+
+ def REGISTRY_PORT = config.listenMonitoringPrimaryPort;
+ def SECONDARY_PORT = config.listenMonitoringSecondaryPort;
+ System.setProperty("java.rmi.server.randomIDs", "true");
+
+ // we must set 'java.rmi.server.hostname' to the host that the client machine
+ // should connect to; in production, it will be the dashboard host, but the property
+ // can also be specified on the command-line, in which case it takes precedence.
+ var listenHost = config.listenMonitoringHost
+ if (listenHost == null || listenHost.length == 0) listenHost = "localhost";
+ if (System.getProperty("java.rmi.server.hostname") == null) {
+ System.setProperty("java.rmi.server.hostname", listenHost);
+ }
+ else {
+ listenHost = System.getProperty("java.rmi.server.hostname");
+ }
+
+ LocateRegistry.createRegistry(REGISTRY_PORT);
+ val mbs = ManagementFactory.getPlatformMBeanServer();
+
+ mbs.createMBean(classOf[JSExecutor].getName, new ObjectName("net.appjet:type=JSExecutor"));
+
+ val env = new java.util.HashMap[String,Object]();
+ //val csf = new javax.rmi.ssl.SslRMIClientSocketFactory();
+ //val ssf = new javax.rmi.ssl.SslRMIServerSocketFactory();
+ //env.put(javax.management.remote.rmi.RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);
+ //env.put(javax.management.remote.rmi.RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, ssf);
+ val PASSWORD_FILE_PATH = "data/jconsole-password.properties";
+ val ACCESS_FILE_PATH = "data/jconsole-access.properties";
+ def writeStringToFile(str: String, path: String) {
+ val out = new java.io.PrintStream(new java.io.BufferedOutputStream(
+ new java.io.FileOutputStream(path)));
+ out.println(str);
+ out.close;
+ }
+ if (! new java.io.File(PASSWORD_FILE_PATH).exists) {
+ System.err.println("Creating "+PASSWORD_FILE_PATH+"...");
+ writeStringToFile("appjet foo", PASSWORD_FILE_PATH);
+ }
+ if (! new java.io.File(ACCESS_FILE_PATH).exists) {
+ System.err.println("Creating "+ACCESS_FILE_PATH+"...");
+ writeStringToFile("appjet readwrite", ACCESS_FILE_PATH);
+ }
+ env.put("jmx.remote.x.password.file", PASSWORD_FILE_PATH);
+ env.put("jmx.remote.x.access.file", ACCESS_FILE_PATH);
+ val url = new JMXServiceURL(
+ "service:jmx:rmi://localhost:"+SECONDARY_PORT+"/jndi/rmi://localhost:"+REGISTRY_PORT+"/server");
+ try {
+ val cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
+ cs.start();
+ System.err.println("Monitor server listening on "+listenHost+":{"+REGISTRY_PORT+
+ ","+SECONDARY_PORT+"}");
+ }
+ catch {
+ case e => {
+ System.err.println("!!Could not start monitor server on "+listenHost+":{"+REGISTRY_PORT+
+ ","+SECONDARY_PORT+"} due to:");
+ e.printStackTrace(System.err);
+ }
+ }
+ }
+
+}
+
+trait JSExecutorMXBean {
+ def executeJS(code: String): String;
+}
+
+class JSExecutor extends JSExecutorMXBean {
+ import org.mozilla.javascript.{Context,ContextFactory,ContextAction};
+ import org.mozilla.javascript.tools.ToolErrorReporter;
+ import org.mozilla.javascript.tools.shell.{Global, ShellContextFactory};
+
+ def executeJS(code: String): String = {
+ val outStream = new java.io.ByteArrayOutputStream;
+ val out = new java.io.PrintStream(outStream, true, "UTF-8");
+
+ val contextFactory = new ShellContextFactory;
+ try {
+ contextFactory.call(new ContextAction { def run(cx: Context): Object = {
+ val global = new Global(cx);
+ global.setOut(out);
+ global.setErr(out);
+ val errorReporter = new ToolErrorReporter(false, global.getErr);
+ val result = cx.evaluateString(global, code, "<script>", 1, null);
+ out.println(Context.toString(result));
+ null;
+ } });
+ }
+ catch {
+ case e => {
+ e.printStackTrace(out);
+ }
+ }
+ return new String(outStream.toByteArray, "UTF-8");
+ }
+}
diff --git a/trunk/infrastructure/net.appjet.oui/network.scala b/trunk/infrastructure/net.appjet.oui/network.scala
new file mode 100644
index 0000000..2965b19
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/network.scala
@@ -0,0 +1,50 @@
+/**
+ * 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 org.mortbay.jetty.nio.SelectChannelConnector;
+import org.mortbay.jetty.security.{SslSelectChannelConnector, SslHttpChannelEndPoint};
+import org.mortbay.io.nio.SelectorManager;
+import org.mortbay.io.Buffers;
+import javax.net.ssl.SSLEngine;
+
+import java.nio.channels.{SocketChannel, SelectionKey};
+
+trait KnowsAboutDispatch extends SelectChannelConnector.ConnectorEndPoint {
+ def isDispatched: Boolean;
+}
+
+class CometConnectorEndPoint(channel: SocketChannel, selectSet: SelectorManager#SelectSet, key: SelectionKey)
+ extends SelectChannelConnector.ConnectorEndPoint(channel, selectSet, key) with KnowsAboutDispatch {
+ def isDispatched = _dispatched;
+}
+
+class CometSelectChannelConnector extends SelectChannelConnector {
+ override def newEndPoint(channel: SocketChannel, selectSet: SelectorManager#SelectSet, key: SelectionKey) =
+ new CometConnectorEndPoint(channel, selectSet, key);
+}
+
+class CometSslHttpChannelEndPoint(buffers: Buffers, channel: SocketChannel, selectSet: SelectorManager#SelectSet,
+ key: SelectionKey, engine: SSLEngine)
+ extends SslHttpChannelEndPoint(buffers, channel, selectSet, key, engine) with KnowsAboutDispatch {
+ def isDispatched = _dispatched;
+}
+
+class CometSslSelectChannelConnector extends SslSelectChannelConnector {
+ override def newEndPoint(channel: SocketChannel, selectSet: SelectorManager#SelectSet, key: SelectionKey) =
+ new CometSslHttpChannelEndPoint(this, channel, selectSet, key, createSSLEngine());
+}
diff --git a/trunk/infrastructure/net.appjet.oui/servermodel.scala b/trunk/infrastructure/net.appjet.oui/servermodel.scala
new file mode 100644
index 0000000..1e2363f
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/servermodel.scala
@@ -0,0 +1,209 @@
+/**
+ * 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.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import scala.collection.mutable.{HashSet, SynchronizedSet};
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.appjet.bodylock.{BodyLock, JSCompileException};
+
+object ScopeReuseManager {
+ // reset handling.
+ // val filesToWatch = new ConcurrentHashMap[CachedFile, Unit];
+ // def watch(libs: DiskLibrary*) {
+ // for(lib <- libs) {
+ // filesToWatch.put(lib,());
+ // }
+ // }
+ val t = new java.util.TimerTask {
+ def run() {
+ try {
+ // val t1 = System.currentTimeMillis;
+ // var doReset = false;
+ // val libIter = filesToWatch.keySet.iterator;
+ // while (libIter.hasNext) {
+ // if (libIter.next.hasBeenModified) {
+ // doReset = true;
+ // }
+ // }
+ // val t2 = System.currentTimeMillis;
+ // val elapsedMs = (t2 -t1).toInt;
+ // if (elapsedMs >= 500) {
+ // eventlog(Map(
+ // "type" -> "event",
+ // "event" -> "scopereusefilewatcher-slowtask",
+ // "elapsedMs" -> elapsedMs
+ // ));
+ // }
+ if (FileCache.testFiles()) {
+ reset();
+ }
+ } catch {
+ case e => e.printStackTrace();
+ }
+ }
+ }
+ val timerPeriod = if (! config.devMode) 5000 else 500;
+ val timer = new java.util.Timer(true);
+ timer.schedule(t, timerPeriod, timerPeriod);
+
+ // scope handling
+ val mainLib = new VariableDiskLibrary("main.js");
+ val preambleLib = new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "preamble.js"));
+ val postambleLib = new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "postamble.js"));
+ def mainExecutable = mainLib.executable;
+ def preambleExecutable = preambleLib.executable;
+ def postambleExecutable = postambleLib.executable;
+
+ val mainGlobalScope = BodyLock.newScope;
+
+ val nextId = new AtomicLong(0);
+ val freeRunners = new ConcurrentLinkedQueue[Runner]();
+ var lastReset = new AtomicLong(0);
+ val resetLock = new ReentrantReadWriteLock(true);
+ def readLocked[E](block: => E): E = {
+ resetLock.readLock().lock();
+ try {
+ block;
+ } finally {
+ resetLock.readLock().unlock();
+ }
+ }
+ def writeLocked[E](block: => E): E = {
+ resetLock.writeLock().lock();
+ try {
+ block;
+ } finally {
+ resetLock.writeLock().unlock();
+ }
+ }
+
+ case class Runner(val globalScope: org.mozilla.javascript.Scriptable) {
+ var count = 0;
+ val created = timekeeper.time;
+ val id = nextId.incrementAndGet();
+ val mainScope = BodyLock.subScope(globalScope);
+ var reuseOk = true;
+ var trace: Option[Array[StackTraceElement]] = None;
+ override def finalize() {
+ trace.foreach(t => eventlog(Map(
+ "type" -> "error",
+ "error" -> "unreleased runner",
+ "runnerId" -> id,
+ "trace" -> t.mkString("\n"))));
+ super.finalize();
+ }
+ val attributes = new scala.collection.mutable.HashMap[String, Object];
+ }
+
+ def newRunner = {
+ // watch(mainLib, preambleLib, postambleLib);
+ val startTime = System.currentTimeMillis();
+ val scope = BodyLock.subScope(mainGlobalScope);
+ val r = Runner(scope);
+ ExecutionContextUtils.withContext(ExecutionContext(null, null, r)) {
+// scope.put("_appjetcontext_", scope, );
+ preambleExecutable.execute(scope);
+ mainExecutable.execute(r.mainScope);
+ postambleExecutable.execute(scope);
+ val endTime = System.currentTimeMillis();
+ eventlog(Map(
+ "type" -> "event",
+ "event" -> "runner-created",
+ "latency" -> (endTime - startTime).toString(),
+ "runnerId" -> r.id));
+ }
+ r;
+ }
+
+ def getRunner = readLocked {
+ val runner = freeRunners.poll();
+ if (runner == null) {
+ newRunner;
+ } else {
+ if (config.devMode) {
+ runner.trace = Some(Thread.currentThread().getStackTrace());
+ }
+ runner;
+ }
+ }
+
+ def getEmpty(block: Runner => Unit): Runner = readLocked {
+ // watch(preambleLib, postambleLib);
+ val scope = BodyLock.newScope;
+ val r = Runner(scope);
+// scope.put("_appjetcontext_", scope, ExecutionContext(null, null, r));
+ ExecutionContextUtils.withContext(ExecutionContext(null, null, r)) {
+ preambleExecutable.execute(scope);
+ block(r);
+ postambleExecutable.execute(scope);
+ }
+ r;
+ }
+
+ def getEmpty: Runner = getEmpty(r => {});
+
+ def freeRunner(r: Runner) {
+ r.trace = None;
+ if (r.reuseOk && r.created > lastReset.get()) {
+ freeRunners.offer(r);
+ } else {
+ if (r.reuseOk) {
+ eventlog(Map(
+ "type" -> "event",
+ "event" -> "runner-discarded",
+ "runnerId" -> r.id));
+ } else {
+ eventlog(Map(
+ "type" -> "event",
+ "event" -> "runner-retired",
+ "runnerId" -> r.id));
+ }
+ }
+ }
+
+ lazy val resetExecutable = (new FixedDiskLibrary(new SpecialJarOrNotFile(config.ajstdlibHome, "onreset.js"))).executable;
+ def runOnReset() {
+ execution.runOutOfBand(resetExecutable, "Reset", None, { error =>
+ error match {
+ case e: JSCompileException => { }
+ case e: Throwable => { exceptionlog(e); }
+ case (sc: Int, msg: String) => { exceptionlog("Reset failed: "+msg); }
+ case x => exceptionlog("Reset failed: "+String.valueOf(x));
+ }
+ });
+ }
+
+ def reset() = writeLocked {
+ eventlog(Map(
+ "type" -> "event",
+ "event" -> "files-reset"));
+ // filesToWatch.clear();
+ lastReset.set(timekeeper.time);
+ freeRunners.clear();
+ runOnReset();
+ }
+
+ eventlog(Map(
+ "type" -> "event",
+ "event" -> "server-restart"));
+}
+
diff --git a/trunk/infrastructure/net.appjet.oui/stats.scala b/trunk/infrastructure/net.appjet.oui/stats.scala
new file mode 100644
index 0000000..075182f
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/stats.scala
@@ -0,0 +1,220 @@
+/**
+ * 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.util.Date;
+
+import scala.collection.mutable.{HashMap, HashSet, Set, Map, ArrayBuffer};
+import scala.util.Sorting;
+
+trait BucketMap extends scala.collection.mutable.Map[int, BucketedLastHits] {
+ def t = 1000*60;
+ override def apply(s: int) = synchronized { getOrElseUpdate(s, new BucketedLastHits(t)) };
+ def counts = { val p = this; new scala.collection.Map.Projection[int, int] {
+ def size = p.size;
+ def get(s: int) = p.get(s).map(_.count);
+ def elements = p.elements.map(o => (o._1, o._2.count));
+ }};
+}
+
+abstract class BucketKeeper[A, B](val size: Long, val numbuckets: int, val noUpdate: Boolean) {
+ def this(size: Long, noUpdate: Boolean) =
+ this(size, Math.max(100, if (noUpdate) 1 else (size/60000).toInt), noUpdate)
+ def this(size: Long) = this(size, false);
+
+ val buckets = new Array[A](numbuckets);
+
+ val millisPerBucket = size/numbuckets;
+ var lastSwitch = System.currentTimeMillis();
+ var currentBucket = 0;
+
+ def withSyncUpdate[E](block: E): E = synchronized {
+ updateBuckets();
+ block;
+ }
+
+ protected def bucketAtTime(d: Date) = {
+ val msAgo = lastSwitch - d.getTime();
+ val bucketsAgo = Math.floor(msAgo/millisPerBucket).asInstanceOf[Int];
+ if (bucketsAgo < numbuckets) {
+ val bucket = (currentBucket - bucketsAgo + numbuckets) % numbuckets
+ // println("Applying to old bucket: "+bucket+" / current: "+currentBucket+", old count: "+count);
+ Some(bucket);
+ } else {
+ // println("No bucket found for: "+d);
+ None;
+ }
+ }
+
+ protected def updateBuckets(): Unit = {
+ if (! noUpdate) {
+ val now = System.currentTimeMillis();
+ while (now > lastSwitch + millisPerBucket) {
+ lastSwitch += millisPerBucket;
+ currentBucket = (currentBucket + 1) % numbuckets;
+ bucketClear(currentBucket);
+ }
+ }
+ }
+
+ protected def bucketClear(index: Int);
+ protected def bucketsInOrder: Seq[A] =
+ buckets.slice((currentBucket+1)%numbuckets, numbuckets) ++
+ buckets.slice(0, currentBucket)
+
+ def mergeBuckets(b: Seq[A]): B;
+
+ def history(bucketsPerSample: Int, numSamples: Int): Array[B] = withSyncUpdate {
+ val bseq = bucketsInOrder.reverse.take(bucketsPerSample*numSamples);
+ val sampleCount = Math.min(numSamples, bseq.length);
+ val samples =
+ for (i <- 0 until sampleCount) yield {
+ mergeBuckets(bseq.slice(i*bucketsPerSample, (i+1)*bucketsPerSample));
+ }
+ samples.reverse.toArray;
+ }
+ def latest(bucketsPerSample: Int): B = history(bucketsPerSample, 1)(0);
+ def count: B = withSyncUpdate { mergeBuckets(buckets); }
+
+ for (i <- 0 until numbuckets) {
+ bucketClear(i);
+ }
+}
+
+class BucketedUniques(size: Long, noUpdate: Boolean)
+extends BucketKeeper[Set[Any], Int](size, noUpdate) {
+ def this(size: Long) = this(size, false);
+
+ override protected def bucketClear(index: Int): Unit = {
+ buckets(index) = new HashSet[Any];
+ }
+
+ override def mergeBuckets(b: Seq[Set[Any]]) = {
+ b.foldLeft(scala.collection.immutable.Set[Any]())(_ ++ _).size;
+ }
+
+ def hit(d: Date, value: Any): Unit = withSyncUpdate {
+ for (bucket <- bucketAtTime(d)) {
+ buckets(bucket) += value;
+ }
+ }
+}
+
+class BucketedValueCounts(size: Long, noUpdate: Boolean)
+extends BucketKeeper[HashMap[String, Int], (Int, Map[String, Int])](size, noUpdate) {
+ def this(size: Long) = this(size, false);
+
+ override protected def bucketClear(index: Int): Unit = {
+ buckets(index) = new HashMap[String, Int];
+ }
+
+ override def mergeBuckets(b: Seq[HashMap[String, Int]]) = {
+ val out = new HashMap[String, Int];
+ var total = 0;
+ for (m <- b) {
+ for ((k, v) <- m) {
+ out(k) = out.getOrElse(k, 0) + v;
+ total += v;
+ }
+ }
+ (total, out);
+ }
+
+ def hit(d: Date, value: String, increment: Int): Unit = withSyncUpdate {
+ for (bucket <- bucketAtTime(d)) {
+ buckets(bucket)(value) =
+ buckets(bucket).getOrElse(value, 0)+increment;
+ }
+ }
+
+ def hit(d: Date, value: String): Unit = hit(d, value, 1);
+}
+
+
+/**
+ * Keeps track of how many "hits" in the last size milliseconds.
+ * Has granularity speicified by numbuckets.
+ */
+class BucketedLastHits(size: Long, noUpdate: Boolean)
+extends BucketKeeper[Int, Int](size, noUpdate) {
+ def this(size: Long) = this(size, false);
+
+ override protected def bucketClear(index: int): Unit = {
+ buckets(index) = 0;
+ }
+
+ override def mergeBuckets(b: Seq[Int]) = {
+ b.foldRight(0)(_+_);
+ }
+
+ def hit(d: Date): Unit = hit(d, 1);
+ def hit(d: Date, n: Int): Unit = withSyncUpdate {
+ for (bucket <- bucketAtTime(d)) {
+ buckets(bucket) = buckets(bucket) + n;
+ }
+ }
+}
+
+class BucketedLastHitsHistogram(size: Long, noUpdate: Boolean)
+extends BucketKeeper[ArrayBuffer[Int], Function1[Float, Int]](size, noUpdate) {
+ def this(size: Long) = this(size, false);
+
+ override protected def bucketClear(index: Int): Unit = {
+ buckets(index) = new ArrayBuffer[Int];
+ }
+
+ // elements will end up sorted.
+ protected def histogramFunction(elements: Array[Int]): Function1[Float, Int] = {
+ Sorting.quickSort(elements);
+ (percentile: Float) => {
+ if (elements.length == 0) {
+ 0
+ } else {
+ elements(
+ Math.round(percentile/100.0f*(elements.length-1)));
+ }
+ }
+ }
+
+ override def mergeBuckets(b: Seq[ArrayBuffer[Int]]) = {
+ val elements = new Array[Int](b.foldRight(0)(_.size + _));
+ var currentIndex = 0;
+ for (bucket <- b if bucket.length > 0) {
+ // copyToArray is broken through scala 2.7.5, fixed in trunk.
+ // bucket.copyToArray(allElements, currentIndex);
+ val bucketArray = bucket.toArray;
+ System.arraycopy(bucketArray, 0, elements, currentIndex, bucketArray.length);
+ currentIndex += bucket.size
+ }
+ histogramFunction(elements);
+ }
+
+ def hit(d: Date): Unit = hit(d, 1);
+ def hit(d: Date, n: Int): Unit = withSyncUpdate {
+ for (bucket <- bucketAtTime(d)) {
+ buckets(bucket) += n;
+ }
+ }
+}
+
+object appstats {
+ val minutelyStatus = new HashMap[int, BucketedLastHits] with BucketMap;
+ val hourlyStatus = new HashMap[int, BucketedLastHits] with BucketMap { override val t = 1000*60*60 };
+ val dailyStatus = new HashMap[int, BucketedLastHits] with BucketMap { override val t = 1000*60*60*24 };
+ val weeklyStatus = new HashMap[int, BucketedLastHits] with BucketMap { override val t = 1000*60*60*24*7 };
+ val stati = Array(minutelyStatus, hourlyStatus, dailyStatus, weeklyStatus);
+}
diff --git a/trunk/infrastructure/net.appjet.oui/synchronizer.scala b/trunk/infrastructure/net.appjet.oui/synchronizer.scala
new file mode 100644
index 0000000..2a6d9c1
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/synchronizer.scala
@@ -0,0 +1,69 @@
+/**
+ * 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;
+
+class Synchronizer {
+ import java.util.concurrent.locks.ReentrantLock;
+ import java.util.concurrent.ConcurrentHashMap;
+
+ private val lockMap = new ConcurrentHashMap[Object, Lock];
+ private val monitor = new Object {};
+
+ private class Lock {
+ var users = 0;
+ val impl = new ReentrantLock;
+ }
+
+ def acquire(key: Object) {
+ val lock = monitor.synchronized {
+ var lck = lockMap.get(key);
+ if (lck == null) {
+ lck = new Lock;
+ lockMap.put(key, lck);
+ }
+ lck.users += 1;
+ lck;
+ }
+ lock.impl.lock();
+ }
+
+ def isHeld(key: Object): Boolean = {
+ monitor.synchronized {
+ val lck = lockMap.get(key);
+ if (lck == null) {
+ false;
+ }
+ else {
+ lck.impl.isLocked;
+ }
+ }
+ }
+
+ def release(key: Object) {
+ val lock = monitor.synchronized {
+ var lck = lockMap.get(key);
+ lck.users -= 1;
+ if (lck.users == 0) {
+ lockMap.remove(key);
+ }
+ lck;
+ }
+ lock.impl.unlock();
+ }
+}
+
+object GlobalSynchronizer extends Synchronizer;
diff --git a/trunk/infrastructure/net.appjet.oui/util.scala b/trunk/infrastructure/net.appjet.oui/util.scala
new file mode 100644
index 0000000..ba8a736
--- /dev/null
+++ b/trunk/infrastructure/net.appjet.oui/util.scala
@@ -0,0 +1,153 @@
+/**
+ * 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 scala.collection.mutable.HashMap;
+
+import java.util.Enumeration;
+import java.util.zip.GZIPOutputStream;
+import java.io.ByteArrayOutputStream;
+
+object Util {
+ def noCacheHeaders =
+ Map("Expires" -> "Sat, 5 Feb 1983 07:07:07 GMT",
+ "Last-Modified" -> (new java.util.Date()).toGMTString(),
+ "Cache-Control" -> "no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0",
+ "Pragma" -> "no-cache");
+
+
+ class RichEnumeration[T](enumeration: Enumeration[T]) extends Iterator[T] {
+ def hasNext: Boolean = enumeration.hasMoreElements();
+ def next: T = enumeration.nextElement();
+ }
+ class RichIterator[T](iterator: java.util.Iterator[T]) extends Iterator[T] {
+ def hasNext: Boolean = iterator.hasNext();
+ def next: T = iterator.next();
+ }
+ implicit def enumerationToRichEnumeration[T](
+ enumeration: Enumeration[T]): RichEnumeration[T] = {
+ new RichEnumeration(enumeration)
+ }
+ implicit def iteratorToRichIterator[T](
+ iterator: java.util.Iterator[T]): RichIterator[T] = {
+ new RichIterator(iterator);
+ }
+
+ def enumerationToArray[T](e: Enumeration[T]): Array[T] =
+ enumerationToRichEnumeration(e).toList.toArray;
+
+ def stringToHTML(str: String): String = {
+ val result = new StringBuilder(str.length);
+ var lastCharBlank = false;
+ for(i <- 0 until str.length) {
+ val c = str.charAt(i);
+ if (c == ' ') {
+ // every second consecutive space becomes a &nbsp;
+ if (lastCharBlank) {
+ lastCharBlank = false;
+ result.append("&nbsp;");
+ }
+ else {
+ lastCharBlank = true;
+ result.append(' ');
+ }
+ } else {
+ lastCharBlank = false;
+ if (c == '&') result.append("&amp;");
+ else if (c == '<') result.append("&lt;");
+ else if (c == '>') result.append("&gt;");
+ else if (c == '\n') result.append("<br/>\n");
+ else if (c == '\t') {
+ for(j <- 1 to 7) {
+ result.append("&nbsp;");
+ }
+ result.append(' ');
+ }
+ else {
+ val code = c.toInt;
+ if (code < 127) {
+ result.append(c);
+ }
+ else {
+ // use character code
+ result.append("&#");
+ result.append(code);
+ result.append(';');
+ }
+ }
+ }
+ }
+ return result.toString;
+ }
+
+ def gzip(bytes: Array[Byte]): Array[Byte] = {
+ val baos = new ByteArrayOutputStream();
+ val gzos = new GZIPOutputStream(baos);
+ gzos.write(bytes, 0, bytes.length);
+ gzos.close();
+ baos.toByteArray();
+ }
+}
+
+object timekeeper {
+ var timestamp: Long = 0;
+ def time: Long = {
+ val t = System.currentTimeMillis();
+ synchronized {
+ if (t <= timestamp) {
+ timestamp += 1
+ } else {
+ timestamp = t
+ }
+ timestamp
+ }
+ }
+ def update(t: Long) = synchronized {
+ if (t > timestamp)
+ timestamp = t+1;
+ }
+}
+
+trait LoggingHandler extends org.mortbay.jetty.handler.AbstractHandler {
+ abstract override def handle(target: String, req: javax.servlet.http.HttpServletRequest, res: javax.servlet.http.HttpServletResponse, dispatch: Int) {
+ println("all ("+isStarted+") handling: "+(this match {
+ case hc: org.mortbay.jetty.handler.HandlerCollection => hc.getHandlers.mkString(", ");
+ case ahc: org.mortbay.jetty.handler.AbstractHandlerContainer => ahc.getChildHandlers.mkString(", ");
+ case x => "(unknown)";
+ }));
+ super.handle(target, req, res, dispatch);
+ }
+ override def doStart() {
+ println("all started.");
+ // Thread.dumpStack();
+ try {
+ super.doStart();
+ } catch {
+ case e: Exception => {
+ e.printStackTrace();
+ throw e;
+ }
+ } finally {
+ println("and: "+isStarted);
+ }
+ }
+ override def doStop() {
+ println("all stopped.");
+ // Thread.dumpStack();
+ super.doStop();
+ }
+}