/** * 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.bodylock; import net.appjet.common.rhino.rhinospect; import scala.collection.mutable.{SynchronizedMap, ArrayBuffer, HashMap}; import org.mozilla.javascript.{Context, Scriptable, ScriptableObject, Script, JavaScriptException, NativeJavaObject, WrappedException, IdScriptableObject}; trait Executable { def execute(scope: Scriptable): Object; } trait JSStackFrame { def errorLine: Int; // 1-indexed. def errorContext(rad: Int): (Int, Int, Seq[String]); // 1-indexed def name: String; } class ExecutionException(message: String, cause: Throwable) extends RuntimeException(message, cause) { def this(message: String) = this(message, null); } class JSRuntimeException(val message: String, val cause: Throwable) extends ExecutionException(message, cause) { private val i_frames: Seq[JSStackFrame] = if (cause == null) List() else { val ab = new ArrayBuffer[JSStackFrame]; for (elt <- cause.getStackTrace() if (elt.getFileName != null && BodyLock.map.filter(_.contains(elt.getFileName)).isDefined && elt.getLineNumber >= 0)) { ab += new JSStackFrame { val errorLine = elt.getLineNumber; val name = elt.getFileName; val code = BodyLock.map.getOrElse(Map[String, String]()).getOrElse(elt.getFileName, "").split("\n"); // 0-indexed. def errorContext(rad: Int) = { val start_i = Math.max(errorLine-rad, 1)-1; val end_i = Math.min(errorLine+rad, code.length)-1; (start_i+1, end_i+1, code.slice(start_i, end_i+1)); } } } ab; } def frames = i_frames; } class JSCompileException(message: String, cause: org.mozilla.javascript.EvaluatorException) extends JSRuntimeException(message, cause) { override val frames = List(new JSStackFrame { val errorLine = cause.lineNumber(); val name = cause.sourceName(); val code = BodyLock.map.getOrElse(Map[String, String]()).getOrElse(cause.sourceName(), "").split("\n"); // 0-indexed. def errorContext(rad: Int) = { val start_i = Math.max(errorLine-rad, 1)-1; val end_i = Math.min(errorLine+rad, code.length)-1; (start_i+1, end_i+1, code.slice(start_i, end_i+1)); } }).concat(List(super.frames: _*)); } private[bodylock] class InnerExecutable(val code: String, val script: Script) extends Executable { def execute(scope: Scriptable) = try { BodyLock.runInContext { cx => script.exec(cx, scope); } } catch { case e: Throwable => { val orig = BodyLock.unwrapExceptionIfNecessary(e); orig match { case e: JSRuntimeException => throw e; case e: org.mortbay.jetty.RetryRequest => throw e; case _ => throw new JSRuntimeException("Error while executing: "+orig.getMessage, orig); } } } override def toString() = rhinospect.dumpFields(script, 1, ""); } object CustomContextFactory extends org.mozilla.javascript.ContextFactory { val wrapFactory = new org.mozilla.javascript.WrapFactory { setJavaPrimitiveWrap(false); // don't wrap strings, numbers, booleans } class CustomContext() extends Context() { setWrapFactory(wrapFactory); } override def makeContext(): Context = new CustomContext(); } object BodyLock { var map: Option[SynchronizedMap[String, String]] = None; def runInContext[E](expr: Context => E): E = { val cx = CustomContextFactory.enterContext(); try { expr(cx); } finally { Context.exit(); } } def newScope = runInContext { cx => cx.initStandardObjects(null, true); } def subScope(scope: Scriptable) = runInContext { cx => val newObj = cx.newObject(scope).asInstanceOf[ScriptableObject]; newObj.setPrototype(scope); newObj.setParentScope(null); newObj; } def evaluateString(scope: Scriptable, source: String, sourceName: String, lineno: Int /*, securityDomain: AnyRef = null */) = runInContext { cx => cx.evaluateString(scope, source, sourceName, lineno, null); } def compileString(source: String, sourceName: String, lineno: Int /*, securityDomain: AnyRef = null */) = runInContext { cx => map.foreach(_(sourceName) = source); try { new InnerExecutable(source, compileToScript(source, sourceName, lineno)); } catch { case e: org.mozilla.javascript.EvaluatorException => { throw new JSCompileException(e.getMessage(), e); } } } private val classId = new java.util.concurrent.atomic.AtomicInteger(0); private def compileToScript(source: String, sourceName: String, lineNumber: Int): Script = { val className = "JS$"+sourceName.replaceAll("[^a-zA-Z0-9]", "\\$")+"$"+classId.incrementAndGet(); compilationutils.compileToScript(source, sourceName, lineNumber, className); } def executableFromBytes(bytes: Array[byte], className: String) = new InnerExecutable("(source not available)", compilationutils.bytesToScript(bytes, className)); def unwrapExceptionIfNecessary(e: Throwable): Throwable = { e match { case e: JavaScriptException => e.getValue() match { case njo: NativeJavaObject => Context.jsToJava(njo, classOf[Object]) match { case e: Throwable => e; case _ => e; } case ne: IdScriptableObject => new JSRuntimeException("Error: "+ne.get("message", ne), e); case t: Throwable => t; case _ => e; } case e: WrappedException => unwrapExceptionIfNecessary(e.getWrappedException()); case _ => e; } } } private[bodylock] object compilationutils { class Loader(parent: ClassLoader) extends ClassLoader(parent) { def this() = this(getClass.getClassLoader); def defineClass(className: String, bytes: Array[Byte]): Class[_] = { // call protected method defineClass(className, bytes, 0, bytes.length); } } def compileToBytes(source: String, sourceName: String, lineNumber: Int, className: String): Array[Byte] = { val environs = new org.mozilla.javascript.CompilerEnvirons; BodyLock.runInContext(environs.initFromContext(_)); environs.setGeneratingSource(false); val compiler = new org.mozilla.javascript.optimizer.ClassCompiler(environs); // throws EvaluatorException val result:Array[Object] = compiler.compileToClassFiles(source, sourceName, lineNumber, className); // result[0] is class name, result[1] is class bytes result(1).asInstanceOf[Array[Byte]]; } def compileToScript(source: String, sourceName: String, lineNumber: Int, className: String): Script = { bytesToScript(compileToBytes(source, sourceName, lineNumber, className), className); } def bytesToScript(bytes: Array[Byte], className: String): Script = { (new Loader()).defineClass(className, bytes).newInstance.asInstanceOf[Script]; } } import java.io.File; import scala.collection.mutable.HashMap; import net.appjet.common.util.BetterFile; import net.appjet.common.cli._; object Compiler { val optionsList = Array( ("destination", true, "Destination for class files", "path"), ("cutPrefix", true, "Drop this prefix from files", "path"), ("verbose", false, "Print debug information", "") ); val chosenOptions = new HashMap[String, String]; val options = for (opt <- optionsList) yield new CliOption(opt._1, opt._3, if (opt._2) Some(opt._4) else None) // var o = new Options; // for (m <- optionsList) { // o.addOption({ // if (m._2) { // withArgName(m._4); // hasArg(); // } // withDescription(m._3); // // withLongOpt(m.getName()); // create(m._1); // }); // } // o; // } var verbose = true; def vprintln(s: String) { if (verbose) println(s); } def printUsage() { println((new CliParser(options)).usage); } def extractOptions(args0: Array[String]) = { val parser = new CliParser(options); val (opts, args) = try { parser.parseOptions(args0); } catch { case e: ParseException => { println("error: "+e.getMessage()); printUsage(); System.exit(1); null; } } for ((k, v) <- opts) { chosenOptions(k) = v; } args } def compileSingleFile(src: File, dst: File) { val source = BetterFile.getFileContents(src); vprintln("to: "+dst.getPath()); val classBytes = compilationutils.compileToBytes(source, src.getName(), 1, dst.getName().split("\\.")(0)); val fos = new java.io.FileOutputStream(dst); fos.write(classBytes); } def main(args0: Array[String]) { // should contain paths, relative to PWD, of javascript files to compile. val args = extractOptions(args0); val dst = chosenOptions("destination"); val pre = chosenOptions.getOrElse("cutPrefix", ""); verbose = chosenOptions.getOrElse("verbose", "false") == "true"; for (p <- args) { val srcFile = new File(p); if (srcFile.getParent() != null && ! srcFile.getParent().startsWith(pre)) throw new RuntimeException("srcFile "+srcFile.getPath()+" doesn't start with "+pre); val parentDir = if (srcFile.getParent() != null) { new File(dst+"/"+srcFile.getParent().substring(pre.length)); } else { new File(dst); } parentDir.mkdirs(); compileSingleFile(srcFile, new File(parentDir.getPath()+"/JS$"+srcFile.getName().split("\\.").reverse.drop(1).reverse.mkString(".").replaceAll("[^a-zA-Z0-9]", "\\$")+".class")); } } }