增加脚本模块
This commit is contained in:
parent
14cc4224ef
commit
c2e39e9ca9
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>jetlinks-components</artifactId>
|
||||
<groupId>org.jetlinks.community</groupId>
|
||||
<version>1.20.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>script-component</artifactId>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.graalvm.js</groupId>-->
|
||||
<!-- <artifactId>js-scriptengine</artifactId>-->
|
||||
<!-- <version>21.2.0</version>-->
|
||||
<!-- <optional>true</optional>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jetlinks</groupId>
|
||||
<artifactId>reactor-ql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package org.jetlinks.community.script;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineFactory;
|
||||
import java.io.File;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class AbstractScriptFactory implements ScriptFactory {
|
||||
|
||||
static Class<?>[] DEFAULT_DENIES = {
|
||||
System.class,
|
||||
File.class,
|
||||
Paths.class,
|
||||
ObjectInputStream.class,
|
||||
ObjectOutputStream.class,
|
||||
Thread.class,
|
||||
Runtime.class,
|
||||
ScriptEngine.class,
|
||||
ScriptEngineFactory.class
|
||||
};
|
||||
|
||||
static Class<?>[] DEFAULT_ALLOWS = {
|
||||
byte.class, short.class, int.class, long.class, char.class, float.class, double.class, boolean.class,
|
||||
Byte.class, Short.class, Integer.class, Long.class, Character.class, Float.class, Double.class, Boolean.class,
|
||||
BigDecimal.class, BigInteger.class,
|
||||
String.class,
|
||||
HashMap.class, ConcurrentHashMap.class, LinkedHashMap.class,
|
||||
Date.class, LocalDateTime.class,
|
||||
ArrayList.class,LinkedList.class
|
||||
};
|
||||
|
||||
private final Set<String> denies = new HashSet<>();
|
||||
private final Set<String> allows = new HashSet<>();
|
||||
|
||||
public AbstractScriptFactory() {
|
||||
denies.add("*");
|
||||
allows(DEFAULT_ALLOWS);
|
||||
//denies(DEFAULT_DENIES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void allows(Collection<Class<?>> allowTypes) {
|
||||
allows.addAll(allowTypes.stream().map(Class::getName).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void allows(Class<?>... allowTypes) {
|
||||
allows(Arrays.asList(allowTypes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void denies(Collection<Class<?>> allowTypes) {
|
||||
denies.addAll(allowTypes.stream().map(Class::getName).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void denies(Class<?>... allowTypes) {
|
||||
denies(Arrays.asList(allowTypes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void allowsPattern(String... allowTypes) {
|
||||
allowsPattern(Arrays.asList(allowTypes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void allowsPattern(Collection<String> allowTypes) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deniesPattern(String... allowTypes) {
|
||||
deniesPattern(Arrays.asList(allowTypes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deniesPattern(Collection<String> allowTypes) {
|
||||
|
||||
}
|
||||
|
||||
public final boolean isDenied(Class<?> type) {
|
||||
return isDenied(type.getName());
|
||||
}
|
||||
|
||||
public final boolean isDenied(String typeName) {
|
||||
if (allows.contains(typeName)) {
|
||||
return false;
|
||||
}
|
||||
return denies.contains("*") || denies.contains(typeName);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package org.jetlinks.community.script;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class AbstractScriptFactoryProvider implements ScriptFactoryProvider {
|
||||
|
||||
private final Set<String> supports = new HashSet<>();
|
||||
|
||||
public AbstractScriptFactoryProvider(String... supports) {
|
||||
this.supports.addAll(Arrays.asList(supports));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupport(String langOrMediaType) {
|
||||
return supports.contains(langOrMediaType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract ScriptFactory factory();
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package org.jetlinks.community.script;
|
||||
|
||||
import org.jetlinks.community.script.context.ExecutionContext;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 已编译的脚本信息.
|
||||
*
|
||||
* @author zhouhao
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface CompiledScript {
|
||||
|
||||
/**
|
||||
* 使用指定上下文执行脚本
|
||||
*
|
||||
* @param context 上下文
|
||||
* @return 脚本返回结果
|
||||
*/
|
||||
Object call(ExecutionContext context);
|
||||
|
||||
default Object call(Map<String, Object> context) {
|
||||
return call(ExecutionContext.create(context));
|
||||
}
|
||||
|
||||
default Object call() {
|
||||
return call(Collections.emptyMap());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package org.jetlinks.community.script;
|
||||
|
||||
import org.jetlinks.community.script.context.ExecutionContext;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 提供支持暴露方法的脚本
|
||||
*
|
||||
* @param <T> 暴露方法的实例类型
|
||||
* @author zhouhao
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface ExposedScript<T> {
|
||||
|
||||
/**
|
||||
* 使用指定的暴露实例和上下文来执行脚本.在脚本中访问暴露的方法将调用指定实例的指定方法.
|
||||
*
|
||||
* @param expose 需要暴露的实例
|
||||
* @param context 上下文
|
||||
* @return 脚本执行结果
|
||||
*/
|
||||
Object call(T expose, ExecutionContext context);
|
||||
|
||||
default Object call(T expose, Map<String, Object> context) {
|
||||
return call(expose, ExecutionContext.create(context));
|
||||
}
|
||||
|
||||
default Object call(T expose) {
|
||||
return call(expose, ExecutionContext.create());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package org.jetlinks.community.script;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(staticName = "of")
|
||||
public class Script {
|
||||
|
||||
@NonNull
|
||||
private final String name;
|
||||
@NonNull
|
||||
private final String content;
|
||||
|
||||
private final Object source;
|
||||
|
||||
public static Script of(String name, String content) {
|
||||
return Script.of(name, content, null);
|
||||
}
|
||||
|
||||
public Script content(String content) {
|
||||
return of(name, content, source);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package org.jetlinks.community.script;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
public interface ScriptFactory {
|
||||
|
||||
void allows(Collection<Class<?>> allowTypes);
|
||||
|
||||
void allows(Class<?>... allowTypes);
|
||||
|
||||
void denies(Collection<Class<?>> allowTypes);
|
||||
|
||||
void denies(Class<?>... allowTypes);
|
||||
|
||||
void allowsPattern(Collection<String> allowTypes);
|
||||
|
||||
void allowsPattern(String... allowTypes);
|
||||
|
||||
void deniesPattern(Collection<String> allowTypes);
|
||||
|
||||
void deniesPattern(String... allowTypes);
|
||||
|
||||
Object convertToJavaType(Object data);
|
||||
|
||||
/**
|
||||
* 编译脚本,编译后通过@{@link CompiledScript#call(Map)}来执行脚本.
|
||||
*
|
||||
* <pre>{@code
|
||||
* CompiledScript script = factory.compile(Script.of("test","return arg0+2;"));
|
||||
*
|
||||
* // val = 12
|
||||
* Object val = script.call(Collections.singletonMap("arg0",10));
|
||||
* }</pre>
|
||||
*
|
||||
* @param script 脚本
|
||||
* @return 编译后的可执行脚本
|
||||
*/
|
||||
CompiledScript compile(Script script);
|
||||
|
||||
/**
|
||||
* 编译脚本并将指定的类的方法暴露到脚本中,在脚本中可以直接调用内嵌对象的方法.比如:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* public class Helper{
|
||||
* public int max(int a,int b){
|
||||
* return Math.max(a,b);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* CompiledScript script = factory.compile(Script.of("test","return max(1,2);"),Helper.class)
|
||||
*
|
||||
* Object val = script.call(new Helper());
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param script 脚本
|
||||
* @param expose 要暴露的方法
|
||||
* @return CompiledScript
|
||||
*/
|
||||
<T> ExposedScript<T> compileExpose(Script script, Class<? super T> expose);
|
||||
|
||||
/**
|
||||
* 将脚本构造为一个接口的实现,在脚本中定义方法,然后将脚本的方法绑定到接口上.
|
||||
* <p>
|
||||
* 如果在脚本中没有定义方法的实现,调用方法后将返回<code>null</code>
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* public interface MyInterface{
|
||||
*
|
||||
* Object encode(Object data);
|
||||
*
|
||||
* }
|
||||
*
|
||||
* MyInterface inf = factory.bind(Script.of("function encode(data){ return 1; }"),MyInterface.class);
|
||||
*
|
||||
* //执行,将调用脚本中的encode方法
|
||||
* Object val = inf.encode(arg);
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param script 脚本
|
||||
* @param interfaceType 接口类型
|
||||
* @param <T> 泛型
|
||||
* @return 接口代理实现
|
||||
*/
|
||||
<T> T bind(Script script,
|
||||
Class<T> interfaceType);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package org.jetlinks.community.script;
|
||||
|
||||
public interface ScriptFactoryProvider {
|
||||
|
||||
boolean isSupport(String langOrMediaType);
|
||||
|
||||
ScriptFactory factory();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package org.jetlinks.community.script;
|
||||
|
||||
import org.jetlinks.community.script.nashorn.NashornScriptFactoryProvider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public class Scripts {
|
||||
private final static List<ScriptFactoryProvider> providers = new CopyOnWriteArrayList<>();
|
||||
|
||||
private final static Map<String, ScriptFactory> globals = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
providers.add(new NashornScriptFactoryProvider());
|
||||
|
||||
try {
|
||||
for (ScriptFactoryProvider scriptFactoryProvider : ServiceLoader.load(ScriptFactoryProvider.class)) {
|
||||
providers.add(scriptFactoryProvider);
|
||||
}
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
private static ScriptFactoryProvider lookup(String lang) {
|
||||
for (ScriptFactoryProvider provider : providers) {
|
||||
if (provider.isSupport(lang)) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
throw new UnsupportedOperationException("unsupported script lang:" + lang);
|
||||
}
|
||||
|
||||
public static ScriptFactory getFactory(String lang) {
|
||||
return globals.computeIfAbsent(lang, Scripts::newFactory);
|
||||
}
|
||||
|
||||
public static ScriptFactory newFactory(String lang) {
|
||||
return lookup(lang).factory();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package org.jetlinks.community.script.context;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import javax.script.Bindings;
|
||||
import javax.script.ScriptContext;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class CompositeExecutionContext implements ExecutionContext {
|
||||
private ExecutionContext[] contexts;
|
||||
|
||||
@Override
|
||||
public synchronized ExecutionContext merge(ExecutionContext target) {
|
||||
|
||||
contexts = Arrays.copyOf(contexts, contexts.length + 1);
|
||||
contexts[contexts.length - 1] = target;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBindings(Bindings bindings, int scope) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bindings getBindings(int scope) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value, int scope) {
|
||||
contexts[contexts.length - 1].setAttribute(name, value, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String name, int scope) {
|
||||
|
||||
return getAttribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object removeAttribute(String name, int scope) {
|
||||
for (ExecutionContext context : contexts) {
|
||||
if (context.hasAttribute(name)) {
|
||||
return context.removeAttribute(name, scope);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String name) {
|
||||
for (ExecutionContext context : contexts) {
|
||||
if (context.hasAttribute(name)) {
|
||||
return context.getAttribute(name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAttributesScope(String name) {
|
||||
for (ExecutionContext context : contexts) {
|
||||
if (context.hasAttribute(name)) {
|
||||
return context.getAttributesScope(name);
|
||||
}
|
||||
}
|
||||
return ENGINE_SCOPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAttribute(String key) {
|
||||
for (ExecutionContext context : contexts) {
|
||||
if (context.hasAttribute(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Writer getWriter() {
|
||||
for (ExecutionContext context : contexts) {
|
||||
Writer writer = context.getWriter();
|
||||
if (writer != null) {
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Writer getErrorWriter() {
|
||||
for (ExecutionContext context : contexts) {
|
||||
Writer writer = context.getErrorWriter();
|
||||
if (writer != null) {
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWriter(Writer writer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setErrorWriter(Writer writer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader getReader() {
|
||||
for (ExecutionContext context : contexts) {
|
||||
Reader reader = context.getReader();
|
||||
if (reader != null) {
|
||||
return reader;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReader(Reader reader) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getScopes() {
|
||||
return DefaultExecutionContext.scopes;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package org.jetlinks.community.script.context;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import javax.script.Bindings;
|
||||
import javax.script.ScriptContext;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
class DefaultExecutionContext implements ExecutionContext {
|
||||
|
||||
static final List<Integer> scopes = Arrays.asList(ENGINE_SCOPE, GLOBAL_SCOPE);
|
||||
|
||||
private final Map<String, Object>[] ctx;
|
||||
|
||||
private final Function<String, Object> fallback;
|
||||
|
||||
public DefaultExecutionContext(Map<String, Object>[] ctx) {
|
||||
this(ctx, ignore -> null);
|
||||
}
|
||||
|
||||
public DefaultExecutionContext(Map<String, Object>[] ctx,
|
||||
Function<String, Object> fallback) {
|
||||
this.ctx = Arrays.copyOf(ctx, ctx.length + 1);
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBindings(Bindings bindings, int scope) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bindings getBindings(int scope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Object> self() {
|
||||
Map<String, Object> self = ctx[ctx.length - 1];
|
||||
|
||||
return self == null ?
|
||||
ctx[ctx.length - 1] = Maps.newHashMapWithExpectedSize(16)
|
||||
: self;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value, int scope) {
|
||||
self().put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String name, int scope) {
|
||||
|
||||
return getAttribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object removeAttribute(String name, int scope) {
|
||||
return self().remove(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String name) {
|
||||
for (Map<String, Object> attr : ctx) {
|
||||
if (attr != null && attr.containsKey(name)) {
|
||||
return attr.get(name);
|
||||
}
|
||||
}
|
||||
return fallback.apply(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAttribute(String key) {
|
||||
for (Map<String, Object> attr : ctx) {
|
||||
if (attr != null && attr.containsKey(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return fallback.apply(key) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAttributesScope(String name) {
|
||||
return ENGINE_SCOPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Writer getWriter() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Writer getErrorWriter() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWriter(Writer writer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setErrorWriter(Writer writer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader getReader() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReader(Reader reader) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getScopes() {
|
||||
return scopes;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package org.jetlinks.community.script.context;
|
||||
|
||||
import javax.script.ScriptContext;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public interface ExecutionContext extends ScriptContext {
|
||||
|
||||
boolean hasAttribute(String key);
|
||||
|
||||
@SafeVarargs
|
||||
static ExecutionContext create(Map<String, Object>... context) {
|
||||
return new DefaultExecutionContext(context);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
static ExecutionContext create(Function<String, Object> fallback, Map<String, Object>... context) {
|
||||
return new DefaultExecutionContext(context, fallback);
|
||||
}
|
||||
|
||||
static ExecutionContext compose(ExecutionContext... contexts) {
|
||||
return new CompositeExecutionContext(contexts);
|
||||
}
|
||||
|
||||
default ExecutionContext merge(ExecutionContext target) {
|
||||
return compose(this, target);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package org.jetlinks.community.script.jsr223;
|
||||
|
||||
import org.jetlinks.community.script.CompiledScript;
|
||||
import org.jetlinks.community.script.ExposedScript;
|
||||
import org.jetlinks.community.script.Script;
|
||||
|
||||
import javax.script.ScriptContext;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public abstract class JavaScriptFactory extends Jsr223ScriptFactory {
|
||||
|
||||
public JavaScriptFactory() {
|
||||
super();
|
||||
}
|
||||
|
||||
protected final String prepare(Script script) {
|
||||
StringJoiner wrap = new StringJoiner("\n");
|
||||
//使用匿名函数包装,防止变量逃逸
|
||||
wrap.add("(function(){");
|
||||
//注入安全性控制代码
|
||||
//✨企业版还支持资源限制(防止死循环等操作)
|
||||
wrap.add("function exit(){};" +
|
||||
"function Function(e){return function(){}};" +
|
||||
"function quit(){};" +
|
||||
"function eval(s){};" +
|
||||
"this.eval = function(e){};" +
|
||||
"function readFully(){};" +
|
||||
"function readLine(){};" +
|
||||
"const print = console.log;" +
|
||||
"const echo = console.log;");
|
||||
|
||||
wrap.add("/* script start */");
|
||||
|
||||
wrap.add(script.getContent());
|
||||
|
||||
wrap.add("/* script end */");
|
||||
wrap.add("})()");
|
||||
|
||||
return wrap.toString();
|
||||
}
|
||||
|
||||
private final Set<Method> ignoreMethod = new HashSet<>(
|
||||
Stream
|
||||
.concat(
|
||||
Arrays.stream(Object.class.getMethods()),
|
||||
Arrays.stream(Callable.class.getMethods())
|
||||
)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
|
||||
@Override
|
||||
public <T> ExposedScript<T> compileExpose(Script script,
|
||||
Class<? super T> expose) {
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
Set<String> distinct = new HashSet<>();
|
||||
joiner.add(
|
||||
Arrays.stream(expose.getMethods())
|
||||
.filter(method -> !ignoreMethod.contains(method))
|
||||
.sorted(Comparator.comparingInt(Method::getParameterCount).reversed())
|
||||
.map(method -> {
|
||||
if (!distinct.add(method.getName())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder call = new StringBuilder("function ")
|
||||
.append(method.getName())
|
||||
.append("(){");
|
||||
if (method.getParameterCount() == 0) {
|
||||
call.append("return $$__that.")
|
||||
.append(method.getName())
|
||||
.append("();");
|
||||
} else {
|
||||
|
||||
for (int i = 0; i <= method.getParameterCount(); i++) {
|
||||
String[] args = new String[i];
|
||||
for (int j = 0; j < i; j++) {
|
||||
args[j] = "arguments[" + j + "]";
|
||||
}
|
||||
String arg = String.join(",", args);
|
||||
call.append("if(arguments.length==").append(i).append("){")
|
||||
.append("return $$__that.")
|
||||
.append(method.getName())
|
||||
.append("(").append(arg).append(");")
|
||||
.append("}");
|
||||
}
|
||||
}
|
||||
|
||||
call.append("}");
|
||||
|
||||
return call.toString();
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.joining())
|
||||
);
|
||||
|
||||
joiner.add(script.getContent());
|
||||
CompiledScript compiledScript = compile(script.content(joiner.toString()));
|
||||
|
||||
return (instance, ctx) -> {
|
||||
ctx.setAttribute("$$__that", instance, ScriptContext.ENGINE_SCOPE);
|
||||
return compiledScript.call(ctx);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String createFunctionMapping(Method[] methods) {
|
||||
return Arrays
|
||||
.stream(methods)
|
||||
.map(Method::getName)
|
||||
.map(m -> m + ":typeof(" + m + ")==='undefined'?null:" + m)
|
||||
.collect(Collectors.joining(",",
|
||||
"{", "}"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
package org.jetlinks.community.script.jsr223;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetlinks.community.script.AbstractScriptFactory;
|
||||
import org.jetlinks.community.script.CompiledScript;
|
||||
import org.jetlinks.community.script.Script;
|
||||
import org.jetlinks.community.script.context.ExecutionContext;
|
||||
import org.jetlinks.reactor.ql.utils.CastUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.script.Compilable;
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptContext;
|
||||
import javax.script.ScriptEngine;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
public abstract class Jsr223ScriptFactory extends AbstractScriptFactory {
|
||||
|
||||
private final ScriptEngine engine;
|
||||
|
||||
public Jsr223ScriptFactory() {
|
||||
this.engine = createEngine();
|
||||
}
|
||||
|
||||
|
||||
protected abstract ScriptEngine createEngine();
|
||||
|
||||
@Override
|
||||
public final CompiledScript compile(Script script) {
|
||||
return compile(script, true);
|
||||
}
|
||||
|
||||
private CompiledScript compile(Script script, boolean convert) {
|
||||
|
||||
ExecutionContext ctx = ExecutionContext.create();
|
||||
|
||||
ctx.setAttribute("console", new Jsr223ScriptFactory.Console(
|
||||
LoggerFactory.getLogger("org.jetlinks.community.script." + script.getName())),
|
||||
ScriptContext.ENGINE_SCOPE);
|
||||
|
||||
ctx.setAttribute("engine", null, ScriptContext.ENGINE_SCOPE);
|
||||
|
||||
javax.script.CompiledScript compiledScript = compile0(script);
|
||||
|
||||
return (context) -> Jsr223ScriptFactory.this
|
||||
.eval(compiledScript,
|
||||
script,
|
||||
ExecutionContext.compose(ctx, context),
|
||||
convert);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private Object eval(javax.script.CompiledScript script,
|
||||
Script source,
|
||||
ExecutionContext context,
|
||||
boolean convert) {
|
||||
Object res = script.eval(acceptScriptContext(source, context));
|
||||
|
||||
return convert ? convertToJavaType(res) : res;
|
||||
}
|
||||
|
||||
|
||||
protected ExecutionContext acceptScriptContext(Script script, ExecutionContext context) {
|
||||
return context;
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
public static class Console {
|
||||
private final Logger logger;
|
||||
|
||||
public void trace(String text, Object... args) {
|
||||
logger.trace(text, args);
|
||||
}
|
||||
|
||||
public void warn(String text, Object... args) {
|
||||
logger.warn(text, args);
|
||||
}
|
||||
|
||||
public void log(String text, Object... args) {
|
||||
logger.debug(text, args);
|
||||
}
|
||||
|
||||
public void error(String text, Object... args) {
|
||||
logger.error(text, args);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("all")
|
||||
public final <T> T bind(Script script, Class<T> interfaceType) {
|
||||
String returns = createFunctionMapping(interfaceType.getDeclaredMethods());
|
||||
String content = script.getContent() + "\n return " + returns + ";";
|
||||
|
||||
CompiledScript compiledScript = compile(script.content(content), false);
|
||||
Object source = compiledScript.call(Collections.emptyMap());
|
||||
Set<Method> ignoreMethods = new HashSet<>();
|
||||
|
||||
return (T) Proxy.newProxyInstance(
|
||||
interfaceType.getClassLoader(),
|
||||
new Class[]{interfaceType},
|
||||
(proxy, method, args) -> {
|
||||
//方法已经被忽略执行
|
||||
if (ignoreMethods.contains(method)) {
|
||||
return convertValue(method, null);
|
||||
}
|
||||
try {
|
||||
return this.convertValue(method,
|
||||
((Invocable) engine).invokeMethod(source, method.getName(), args));
|
||||
} catch (Throwable e) {
|
||||
if (e instanceof NoSuchMethodException) {
|
||||
log.info("method [{}] undefined in script", method, e);
|
||||
//脚本未定义方法
|
||||
ignoreMethods.add(method);
|
||||
}
|
||||
}
|
||||
return convertValue(method, null);
|
||||
});
|
||||
}
|
||||
|
||||
protected boolean valueIsUndefined(Object value) {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
public Object convertToJavaType(Object value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
private Object convertValue(Method method, Object value) {
|
||||
if (valueIsUndefined(value)) {
|
||||
return null;
|
||||
}
|
||||
value = convertToJavaType(value);
|
||||
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (returnType == void.class) {
|
||||
return null;
|
||||
}
|
||||
if (returnType == int.class) {
|
||||
return CastUtils.castNumber(value).intValue();
|
||||
}
|
||||
if (returnType == float.class) {
|
||||
return CastUtils.castNumber(value).floatValue();
|
||||
}
|
||||
if (returnType == double.class) {
|
||||
return CastUtils.castNumber(value).doubleValue();
|
||||
}
|
||||
if (returnType == long.class) {
|
||||
return CastUtils.castNumber(value).longValue();
|
||||
}
|
||||
if (returnType == byte.class) {
|
||||
return CastUtils.castNumber(value).byteValue();
|
||||
}
|
||||
if (returnType == short.class) {
|
||||
return CastUtils.castNumber(value).shortValue();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
protected abstract String createFunctionMapping(Method[] methods);
|
||||
|
||||
@SneakyThrows
|
||||
private javax.script.CompiledScript compile0(Script script) {
|
||||
String rewriteScript = prepare(script);
|
||||
log.debug("compile script :\n{}", rewriteScript);
|
||||
return ((Compilable) engine).compile(rewriteScript);
|
||||
}
|
||||
|
||||
protected String prepare(Script script) {
|
||||
return script.getContent();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package org.jetlinks.community.script.nashorn;
|
||||
|
||||
import jdk.nashorn.api.scripting.ClassFilter;
|
||||
import jdk.nashorn.api.scripting.JSObject;
|
||||
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
|
||||
import jdk.nashorn.internal.runtime.Undefined;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetlinks.community.script.jsr223.JavaScriptFactory;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class NashornScriptFactory extends JavaScriptFactory implements ClassFilter {
|
||||
|
||||
@Override
|
||||
protected ScriptEngine createEngine() {
|
||||
return new NashornScriptEngineFactory()
|
||||
.getScriptEngine(new String[]{"-doe", "--language=es6", "--global-per-engine"},
|
||||
NashornScriptFactory.class.getClassLoader(),
|
||||
this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exposeToScripts(String s) {
|
||||
return !isDenied(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean valueIsUndefined(Object value) {
|
||||
return value == null || value instanceof Undefined;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToJavaType(Object value) {
|
||||
return convertToJavaObject(value);
|
||||
}
|
||||
|
||||
public static Object convertToJavaObject(Object object) {
|
||||
if (object instanceof JSObject) {
|
||||
return convertJSObject(((JSObject) object));
|
||||
}
|
||||
if (object instanceof Undefined) {
|
||||
return null;
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
public static Object convertJSObject(JSObject jsObject) {
|
||||
if (jsObject.isArray()) {
|
||||
return jsObject
|
||||
.values()
|
||||
.stream()
|
||||
.map(obj -> {
|
||||
if (obj instanceof JSObject) {
|
||||
return convertJSObject(((JSObject) obj));
|
||||
}
|
||||
return obj;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
if (jsObject instanceof Map) {
|
||||
Map<Object, Object> newMap = new HashMap<>(((Map<?, ?>) jsObject).size());
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) jsObject).entrySet()) {
|
||||
Object val = entry.getValue();
|
||||
if (val instanceof JSObject) {
|
||||
val = convertJSObject(((JSObject) val));
|
||||
}
|
||||
newMap.put(entry.getKey(), val);
|
||||
}
|
||||
return newMap;
|
||||
}
|
||||
throw new UnsupportedOperationException("unsupported type:" + jsObject);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package org.jetlinks.community.script.nashorn;
|
||||
|
||||
import org.jetlinks.community.script.AbstractScriptFactoryProvider;
|
||||
import org.jetlinks.community.script.ScriptFactory;
|
||||
|
||||
public class NashornScriptFactoryProvider extends AbstractScriptFactoryProvider {
|
||||
|
||||
public NashornScriptFactoryProvider() {
|
||||
super("js", "javascript", "nashorn");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptFactory factory() {
|
||||
return new NashornScriptFactory();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue