Added ability for ScriptEngines to allow script dependencies to be tracked and changes to trigger script reloading (#1884)
Signed-off-by: Jonathan Gilbert <jpg@trillica.com>pull/2161/head
parent
b55933d769
commit
f0c9a8434d
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.openhab.core.automation.module.script.rulesupport.internal.loader;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.openhab.core.automation.module.script.rulesupport.internal.loader.collection.BidiSetBag;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks dependencies between scripts and reloads dependees
|
||||||
|
*
|
||||||
|
* @author Jonathan Gilbert
|
||||||
|
*/
|
||||||
|
public abstract class DependencyTracker {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(DependencyTracker.class);
|
||||||
|
|
||||||
|
private final BidiSetBag<String, String> scriptToLibs = new BidiSetBag<>();
|
||||||
|
private final ScriptLibraryWatcher scriptLibraryWatcher = new ScriptLibraryWatcher() {
|
||||||
|
@Override
|
||||||
|
void updateFile(String libraryPath) {
|
||||||
|
Set<String> scripts;
|
||||||
|
synchronized (scriptToLibs) {
|
||||||
|
scripts = new HashSet<>(scriptToLibs.getKeys(libraryPath)); // take a copy as it will change as we
|
||||||
|
// reimport
|
||||||
|
}
|
||||||
|
DependencyTracker.this.logger.debug("Library {} changed; reimporting {} scripts...", libraryPath,
|
||||||
|
scripts.size());
|
||||||
|
for (String scriptUrl : scripts) {
|
||||||
|
reimportScript(scriptUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public void activate() {
|
||||||
|
scriptLibraryWatcher.activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deactivate() {
|
||||||
|
scriptLibraryWatcher.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void reimportScript(String scriptPath);
|
||||||
|
|
||||||
|
public void addLibForScript(String scriptPath, String libPath) {
|
||||||
|
synchronized (scriptToLibs) {
|
||||||
|
scriptToLibs.put(scriptPath, libPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeScript(String scriptPath) {
|
||||||
|
synchronized (scriptToLibs) {
|
||||||
|
scriptToLibs.removeKey(scriptPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,7 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
|
||||||
|
|
||||||
private final ScriptEngineManager manager;
|
private final ScriptEngineManager manager;
|
||||||
private final ReadyService readyService;
|
private final ReadyService readyService;
|
||||||
|
private @Nullable DependencyTracker dependencyTracker;
|
||||||
private @Nullable ScheduledExecutorService scheduler;
|
private @Nullable ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
private final Map<String, Set<URL>> urlsByScriptExtension = new ConcurrentHashMap<>();
|
private final Map<String, Set<URL>> urlsByScriptExtension = new ConcurrentHashMap<>();
|
||||||
|
@ -93,6 +94,18 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
|
||||||
super.activate();
|
super.activate();
|
||||||
readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE)
|
readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE)
|
||||||
.withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL)));
|
.withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL)));
|
||||||
|
dependencyTracker = new DependencyTracker() {
|
||||||
|
@Override
|
||||||
|
public void reimportScript(String scriptPath) {
|
||||||
|
logger.debug("Reimporting {}...", scriptPath);
|
||||||
|
try {
|
||||||
|
importFile(new URL(scriptPath));
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
logger.warn("Failed to reimport {} as it cannot be parsed as a URL", scriptPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dependencyTracker.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deactivate
|
@Deactivate
|
||||||
|
@ -104,6 +117,12 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
|
||||||
localScheduler.shutdownNow();
|
localScheduler.shutdownNow();
|
||||||
scheduler = null;
|
scheduler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dependencyTracker != null) {
|
||||||
|
dependencyTracker.deactivate();
|
||||||
|
dependencyTracker = null;
|
||||||
|
}
|
||||||
|
|
||||||
super.deactivate();
|
super.deactivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +183,9 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
|
||||||
|
|
||||||
private void removeFile(URL url) {
|
private void removeFile(URL url) {
|
||||||
dequeueUrl(url);
|
dequeueUrl(url);
|
||||||
manager.removeEngine(getScriptIdentifier(url));
|
String scriptIdentifier = getScriptIdentifier(url);
|
||||||
|
dependencyTracker.removeScript(scriptIdentifier);
|
||||||
|
manager.removeEngine(scriptIdentifier);
|
||||||
loaded.remove(url);
|
loaded.remove(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,11 +205,12 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
|
||||||
StandardCharsets.UTF_8)) {
|
StandardCharsets.UTF_8)) {
|
||||||
logger.info("Loading script '{}'", fileName);
|
logger.info("Loading script '{}'", fileName);
|
||||||
|
|
||||||
ScriptEngineContainer container = manager.createScriptEngine(scriptType,
|
String scriptIdentifier = getScriptIdentifier(url);
|
||||||
getScriptIdentifier(url));
|
ScriptEngineContainer container = manager.createScriptEngine(scriptType, scriptIdentifier);
|
||||||
|
|
||||||
if (container != null) {
|
if (container != null) {
|
||||||
manager.loadScript(container.getIdentifier(), reader);
|
manager.loadScript(container.getIdentifier(), reader,
|
||||||
|
dependency -> dependencyTracker.addLibForScript(scriptIdentifier, dependency));
|
||||||
loaded.add(url);
|
loaded.add(url);
|
||||||
logger.debug("Script loaded: {}", fileName);
|
logger.debug("Script loaded: {}", fileName);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.openhab.core.automation.module.script.rulesupport.internal.loader;
|
||||||
|
|
||||||
|
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
|
||||||
|
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
|
||||||
|
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.WatchEvent;
|
||||||
|
|
||||||
|
import org.openhab.core.OpenHAB;
|
||||||
|
import org.openhab.core.service.AbstractWatchService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for changes to script libraries
|
||||||
|
*
|
||||||
|
* @author Jonathan Gilbert
|
||||||
|
*/
|
||||||
|
abstract class ScriptLibraryWatcher extends AbstractWatchService {
|
||||||
|
|
||||||
|
public static final String LIB_PATH = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib",
|
||||||
|
"javascript");
|
||||||
|
|
||||||
|
ScriptLibraryWatcher() {
|
||||||
|
super(LIB_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean watchSubDirectories() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected WatchEvent.Kind<?>[] getWatchEventKinds(Path path) {
|
||||||
|
return new WatchEvent.Kind<?>[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void processWatchEvent(WatchEvent<?> watchEvent, WatchEvent.Kind<?> kind, Path path) {
|
||||||
|
File file = path.toFile();
|
||||||
|
if (!file.isHidden()) {
|
||||||
|
if (kind.equals(ENTRY_DELETE)) {
|
||||||
|
this.updateFile(file.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.canRead() && (kind.equals(ENTRY_CREATE) || kind.equals(ENTRY_MODIFY))) {
|
||||||
|
this.updateFile(file.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract void updateFile(String filePath);
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.openhab.core.automation.module.script.rulesupport.internal.loader.collection;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bidirectional bag of unique elements. A map allowing multiple, unique values to be stored against a single key.
|
||||||
|
* Provides optimized lookup of values for a key, as well as keys referencing a value.
|
||||||
|
*
|
||||||
|
* @author Jonathan Gilbert - Initial contribution
|
||||||
|
* @param <K> Type of Key
|
||||||
|
* @param <V> Type of Value
|
||||||
|
*/
|
||||||
|
public class BidiSetBag<K, V> {
|
||||||
|
private Map<K, Set<V>> keyToValues = new HashMap<>();
|
||||||
|
private Map<V, Set<K>> valueToKeys = new HashMap<>();
|
||||||
|
|
||||||
|
public void put(K key, V value) {
|
||||||
|
addElement(keyToValues, key, value);
|
||||||
|
addElement(valueToKeys, value, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<V> getValues(K key) {
|
||||||
|
Set<V> existing = keyToValues.get(key);
|
||||||
|
return existing == null ? Collections.emptySet() : Collections.unmodifiableSet(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<K> getKeys(V value) {
|
||||||
|
Set<K> existing = valueToKeys.get(value);
|
||||||
|
return existing == null ? Collections.emptySet() : Collections.unmodifiableSet(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<V> removeKey(K key) {
|
||||||
|
Set<V> values = keyToValues.remove(key);
|
||||||
|
if (values != null) {
|
||||||
|
for (V value : values) {
|
||||||
|
valueToKeys.computeIfPresent(value, (k, v) -> {
|
||||||
|
v.remove(key);
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
} else {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<K> removeValue(V value) {
|
||||||
|
Set<K> keys = valueToKeys.remove(value);
|
||||||
|
if (keys != null) {
|
||||||
|
for (K key : keys) {
|
||||||
|
keyToValues.computeIfPresent(key, (k, v) -> {
|
||||||
|
v.remove(value);
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
} else {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T, U> void addElement(Map<T, Set<U>> map, T key, U value) {
|
||||||
|
Set<U> elements = map.compute(key, (k, l) -> l == null ? new HashSet<>() : l);
|
||||||
|
elements.add(value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.core.automation.module.script;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that allows listener to be notified of script dependencies (libraries)
|
||||||
|
*
|
||||||
|
* @author Jonathan Gilbert - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ScriptDependencyListener extends Consumer<String> {
|
||||||
|
void accept(String dependency);
|
||||||
|
}
|
|
@ -45,6 +45,11 @@ public interface ScriptEngineFactory {
|
||||||
*/
|
*/
|
||||||
String CONTEXT_KEY_EXTENSION_ACCESSOR = "oh.extension-accessor";
|
String CONTEXT_KEY_EXTENSION_ACCESSOR = "oh.extension-accessor";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key to access Dependency Listener {@link ScriptDependencyListener}
|
||||||
|
*/
|
||||||
|
String CONTEXT_KEY_DEPENDENCY_LISTENER = "oh.dependency-listener";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method returns a list of file extensions and MimeTypes that are supported by the ScriptEngine, e.g. py,
|
* This method returns a list of file extensions and MimeTypes that are supported by the ScriptEngine, e.g. py,
|
||||||
* application/python, js, application/javascript, etc.
|
* application/python, js, application/javascript, etc.
|
||||||
|
|
|
@ -45,6 +45,16 @@ public interface ScriptEngineManager {
|
||||||
*/
|
*/
|
||||||
void loadScript(String engineIdentifier, InputStreamReader scriptData);
|
void loadScript(String engineIdentifier, InputStreamReader scriptData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a script and initializes its scope variables
|
||||||
|
*
|
||||||
|
* @param engineIdentifier the unique identifier for the ScriptEngine (script file path or UUID)
|
||||||
|
* @param scriptData the content of the script
|
||||||
|
* @param scriptDependencyListener listener to be notified of script dependencies
|
||||||
|
*/
|
||||||
|
void loadScript(String engineIdentifier, InputStreamReader scriptData,
|
||||||
|
ScriptDependencyListener scriptDependencyListener);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unloads the ScriptEngine loaded with the engineIdentifier
|
* Unloads the ScriptEngine loaded with the engineIdentifier
|
||||||
*
|
*
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.core.automation.module.script.internal;
|
package org.openhab.core.automation.module.script.internal;
|
||||||
|
|
||||||
|
import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_DEPENDENCY_LISTENER;
|
||||||
import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_ENGINE_IDENTIFIER;
|
import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_ENGINE_IDENTIFIER;
|
||||||
import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_EXTENSION_ACCESSOR;
|
import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_EXTENSION_ACCESSOR;
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ import javax.script.SimpleScriptContext;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.automation.module.script.ScriptDependencyListener;
|
||||||
import org.openhab.core.automation.module.script.ScriptEngineContainer;
|
import org.openhab.core.automation.module.script.ScriptEngineContainer;
|
||||||
import org.openhab.core.automation.module.script.ScriptEngineFactory;
|
import org.openhab.core.automation.module.script.ScriptEngineFactory;
|
||||||
import org.openhab.core.automation.module.script.ScriptEngineManager;
|
import org.openhab.core.automation.module.script.ScriptEngineManager;
|
||||||
|
@ -136,17 +138,9 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
|
||||||
logger.debug("Added ScriptEngine for language '{}' with identifier: {}", scriptType,
|
logger.debug("Added ScriptEngine for language '{}' with identifier: {}", scriptType,
|
||||||
engineIdentifier);
|
engineIdentifier);
|
||||||
|
|
||||||
ScriptContext scriptContext = engine.getContext();
|
addAttributeToScriptContext(engine, CONTEXT_KEY_ENGINE_IDENTIFIER, engineIdentifier);
|
||||||
|
addAttributeToScriptContext(engine, CONTEXT_KEY_EXTENSION_ACCESSOR, scriptExtensionManager);
|
||||||
|
|
||||||
if (scriptContext == null) {
|
|
||||||
scriptContext = new SimpleScriptContext();
|
|
||||||
engine.setContext(scriptContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptContext.setAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER, engineIdentifier,
|
|
||||||
ScriptContext.ENGINE_SCOPE);
|
|
||||||
scriptContext.setAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR, scriptExtensionManager,
|
|
||||||
ScriptContext.ENGINE_SCOPE);
|
|
||||||
} else {
|
} else {
|
||||||
logger.error("ScriptEngine for language '{}' could not be created for identifier: {}", scriptType,
|
logger.error("ScriptEngine for language '{}' could not be created for identifier: {}", scriptType,
|
||||||
engineIdentifier);
|
engineIdentifier);
|
||||||
|
@ -161,11 +155,22 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadScript(String engineIdentifier, InputStreamReader scriptData) {
|
public void loadScript(String engineIdentifier, InputStreamReader scriptData) {
|
||||||
|
loadScript(engineIdentifier, scriptData, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadScript(String engineIdentifier, InputStreamReader scriptData,
|
||||||
|
@Nullable ScriptDependencyListener dependencyListener) {
|
||||||
ScriptEngineContainer container = loadedScriptEngineInstances.get(engineIdentifier);
|
ScriptEngineContainer container = loadedScriptEngineInstances.get(engineIdentifier);
|
||||||
if (container == null) {
|
if (container == null) {
|
||||||
logger.error("Could not load script, as no ScriptEngine has been created");
|
logger.error("Could not load script, as no ScriptEngine has been created");
|
||||||
} else {
|
} else {
|
||||||
ScriptEngine engine = container.getScriptEngine();
|
ScriptEngine engine = container.getScriptEngine();
|
||||||
|
|
||||||
|
if (dependencyListener != null) {
|
||||||
|
addAttributeToScriptContext(engine, CONTEXT_KEY_DEPENDENCY_LISTENER, dependencyListener);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
engine.eval(scriptData);
|
engine.eval(scriptData);
|
||||||
if (engine instanceof Invocable) {
|
if (engine instanceof Invocable) {
|
||||||
|
@ -235,4 +240,15 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
|
||||||
public boolean isSupported(String scriptType) {
|
public boolean isSupported(String scriptType) {
|
||||||
return findEngineFactory(scriptType) != null;
|
return findEngineFactory(scriptType) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addAttributeToScriptContext(ScriptEngine engine, String name, Object value) {
|
||||||
|
ScriptContext scriptContext = engine.getContext();
|
||||||
|
|
||||||
|
if (scriptContext == null) {
|
||||||
|
scriptContext = new SimpleScriptContext();
|
||||||
|
engine.setContext(scriptContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptContext.setAttribute(name, value, ScriptContext.ENGINE_SCOPE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue