Read all tutorials from this series.
Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.
Introduction
Typically modules are plain java classes. We could easily create an instance in BeanShell and then call instance.method(). While this is perfectly ok for the average programmer, some of our user might expect a more simple, functional interface. What module loading does is:
- create an instance of the module
- inject the instance into the interpreter
- create dynamic script code to wrap method calls
- execute the created code
method(param1, param2) { result = __MOD_module_instance.method(param1, param2); return result; }After loading this fragment we could access method() without the knowledge of modules, instances and object orientation at all.
Step 1: The code factory
To dynamically create code EASE needs an ICodeFactory implementation. To start you may look at existing implementations, basically we just need to build strings containing script code.
package org.eclipse.ease.lang.beanshell; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import org.eclipse.ease.Logger; import org.eclipse.ease.modules.AbstractCodeFactory; import org.eclipse.ease.modules.IEnvironment; import org.eclipse.ease.modules.IScriptFunctionModifier; import org.eclipse.ease.modules.ModuleHelper; public class BeanShellCodeFactory extends AbstractCodeFactory { public static final List<String> RESERVED_KEYWORDS = Arrays.asList("abstract", "continue", "for", "new", "switch", "assert", "default", "goto", "package", "synchronized", "boolean", "do", "if", "private", "this", "break", "double", "implements", "protected", "throw", "byte", "else", "import", "public", "throws", "case", "enum", "instanceof", "return", "transient", "catch", "extends", "int", "short", "try", "char", "final", "interface", "static", "void", "class", "finally", "long", "strictfp", "volatile", "const", "float", "native", "super", "while"); private static boolean isValidMethodName(final String methodName) { return BeanShellHelper.isSaveName(methodName) && !RESERVED_KEYWORDS.contains(methodName); } @Override public String getSaveVariableName(final String variableName) { return BeanShellHelper.getSaveName(variableName); } @Override public String createFunctionWrapper(final IEnvironment environment, final String moduleVariable, final Method method) { final StringBuilder scriptCode = new StringBuilder(); // parse parameters final List<Parameter> parameters = ModuleHelper.getParameters(method); // build parameter string final StringBuilder parameterList = new StringBuilder(); for (final Parameter parameter : parameters) parameterList.append(", ").append(parameter.getName()); if (parameterList.length() > 2) parameterList.delete(0, 2); final StringBuilder body = new StringBuilder(); // insert hooked pre execution code body.append(getPreExecutionCode(environment, method)); // insert deprecation warnings if (ModuleHelper.isDeprecated(method)) body.append("\tprintError('" + method.getName() + "() is deprecated. Consider updating your code.');\n"); // insert method call body.append("\t "); if (!method.getReturnType().equals(Void.TYPE)) body.append(IScriptFunctionModifier.RESULT_NAME).append(" = "); body.append(moduleVariable).append('.').append(method.getName()).append('('); body.append(parameterList); body.append(");\n"); // insert hooked post execution code body.append(getPostExecutionCode(environment, method)); // insert return statement if (!method.getReturnType().equals(Void.TYPE)) body.append("\treturn ").append(IScriptFunctionModifier.RESULT_NAME).append(";\n"); // build function declarations for (final String name : getMethodNames(method)) { if (!isValidMethodName(name)) { Logger.error(IPluginConstants.PLUGIN_ID, "The method name \"" + name + "\" from the module \"" + moduleVariable + "\" can not be wrapped because it's name is reserved"); } else if (!name.isEmpty()) { scriptCode.append(name).append("(").append(parameterList).append(") {\n"); scriptCode.append(body); scriptCode.append("}\n"); } } return scriptCode.toString(); } @Override public String classInstantiation(final Class<?> clazz, final String[] parameters) { final StringBuilder code = new StringBuilder(); code.append("new ").append(clazz.getName()).append("("); if (parameters != null) { for (final String parameter : parameters) code.append(parameter).append(", "); if (parameters.length > 0) code.delete(code.length() - 2, code.length()); } code.append(")"); return code.toString(); } @Override public String createFinalFieldWrapper(final IEnvironment environment, final String moduleVariable, final Field field) { return getSaveVariableName(field.getName()) + " = " + moduleVariable + "." + field.getName() + ";\n"; } @Override protected String getNullString() { return "null"; } }createFunctionWrapper() is called for each exposed method, createFinalFieldWrapper() is called for each exposed final field. The function wrapper injects preExecutionCode and postExecutionCode. This is interesting for modules implementing IScriptFunctionModifier. If such a module is loaded, it may influence code generation for all other modules. Use cases are creation of log files or unit testing.
Step 2: Register code factory
Now we need to register our code factory, so EASE is able to find it. Open the Extensions tab of org.eclipse.ease.lang.beanshell/plugin.xml. Navigate to the scriptType extension and add the code factory class to the BeanShell type.
Step 3: Bootstrapping
There exists a dedicated Environment module that provides basic commands like loadModule() to load further modules. Lets try to load it in a BeanShell:
run EASE and open a bean shell in the Script Shell View. We will create an instance of EnvironmentModule and advise it to load itself:
new org.eclipse.ease.modules.EnvironmentModule().loadModule("/System/Environment");If your code factory works as expected, commands like print() and loadModule() should be available.
To automate this task we will add a launch extension to our BeanShell language definition.
Switch to the plugin.xml and add a launchExtension to the org.eclipse.ease.language node.We bind it to the engineID of BeanShell and need to provide an implementation:
public class BootStrapper implements IScriptEngineLaunchExtension { @Override public void createEngine(final IScriptEngine engine) { ICodeFactory codeFactory = ScriptService.getCodeFactory(engine); if (codeFactory != null) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(codeFactory.classInstantiation(EnvironmentModule.class, new String[0])); stringBuilder.append(".loadModule(\""); stringBuilder.append(EnvironmentModule.MODULE_NAME); stringBuilder.append("\");\n"); engine.executeAsync(new Script("Bootloader", stringBuilder.toString())); } } }Of course we could directly use the string we tried manually before, but using the code factory is somewhat nicer. The implementation given above is already available from the BootStrapper class from EASE. If you do want to add more stuff to the bootloader you need to provide your own implementation.
With that in place you should be able to load all available modules into BeanShell and use its functions.
Optional: Loading external jars
We added support to register URLs to the classloader in our previous tutorial. Now as we have modules working we may try this out by calling
loadJar("http://central.maven.org/maven2/com/github/lalyos/jfiglet/0.0.7/jfiglet-0.0.7.jar") true com.github.lalyos.jfiglet.FigletFont.convertOneLine("I love scripting"); ___ _ _ _ _ |_ _| | | ___ __ __ ___ ___ ___ _ __ (_) _ __ | |_ (_) _ __ __ _ | | | | / _ \ \ \ / / / _ \ / __| / __|| '__|| || '_ \ | __|| || '_ \ / _` | | | | || (_) | \ V / | __/ \__ \| (__ | | | || |_) || |_ | || | | || (_| | |___| |_| \___/ \_/ \___| |___/ \___||_| |_|| .__/ \__||_||_| |_| \__, | |_| |___/