blob: d0013d8573cc5eb9b2b44d451ff5d35099c9fc23 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2013 Christian Pontesegger and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License_Identifier: EPL-2.0
*
* Contributors:
* Christian Pontesegger - initial API and implementation
*******************************************************************************/
package org.eclipse.ease.modules;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.ease.AbstractScriptEngine;
import org.eclipse.ease.Activator;
import org.eclipse.ease.ExitException;
import org.eclipse.ease.ICodeFactory;
import org.eclipse.ease.IScriptEngine;
import org.eclipse.ease.Logger;
import org.eclipse.ease.Script;
import org.eclipse.ease.modules.ModuleDefinition.ModuleDependency;
import org.eclipse.ease.modules.ModuleTracker.ModuleState;
import org.eclipse.ease.service.ScriptService;
import org.eclipse.ease.tools.ListenerList;
import org.eclipse.ease.tools.ResourceTools;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
/**
* The Environment provides base functions for all script interpreters. It is automatically loaded by any interpreter upon startup.
*/
public class EnvironmentModule extends AbstractScriptModule implements IEnvironment {
public static final String MODULE_NAME = "/System/Environment";
/** Used by code completion. Keep in sync with method name in this class. */
public static final String INCLUDE_METHOD = "include";
/** Used by code completion. Keep in sync with method name in this class. */
public static final String LOAD_JAR_METHOD = "loadJar";
/** Used by code completion. Keep in sync with method name in this class. */
public static final String LOAD_MODULE_METHOD = "loadModule";
private static final Pattern VALID_TOPICS_PATTERN = Pattern.compile("[\\w ]+(?:\\(\\))?");
public static void bootstrap() throws ExecutionException {
final ModuleDefinition definition = ModuleHelper.resolveModuleName(MODULE_NAME);
final IEnvironment instance = (IEnvironment) definition.createModuleInstance();
instance.initialize(AbstractScriptEngine.getCurrentScriptEngine(), instance);
instance.wrap(instance, false);
}
private final ModuleTracker fModuleTracker = new ModuleTracker();
private final ListenerList<IModuleListener> fModuleListeners = new ListenerList<>();
/** Stores short method IDs to method relations used for script callbacks. */
private final Map<String, Method> fRegisteredMethods = new HashMap<>();
/** Callbacks for wrapped method invocations. */
private final ListenerList<IModuleCallbackProvider> fModuleCallbacks = new ListenerList<>();
private final Collection<String> fPrintedErrors = new HashSet<>();
public EnvironmentModule() {
final ModuleDefinition moduleDefinition = ModuleHelper.resolveModuleName(MODULE_NAME);
final ModuleState state = fModuleTracker.addModule(moduleDefinition);
state.setInstance(this);
}
public Object getModuleInstance(ModuleDefinition definition) {
final ModuleState moduleState = fModuleTracker.getOrCreateModuleState(definition);
if (!moduleState.isLoaded()) {
if (definition.isDeprecated())
printError("Module \"" + definition.getName() + "\" is deprecated. Consider updating your code.", false);
createModuleDependencies(definition);
final Object instance = definition.createModuleInstance();
moduleState.setInstance(instance);
// check that module class got initialized correctly
if (instance == null)
throw new RuntimeException("Could not create module instance, see workspace log for more details");
if (instance instanceof IScriptModule)
((IScriptModule) instance).initialize(getScriptEngine(), this);
fireModuleEvent(instance, IModuleListener.LOADED);
}
return moduleState.getInstance();
}
private void createModuleDependencies(ModuleDefinition parentModuleDefinition) {
for (final ModuleDependency dependency : parentModuleDefinition.getDependencies()) {
final ModuleDefinition dependencyDefinition = dependency.getDefinition();
if (dependencyDefinition == null)
throw new RuntimeException("Could not resolve module dependency \"" + dependency.getId() + "\"");
createModuleDependencies(dependencyDefinition);
getModuleInstance(dependencyDefinition);
}
}
@Override
@WrapToScript
public final Object loadModule(final String moduleIdentifier, @ScriptParameter(defaultValue = "false") boolean useCustomNamespace)
throws ExecutionException {
// resolve identifier
final ModuleDefinition definition = ModuleHelper.resolveModuleName(moduleIdentifier);
if (definition == null)
throw new RuntimeException("Could not find module \"" + moduleIdentifier + "\"");
final ModuleState moduleState = fModuleTracker.getOrCreateModuleState(definition);
// make sure module instance gets loaded
getModuleInstance(definition);
if (!useCustomNamespace)
wrapModuleDependencies(definition);
// create function wrappers
return wrap(moduleState.getInstance(), useCustomNamespace);
}
/**
* Find and wrap instances of module dependencies. All found dependencies (and dependencies of dependencies) are wrapped into the root scope if they were
* not wrapped before.
*
* @param parentModuleDefinition
* parent definition to find dependencies for
* @throws ExecutionException
*/
private void wrapModuleDependencies(ModuleDefinition parentModuleDefinition) throws ExecutionException {
for (final ModuleDependency dependency : parentModuleDefinition.getDependencies()) {
final ModuleDefinition dependencyDefinition = dependency.getDefinition();
wrapModuleDependencies(dependencyDefinition);
final ModuleState dependencyState = fModuleTracker.getOrCreateModuleState(dependencyDefinition);
if (!dependencyState.isWrapped())
wrap(dependencyState.getInstance(), false);
}
}
/**
* Resolves a loaded module and returns the Java instance. Will only query previously loaded modules.
*
* @param name
* name of the module to resolve
* @return resolved module instance or <code>null</code>
*/
@Override
@WrapToScript
public final Object getModule(final String name) {
final ModuleDefinition definition = ModuleHelper.resolveModuleName(name);
if (definition == null)
return null;
final ModuleState moduleState = fModuleTracker.getModuleState(definition);
return (moduleState != null) ? moduleState.getInstance() : null;
}
/**
* Resolves a loaded module by its class.
*
* @param clazz
* module class to look resolve
* @return resolved module instance or <code>null</code>
*/
@SuppressWarnings("unchecked")
@Override
public <T extends Object, U extends Class<T>> T getModule(final U clazz) {
for (final ModuleState state : fModuleTracker.getAvailableModules()) {
if (state.getInstance() != null) {
if (clazz.isAssignableFrom(state.getInstance().getClass()))
return (T) state.getInstance();
}
}
return null;
}
@Override
public List<Object> getModules() {
return fModuleTracker.getAvailableModules().stream().filter(state -> state.isLoaded()).map(state -> state.getInstance()).collect(Collectors.toList());
}
@Override
public ModuleDefinition getModuleDefinition(Object moduleInstance) {
final Optional<ModuleDefinition> definition = fModuleTracker.getAvailableModules().stream()
.filter(state -> Objects.equals(moduleInstance, state.getInstance())).map(s -> s.getModuleDefinition()).findFirst();
return definition.orElseThrow(() -> new IllegalArgumentException("No module loaded for provided instance"));
}
/**
* Write a message to the output stream of the script engine.
*
* @param text
* message to write
* @param lineFeed
* <code>true</code> to add a line feed after the text
*/
@Override
@WrapToScript(supportedLanguages = "!Python")
public final void print(final @ScriptParameter(defaultValue = "") Object text, final @ScriptParameter(defaultValue = "true") boolean lineFeed) {
if (lineFeed)
getScriptEngine().getOutputStream().println(text);
else
getScriptEngine().getOutputStream().print(text);
}
/**
* Write a message to the error stream of the script engine.
*
* @param text
* message to write
* @param printOnce
* If <code>true</code>, the text in parameter will be printed only once and ignored in other calls of this method. If <code>false</code>, it
* will be printed in all cases.
*/
@WrapToScript
public final void printError(final @ScriptParameter(defaultValue = "") Object text, final @ScriptParameter(defaultValue = "false") boolean printOnce) {
final String printedText = String.valueOf(text);
if (!(printOnce && fPrintedErrors.contains(printedText))) {
getScriptEngine().getErrorStream().println(printedText);
fPrintedErrors.add(printedText);
}
}
@Override
public void addModuleListener(final IModuleListener listener) {
fModuleListeners.add(listener);
}
@Override
public void removeModuleListener(final IModuleListener listener) {
fModuleListeners.remove(listener);
}
/**
* Read a single line of data from the default input stream of the script engine. Depending on the <i>blocking</i> parameter this method will wait for user
* input or return immediately with available data.
*
* @param blocking
* <code>true</code> results in a blocking call until data is available, <code>false</code> returns in any case
* @return string data from input stream or <code>null</code>
* @throws IOException
* when reading on the input stream fails
*/
@WrapToScript
public String readInput(@ScriptParameter(defaultValue = "true") final boolean blocking) throws IOException {
final InputStream inputStream = getScriptEngine().getInputStream();
boolean doRead = blocking;
if (!doRead) {
try {
doRead = (inputStream.available() > 0);
} catch (final IOException e) {
// no data to read available
}
}
if (doRead) {
// read a single line
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
return reader.readLine();
}
}
return null;
}
protected void fireModuleEvent(final Object module, final int type) {
for (final Object listener : fModuleListeners.getListeners())
((IModuleListener) listener).notifyModule(module, type);
}
@Override
@WrapToScript
public Object wrap(final Object toBeWrapped, @ScriptParameter(defaultValue = "false") boolean useCustomNamespace) throws ExecutionException {
// register new variable in script engine
final String identifier = getCodeFactory().getSaveVariableName(getWrappedVariableName(toBeWrapped));
final boolean reloaded = getScriptEngine().hasVariable(identifier);
getScriptEngine().setVariable(identifier, toBeWrapped);
Logger.trace(Activator.PLUGIN_ID, ICodeFactory.TRACE_MODULE_WRAPPER, "wrapping object: " + toBeWrapped.toString());
// create function wrappers
final Object result = createWrappers(toBeWrapped, identifier, useCustomNamespace);
boolean isTrackedModule = false;
for (final ModuleState state : fModuleTracker.getAvailableModules()) {
if (toBeWrapped.equals(state.getInstance())) {
state.setWrapped(true);
isTrackedModule = true;
break;
}
}
if (!isTrackedModule) {
final ModuleState state = fModuleTracker.addInstance(toBeWrapped);
state.setWrapped(true);
}
// notify listeners
if (reloaded)
fireModuleEvent(toBeWrapped, IModuleListener.RELOADED);
return result;
}
public static final String getWrappedVariableName(final Object toBeWrapped) {
return (MODULE_PREFIX + toBeWrapped.getClass().getName()).replace('.', '_');
}
/**
* Create script wrapper functions for the given instance.
*
* @param instance
* module instance to create wrappers for
* @param identifier
* identifier to be used to store object in script engine scope
* @param useCustomNamespace
* set to <code>true</code> if functions and constants should not be stored to the global namespace but to the return value only
* @return java instance or wrapper object
* @throws ExecutionException
* when execution fails due to script errors
* @throws InterruptedException
* when execution gets interrupted
*/
private Object createWrappers(final Object instance, final String identifier, boolean useCustomNamespace) throws ExecutionException {
final ICodeFactory codeFactory = getCodeFactory();
if (null == codeFactory)
return null;
String wrapperCode;
if (instance instanceof IEnvironment)
wrapperCode = codeFactory.createWrapper((IEnvironment) instance, instance, identifier, useCustomNamespace, getScriptEngine());
else
wrapperCode = codeFactory.createWrapper(this, instance, identifier, useCustomNamespace, getScriptEngine());
if (useCustomNamespace)
return getScriptEngine().inject(new Script("Wrapper(" + instance.getClass().getSimpleName() + ")", wrapperCode), false);
else
getScriptEngine().inject(new Script("Wrapper(" + instance.getClass().getSimpleName() + ")", wrapperCode), false);
return instance;
}
/**
* Execute script code. This method executes script code directly in the running interpreter. Execution is done in the same thread as the caller thread.
*
* @param data
* code to be interpreted
* @return result of code execution
* @throws ExecutionException
* when execution of <i>data</i> fails
*/
@WrapToScript
public final Object execute(final Object data) throws ExecutionException {
return getScriptEngine().inject(data, false);
}
/**
* Terminates script execution immediately. Code following this command will not be executed anymore.
*
* @param value
* return code
* @throws ExitException
* always
*/
@WrapToScript
public final void exit(final @ScriptParameter(defaultValue = ScriptParameter.NULL) Object value) throws ExitException {
throw new ExitException(value);
}
/**
* Include and execute a script file. Quite similar to eval(Object) a source file is opened and its content is executed. Multiple sources are available:
* "workspace://" opens a file relative to the workspace root, "project://" opens a file relative to the current project, "file://" opens a file from the
* file system. All other types of URIs are supported too (like http:// ...). You may also use absolute and relative paths as defined by your local file
* system.
*
* @param filename
* name of file to be included
* @return result of include operation
* @throws ExecutionException
* when included code fails
*/
@WrapToScript
public final Object include(final String filename) throws ExecutionException {
final Object file = ResourceTools.resolve(filename, getScriptEngine().getExecutedFile());
if (file != null)
return getScriptEngine().inject(file, false);
throw new IllegalArgumentException("Cannot locate '" + filename + "'");
}
/**
* Get the current script engine instance.
*
* @return {@link IScriptEngine} instance
*/
@WrapToScript
@Override
public IScriptEngine getScriptEngine() {
final IScriptEngine engine = super.getScriptEngine();
if (engine == null) {
final Job currentJob = Job.getJobManager().currentJob();
if (currentJob instanceof IScriptEngine)
return (IScriptEngine) currentJob;
}
return engine;
}
/**
* Get the generic script code factory registered for this script engine.
*
* @return code factory
*/
private ICodeFactory getCodeFactory() {
return ScriptService.getCodeFactory(getScriptEngine());
}
/**
* Add a jar file to the classpath. Contents of the jar can be accessed right after loading. <i>location</i> can be an URL, a path, a File or an IFile
* instance. Adding a jar location does not necessary mean that its classes can be accessed. If the source is not accessible, then its classes are not
* available for scripting, too.
*
* @param location
* {@link URL}, {@link Path}, {@link File} or {@link IFile}
* @return <code>true</code> when input could be converted to a URL
* @throws MalformedURLException
* invalid URL detected
*/
@WrapToScript
public boolean loadJar(Object location) throws MalformedURLException {
if (!(location instanceof URL)) {
// try to resolve workspace URIs
Object file = ResourceTools.resolve(location, getScriptEngine().getExecutedFile());
if (file instanceof IFile)
file = ((IFile) file).getLocation().toFile();
if (file instanceof File)
location = ((File) file).toURI().toURL();
else
location = new URL(location.toString());
}
if (location instanceof URL) {
getScriptEngine().registerJar((URL) location);
return true;
}
return false;
}
/**
* Open help page on addressed topic. If the given topic matches a method or field from a loaded module, the definition will be opened. If the topic is
* unknown, a search in the whole eclipse help will be launched.
*
* @param topic
* help topic to open (typically a function name)
*/
@WrapToScript(supportedLanguages = "!Python")
public void help(@ScriptParameter(defaultValue = ScriptParameter.NULL) final String topic) {
// sanity check to avoid blue screens for invalid calls, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=479762
if ((topic != null) && !VALID_TOPICS_PATTERN.matcher(topic).matches()) {
printError("Invalid help topic to look for: \"" + topic + "\"", false);
return;
}
if (PlatformUI.isWorkbenchRunning()) {
if (topic != null) {
for (final Object module : getModules()) {
final ModuleDefinition definition = ModuleDefinition.getDefinition(module);
if (definition != null) {
// look for matching method
for (final Method method : definition.getMethods()) {
if (matchesMethod(method, topic)) {
// method found, display help
final String link = definition.getHelpLocation(method.getName());
Display.getDefault().asyncExec(() -> PlatformUI.getWorkbench().getHelpSystem().displayHelpResource(link));
// done
return;
}
}
for (final Field field : definition.getFields()) {
if (matchesField(field, topic)) {
// field found, display help
final String link = definition.getHelpLocation(field.getName());
Display.getDefault().asyncExec(() -> PlatformUI.getWorkbench().getHelpSystem().displayHelpResource(link));
// done
return;
}
}
}
}
// nothing found, start a search in help
Display.getDefault().asyncExec(() -> PlatformUI.getWorkbench().getHelpSystem().search(topic));
} else {
// no topic provided, show main help page
Display.getDefault().asyncExec(() -> PlatformUI.getWorkbench().getHelpSystem().displayHelp());
}
}
}
/**
* Verify that a given name matches the field name or one of its aliases.
*
* @param field
* field to query
* @param name
* name to match
* @return <code>true</code> on match
*/
private boolean matchesField(final Field field, final String name) {
if (name.equalsIgnoreCase(field.getName()))
return true;
final WrapToScript wrapAnnotation = field.getAnnotation(WrapToScript.class);
if (wrapAnnotation != null) {
for (final String alias : wrapAnnotation.alias().split(WrapToScript.DELIMITER))
if (name.equalsIgnoreCase(alias.trim()))
return true;
}
return false;
}
/**
* Verify that a given name matches the method name or one of its aliases.
*
* @param method
* method to query
* @param name
* name to match
* @return <code>true</code> on match
*/
private boolean matchesMethod(final Method method, final String name) {
if (name.equalsIgnoreCase(method.getName()))
return true;
final WrapToScript wrapAnnotation = method.getAnnotation(WrapToScript.class);
if (wrapAnnotation != null) {
for (final String alias : wrapAnnotation.alias().split(WrapToScript.DELIMITER))
if (name.equalsIgnoreCase(alias.trim()))
return true;
}
return false;
}
@Override
public void addModuleCallback(IModuleCallbackProvider callbackProvider) {
fModuleCallbacks.add(callbackProvider);
}
//
/**
* Check if java callbacks are registered for a module method. This method get called on each module function invokation.
* <p>
* ATTENTION: needed by dynamic script code, do not alter synopsis!
* </p>
*
* @param methodToken
* unique method token
* @return <code>true</code> when callbacks are registered
*/
public boolean hasMethodCallback(String methodToken) {
if (getScriptEngine() instanceof AbstractScriptEngine)
((AbstractScriptEngine) getScriptEngine()).checkForCancellation();
final Method method = fRegisteredMethods.get(methodToken);
for (final IModuleCallbackProvider callbackProvider : fModuleCallbacks) {
if ((callbackProvider.hasPreExecutionCallback(method)) || (callbackProvider.hasPostExecutionCallback(method)))
return true;
}
return false;
}
// needed by dynamic script code, do not change synopsis
public void preMethodCallback(String methodToken, Object... parameters) {
final Method method = fRegisteredMethods.get(methodToken);
if (method != null) {
for (final IModuleCallbackProvider callbackProvider : fModuleCallbacks) {
if (callbackProvider.hasPreExecutionCallback(method)) {
callbackProvider.preExecutionCallback(method, parameters);
}
}
}
}
// needed by dynamic script code, do not change synopsis
public void postMethodCallback(String methodToken, Object result) {
final Method method = fRegisteredMethods.get(methodToken);
if (method != null) {
for (final IModuleCallbackProvider callbackProvider : fModuleCallbacks) {
if (callbackProvider.hasPostExecutionCallback(method)) {
callbackProvider.postExecutionCallback(method, result);
}
}
}
}
@Override
public String registerMethod(Method method) {
final String key = Integer.toString(method.toString().hashCode());
fRegisteredMethods.put(key, method);
return key;
}
}