|
| 1 | +package org.scijava.plugins.scripting.scala |
| 2 | + |
| 3 | +import java.io.{OutputStream, Reader, StringWriter, Writer} |
| 4 | +import javax.script.* |
| 5 | +import scala.collection.mutable |
| 6 | +import scala.jdk.CollectionConverters.* |
| 7 | +import scala.util.Try |
| 8 | + |
| 9 | +/** |
| 10 | + * Adapted Scala ScriptEngine |
| 11 | + * |
| 12 | + * @author Jarek Sacha |
| 13 | + * @author Keith Schulze |
| 14 | + * @see ScriptEngine |
| 15 | + */ |
| 16 | +class ScalaAdaptedScriptEngine(engine: ScriptEngine) extends AbstractScriptEngine: |
| 17 | + |
| 18 | + import ScalaAdaptedScriptEngine.* |
| 19 | + |
| 20 | + private val buffer = new Array[Char](8192) |
| 21 | + |
| 22 | + @throws[ScriptException] |
| 23 | + override def eval(reader: Reader, context: ScriptContext): AnyRef = eval(stringFromReader(reader), context) |
| 24 | + |
| 25 | + @throws[ScriptException] |
| 26 | + override def eval(script: String, context: ScriptContext): AnyRef = |
| 27 | + emulateBinding(context) |
| 28 | + evalInner(script, context) |
| 29 | + |
| 30 | + private def emulateBinding(context: ScriptContext): Unit = |
| 31 | + |
| 32 | + // Scala 3.2.2 ignores bindings, emulate binding using setup script |
| 33 | + // Create a line with variable declaration for each binding item |
| 34 | + val lines = |
| 35 | + for |
| 36 | + scope <- context.getScopes.asScala |
| 37 | + bindings <- Option(context.getBindings(scope)).map(_.asScala) // bindings in context can be null |
| 38 | + yield { |
| 39 | + for (name, value) <- bindings yield { |
| 40 | + value match |
| 41 | + case v: Double => s"val $name : Double = ${v}d" |
| 42 | + case v: Float => s"val $name : Float = ${v}f" |
| 43 | + case v: Long => s"val $name : Long = ${v}L" |
| 44 | + case v: Int => s"val $name : Int = $v" |
| 45 | + case v: Char => s"val $name : Char = '$v'" |
| 46 | + case v: Short => s"val $name : Short = $v" |
| 47 | + case v: Byte => s"val $name : Byte = $v" |
| 48 | + case v: Boolean => s"val $name : Int = $v" |
| 49 | + case o: AnyRef if isValidVariableName(name) => |
| 50 | + _transfer = o |
| 51 | + val typeName = Option(o).map(_.getClass.getCanonicalName).getOrElse("AnyRef") |
| 52 | + s""" |
| 53 | + |val $name : $typeName = { |
| 54 | + | val t = org.scijava.plugins.scripting.scala.ScalaAdaptedScriptEngine._transfer |
| 55 | + | t.asInstanceOf[$typeName] |
| 56 | + |}""".stripMargin |
| 57 | + case _: AnyRef => "" // ignore if name is not a variable |
| 58 | + case v: Unit => |
| 59 | + throw ScriptException(s"Unsupported type for bind variable $name: ${v.getClass}") |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + val script = lines |
| 64 | + .flatten |
| 65 | + .filter(_.nonEmpty) |
| 66 | + .mkString("\n") |
| 67 | + |
| 68 | + if script.nonEmpty then |
| 69 | + evalInner(script, context) |
| 70 | + |
| 71 | + end emulateBinding |
| 72 | + |
| 73 | + private def evalInner(script: String, context: ScriptContext) = |
| 74 | + class WriterOutputStream(w: Writer) extends OutputStream: |
| 75 | + override def write(b: Int): Unit = w.write(b) |
| 76 | + |
| 77 | + // Redirect output to writes provided by context |
| 78 | + Console.withOut(WriterOutputStream(context.getWriter)) { |
| 79 | + Console.withErr(WriterOutputStream(context.getErrorWriter)) { |
| 80 | + engine.eval(script, context) |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + private def stringFromReader(in: Reader) = |
| 85 | + val out = new StringWriter() |
| 86 | + var n = in.read(buffer) |
| 87 | + while n > -1 do |
| 88 | + out.write(buffer, 0, n) |
| 89 | + n = in.read(buffer) |
| 90 | + in.close() |
| 91 | + out.toString |
| 92 | + |
| 93 | + override def createBindings(): Bindings = engine.createBindings |
| 94 | + |
| 95 | + override def getFactory: ScriptEngineFactory = engine.getFactory |
| 96 | + |
| 97 | + override def get(key: String): AnyRef = |
| 98 | + // First try to get value from bindings |
| 99 | + var value = super.get(key) |
| 100 | + |
| 101 | + // NB: Extracting values from Scala Script Engine are a little tricky.// NB: Extracting values from Scala Script Engine are a little tricky. |
| 102 | + // Values (variables) initialised or computed in the script are// Values (variables) initialised or computed in the script are |
| 103 | + // not added to the bindings of the CompiledScript AFAICT. Therefore// not added to the bindings of the CompiledScript AFAICT. Therefore |
| 104 | + // the only way to extract them is to evaluate the variable and// the only way to extract them is to evaluate the variable and |
| 105 | + // capture the return. If it evaluates to null or throws a// capture the return. If it evaluates to null or throws a |
| 106 | + // a ScriptException, we simply return null.// a ScriptException, we simply return null. |
| 107 | + if value == null then |
| 108 | + try |
| 109 | + value = evalInner(key, getContext) |
| 110 | + catch |
| 111 | + case _: ScriptException => |
| 112 | + // HACK: Explicitly ignore ScriptException, which arises if |
| 113 | + // key is not found. This feels bad because it fails silently |
| 114 | + // for the user, but it mimics behaviour in other script langs. |
| 115 | + |
| 116 | + value |
| 117 | + end get |
| 118 | + |
| 119 | +end ScalaAdaptedScriptEngine |
| 120 | + |
| 121 | +object ScalaAdaptedScriptEngine: |
| 122 | + private lazy val variableNamePattern = """^[a-zA-Z_$][a-zA-Z_$0-9]*$""".r |
| 123 | + |
| 124 | + /** Do not use externally despite it is declared public. IT is public so it is accessible from scripts */ |
| 125 | + // noinspection ScalaWeakerAccess |
| 126 | + var _transfer: Object = _ |
| 127 | + |
| 128 | + private def isValidVariableName(name: String): Boolean = variableNamePattern.matches(name) |
| 129 | +end ScalaAdaptedScriptEngine |
0 commit comments