aboutsummaryrefslogtreecommitdiff
path: root/libraries
diff options
context:
space:
mode:
authorNick Kallen <nkallen@tallbro.local>2010-08-23 19:31:42 -0700
committerChristopher Vogt <oss.nsp@cvogt.org>2016-11-07 02:08:36 -0500
commitdd94d8fd7a20cfa9d39b91f158a7ce2f697ed0e1 (patch)
tree7d1c1dfa7563865e53754826f920927281ab6acb /libraries
downloadcbt-dd94d8fd7a20cfa9d39b91f158a7ce2f697ed0e1.tar.gz
cbt-dd94d8fd7a20cfa9d39b91f158a7ce2f697ed0e1.tar.bz2
cbt-dd94d8fd7a20cfa9d39b91f158a7ce2f697ed0e1.zip
introducing the evaluator
Diffstat (limited to 'libraries')
-rw-r--r--libraries/eval/Eval.scala123
-rw-r--r--libraries/eval/test/EvalTest.scala16
-rw-r--r--libraries/eval/test/resources/OnePlusOne.scala1
3 files changed, 140 insertions, 0 deletions
diff --git a/libraries/eval/Eval.scala b/libraries/eval/Eval.scala
new file mode 100644
index 0000000..9de9bd4
--- /dev/null
+++ b/libraries/eval/Eval.scala
@@ -0,0 +1,123 @@
+package com.twitter.util
+
+import scala.tools.nsc.{Global, Settings}
+import scala.tools.nsc.reporters.ConsoleReporter
+import scala.runtime._
+import java.io.{File, FileWriter}
+import java.net.{URL, URLClassLoader}
+import scala.io.Source
+
+/**
+ * Eval is a utility function to evaluate a file and return its results.
+ * It is intended to be used for application configuration (rather than Configgy, XML, YAML files, etc.)
+ * and anything else.
+ *
+ * Eval takes a file or string and generates a new scala class that has an apply method that
+ * evaluates that string. The newly generated file is then compiled. All generated .scala and .class
+ * files are stored, by default, in System.getProperty("java.io.tmpdir").
+ *
+ * After compilation, the a new class loader is created with the temporary dir as the classPath.
+ * The generated class is loaded and then apply() is called.
+ *
+ * This implementation is inspired by
+ * http://scala-programming-language.1934581.n4.nabble.com/Compiler-API-td1992165.html
+ */
+object Eval {
+ private val compilerPath = jarPathOfClass("scala.tools.nsc.Interpreter")
+ private val libPath = jarPathOfClass("scala.ScalaObject")
+
+ /**
+ * Eval[Int]("1 + 1") // => 2
+ */
+ def apply[T](stringToEval: String): T = {
+ val className = "Evaluator"
+ val targetDir = new File(System.getProperty("java.io.tmpdir"))
+ val wrappedFile = wrapInClass(stringToEval, className, targetDir)
+ compile(wrappedFile, targetDir)
+ val clazz = loadClass(targetDir, className)
+ val constructor = clazz.getConstructor()
+ val evaluator = constructor.newInstance().asInstanceOf[() => Any]
+ evaluator().asInstanceOf[T]
+ }
+
+ /**
+ * Eval[Int](new File("..."))
+ */
+ def apply[T](fileToEval: File): T = {
+ val stringToEval = scala.io.Source.fromFile(fileToEval).mkString
+ apply(stringToEval)
+ }
+
+ /**
+ * Wrap sourceCode in a new class that has an apply method that evaluates that sourceCode.
+ * Write generated (temporary) classes to targetDir
+ */
+ private def wrapInClass(sourceCode: String, className: String, targetDir: File) = {
+ val targetFile = File.createTempFile(className, ".scala", targetDir)
+ targetFile.deleteOnExit()
+ val writer = new FileWriter(targetFile)
+ writer.write("class " + className + " extends (() => Any) {\n")
+ writer.write(" def apply() = {\n")
+ writer.write(sourceCode)
+ writer.write("\n }\n")
+ writer.write("}\n")
+ writer.close()
+ targetFile
+ }
+
+ /**
+ * Compile a given file into the targetDir
+ */
+ private def compile(file: File, targetDir: File) {
+ val settings = new Settings
+ val origBootclasspath = settings.bootclasspath.value
+
+ // Figure out our app classpath.
+ // TODO: there are likely a ton of corner cases waiting here
+ val configulousClassLoader = this.getClass.getClassLoader.asInstanceOf[URLClassLoader]
+ val configulousClasspath = configulousClassLoader.getURLs.map { url =>
+ val urlStr = url.toString
+ urlStr.substring(5, urlStr.length)
+ }.toList
+ val bootClassPath = origBootclasspath.split(java.io.File.pathSeparator).toList
+
+ // the classpath for compile is our app path + boot path + make sure we have compiler/lib there
+ val pathList = bootClassPath ::: (configulousClasspath ::: List(compilerPath, libPath))
+ val pathString = pathList.mkString(java.io.File.pathSeparator)
+ settings.bootclasspath.value = pathString
+ settings.classpath.value = pathString
+ settings.deprecation.value = true // enable detailed deprecation warnings
+ settings.unchecked.value = true // enable detailed unchecked warnings
+ settings.outdir.value = targetDir.toString
+
+ val reporter = new ConsoleReporter(settings)
+ val compiler = new Global(settings, reporter)
+ (new compiler.Run).compile(List(file.toString))
+
+ if (reporter.hasErrors || reporter.WARNING.count > 0) {
+ // TODO: throw ...
+ }
+ }
+
+ /**
+ * Create a new classLoader with the targetDir as the classPath.
+ * Load the class with className
+ */
+ private def loadClass(targetDir: File, className: String) = {
+ // set up the new classloader in targetDir
+ val scalaClassLoader = this.getClass.getClassLoader
+ val targetDirURL = targetDir.toURL
+ val newClassLoader = URLClassLoader.newInstance(Array(targetDir.toURL), scalaClassLoader)
+ newClassLoader.loadClass(className)
+ }
+
+ private def jarPathOfClass(className: String) = {
+ val resource = className.split('.').mkString("/", "/", ".class")
+ //println("resource for %s is %s".format(className, resource))
+ val path = getClass.getResource(resource).getPath
+ val indexOfFile = path.indexOf("file:")
+ val indexOfSeparator = path.lastIndexOf('!')
+ path.substring(indexOfFile, indexOfSeparator)
+ }
+
+} \ No newline at end of file
diff --git a/libraries/eval/test/EvalTest.scala b/libraries/eval/test/EvalTest.scala
new file mode 100644
index 0000000..eaf42a7
--- /dev/null
+++ b/libraries/eval/test/EvalTest.scala
@@ -0,0 +1,16 @@
+package com.twitter.util
+
+import org.specs.Specification
+import java.io.File
+
+object EvaluatorSpec extends Specification {
+ "Evaluator" should {
+ "apply('expression')" in {
+ Eval[Int]("1 + 1") mustEqual 2
+ }
+
+ "apply(new File(...))" in {
+ Eval[Int](new File("src/test/resources/OnePlusOne.scala")) mustEqual 2
+ }
+ }
+}
diff --git a/libraries/eval/test/resources/OnePlusOne.scala b/libraries/eval/test/resources/OnePlusOne.scala
new file mode 100644
index 0000000..1a5a117
--- /dev/null
+++ b/libraries/eval/test/resources/OnePlusOne.scala
@@ -0,0 +1 @@
+1 + 1 \ No newline at end of file