Skip to content

Scala 2.12.4 upgrade + improve input type resolution #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
<license.copyrightOwners>Board of Regents of the University of
Wisconsin-Madison.</license.copyrightOwners>

<scala.version>2.12.1</scala.version>
<scala.version>2.12.4</scala.version>

<!-- NB: Deploy releases to the ImageJ Maven repository. -->
<releaseProfiles>deploy-to-imagej</releaseProfiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@

package org.scijava.plugins.scripting.scala;

import java.util.Optional;
import java.util.Spliterator;
import java.util.stream.StreamSupport;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.scijava.log.LogService;
import org.scijava.module.ModuleItem;
import org.scijava.plugin.Parameter;
import org.scijava.script.AdaptedScriptEngine;
import org.scijava.script.ScriptModule;
import org.scijava.script.process.ScriptCallback;

/**
* Scala interpreter
Expand All @@ -43,29 +51,61 @@
*/
public class ScalaScriptEngine extends AdaptedScriptEngine {

@Parameter
private LogService log;

public ScalaScriptEngine(ScriptEngine engine) {
super(engine);
}

@Override
public Object get(String key) {
// First try to get value from bindings
Object value = super.get(key);
public void put(final String key, final Object value) {
// Need to get the ScriptModule instance in order to get the inputs
// discovered at runtime. ScriptModule adds a reference to itself to
// the Bindings at the start of its run method.
Optional<String> outKey =
Optional.ofNullable((ScriptModule) super.get(ScriptModule.class.getName()))
.flatMap(sm -> {
Spliterator<ModuleItem<?>> inputs = sm.getInfo().inputs().spliterator();
Optional<String> output = StreamSupport.stream(inputs, false)
.filter(i -> i.getName() == key)
.findFirst()
.map((ModuleItem<?> i) -> { // Foreach input param, cast to the known type
String name = i.getName();
String type = i.getType().getName();
String script = String.format("val %s: %s = _%s.asInstanceOf[%s]",
name, type, name, type);
ScriptCallback e = new ScriptCallback() {
@Override
public void invoke(ScriptModule module) throws ScriptException {
module.getEngine().eval(script);
}
};
sm.getInfo().callbacks().add(e);
return ("_" + key);
});
return (output);
});

super.put(outKey.orElse(key), value);
}

// NB: Extracting values from Scala Script Engine are a little tricky.
// Values (variables) initialised or computed in the script are
// not added to the bindings of the CompiledScript AFAICT. Therefore
// the only way to extract them is to evaluate the variable and
// capture the return. If it evaluates to null or throws a
// a ScriptException, we simply return null.
if (value == null) try {
value = super.eval(key);
} catch (ScriptException ignored) {
// HACK: Explicitly ignore ScriptException, which arises if
// key is not found. This feels bad because it fails silently
// for the user, but it mimics behaviour in other script langs.
}
@Override
public Object get(String key) {
// Values (variables) initialised or computed in the script are not added to
// the bindings of the ScriptContext. One way to extract them is to evaluate
// the variable and capture the return. So, first try to get value from bindings.
// If that returns null, then evaluate the variable in the ScriptEngine. If this
// returns null or throws a ScriptException, we simply return null.
Optional<Object> result = Optional.ofNullable(super.get(key));

return value;
return result.orElseGet(() -> {
try {
return super.eval(key);
} catch (ScriptException se) {
log.error(se.getMessage());
return null;
}
});
}
}
73 changes: 67 additions & 6 deletions src/test/java/org/scijava/plugins/scripting/scala/ScalaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
Expand All @@ -33,13 +33,17 @@
import static org.junit.Assert.assertEquals;

import java.io.StringWriter;
import java.util.concurrent.ExecutionException;

import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import org.junit.Before;
import org.junit.Test;
import org.scijava.Context;
import org.scijava.script.ScriptLanguage;
import org.scijava.script.ScriptModule;
import org.scijava.script.ScriptService;

/**
Expand All @@ -50,14 +54,24 @@
*/
public class ScalaTest {

@Test
public void testBasic() throws Exception {
private ScriptService scriptService;
private ScriptEngine engine;

@Before
public void setUp() {
final Context context = new Context(ScriptService.class);
final ScriptService scriptService = context.getService(ScriptService.class);
scriptService = context.getService(ScriptService.class);

final ScriptLanguage language =
scriptService.getLanguageByExtension("scala");
final ScriptEngine engine = language.getScriptEngine();
engine = language.getScriptEngine();
}

/**
* Test whether we can evaluate something in the ScalaScriptEngine.
*/
@Test
public void testBasic() throws Exception {

final SimpleScriptContext ssc = new SimpleScriptContext();
final StringWriter writer = new StringWriter();
Expand All @@ -67,4 +81,51 @@ public void testBasic() throws Exception {
engine.eval(script, ssc);
assertEquals("3", writer.toString());
}

/**
* Test whether input parameters resolve to the correct type.
*/
@Test
public void testParameterType() throws ScriptException, ExecutionException, InterruptedException {

String ls = System.getProperty("line.separator");
// We create a script that requests injection of a @ScriptService
// via a Script parameter. Then use Scala reflection tools to check
// the runtime TypeTag of the ScriptService. We expect this to return
// org.scijava.script.ScriptService rather than Object.
final String script = String.join(
ls,
"// @ScriptService ss",
"// @OUTPUT String ssType",
"import scala.reflect.runtime.{universe => ru}",
"def getTypeTag[T: ru.TypeTag](v: T) = ru.typeTag[T]",
"val ssType: String = getTypeTag(ss).toString"
);

final ScriptModule sm = scriptService.run("test.scala", script, true).get();

final Object actual = sm.getOutput("ssType");
final String expected = "TypeTag[org.scijava.script.ScriptService]";

assertEquals(expected, actual);
}

@Test
public void testParameters() throws ScriptException, ExecutionException, InterruptedException {

String ls = System.getProperty("line.separator");
final String script = String.join(
ls,
"// @ScriptService ss",
"// @OUTPUT String lang",
"val lang = ss.getLanguageByName(\"scala\").getLanguageName()"
);

final ScriptModule sm = scriptService.run("test.scala", script, true).get();

final Object actual = sm.getOutput("lang");
final String expected = scriptService.getLanguageByName("scala").getLanguageName();

assertEquals(expected, actual);
}
}