[Initial] Initial Commit;

This commit is contained in:
2021-08-13 00:29:19 +08:00
commit 3dda31efb7
46 changed files with 2629 additions and 0 deletions

View File

@ -0,0 +1,17 @@
package net.lamgc.oracle.sentry.script;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
/**
* @author LamGC
*/
public abstract class Script {
/**
* 获取脚本信息.
* @return 返回脚本 ScriptInfo 对象.
*/
public abstract ScriptInfo getScriptInfo();
}

View File

@ -0,0 +1,14 @@
package net.lamgc.oracle.sentry.script;
import net.lamgc.oracle.sentry.ComputeInstanceManager;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
/**
* @author LamGC
*/
public final record ScriptComponent(
ScriptHttpClient HTTP,
ComputeInstanceManager InstanceManager
) {
}

View File

@ -0,0 +1,60 @@
package net.lamgc.oracle.sentry.script;
import java.util.Objects;
/**
* 脚本信息.
* @author LamGC
*/
public class ScriptInfo {
private String group;
private String artifact;
private String version;
public String getGroup() {
return group;
}
public String getArtifact() {
return artifact;
}
public String getVersion() {
return version;
}
@Override
public String toString() {
return getGroup() + ":" + getArtifact() + ":" + getVersion();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ScriptInfo that = (ScriptInfo) o;
return group.equals(that.group) && artifact.equals(that.artifact) && version.equals(that.version);
}
@Override
public int hashCode() {
return Objects.hash(group, artifact, version);
}
public void setGroup(String group) {
this.group = group;
}
public void setArtifact(String artifact) {
this.artifact = artifact;
}
public void setVersion(String version) {
this.version = version;
}
}

View File

@ -0,0 +1,34 @@
package net.lamgc.oracle.sentry.script;
import java.io.File;
/**
* 脚本加载器.
* @author LamGC
*/
public interface ScriptLoader {
/**
* 是否可以加载.
* @param scriptFile 脚本文件.
* @return 如果可以加载, 返回 {@code true}.
*/
boolean canLoad(File scriptFile);
/**
* 加载脚本.
* @param context 脚本上下文.
* @param scriptFile 脚本文件.
* @return 返回脚本对象.
* @throws Exception 当 Loader 抛出异常时, 将视为脚本加载失败, 该脚本跳过加载.
*/
Script loadScript(ScriptComponent context, File scriptFile) throws Exception;
/**
* 获取脚本信息.
* @param script 脚本对象.
* @return 返回脚本信息.
*/
ScriptInfo getScriptInfo(Script script);
}

View File

@ -0,0 +1,119 @@
package net.lamgc.oracle.sentry.script;
import com.google.common.base.Throwables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 脚本管理器.
* @author LamGC
*/
public final class ScriptManager {
private final static Logger log = LoggerFactory.getLogger(ScriptManager.class);
private final Set<ScriptLoader> loaders = new HashSet<>();
private final File scriptsLocation;
private final ScriptComponent context;
private final Map<ScriptInfo, Script> scripts = new ConcurrentHashMap<>();
public ScriptManager(File scriptsLocation, ScriptComponent context) {
this.scriptsLocation = scriptsLocation;
this.context = context;
loadScriptLoaders();
}
private synchronized void loadScriptLoaders() {
if (!loaders.isEmpty()) {
return;
}
ServiceLoader<ScriptLoader> serviceLoader = ServiceLoader.load(ScriptLoader.class);
for (ScriptLoader scriptLoader : serviceLoader) {
loaders.add(scriptLoader);
}
log.info("存在 {} 个加载器可用.", loaders.size());
}
/**
* 从文件中加载一个脚本.
* @param scriptFile 脚本文件.
* @return 如果加载成功, 返回 {@code true}, 加载失败或无加载器可用时返回 {@code false}
* @throws InvocationTargetException 当加载器加载脚本抛出异常时, 将通过该异常包装后抛出.
* @throws NullPointerException 当 scriptFile 为 {@code null} 时抛出.
*/
public boolean loadScript(File scriptFile) throws InvocationTargetException {
Objects.requireNonNull(scriptFile);
for (ScriptLoader loader : loaders) {
Script script;
try {
if (loader.canLoad(scriptFile)) {
script = loader.loadScript(context, scriptFile);
if (script == null) {
log.warn("加载器未能正确加载脚本, 已跳过该脚本.(ScriptName: {})", scriptFile.getName());
return false;
}
} else {
continue;
}
} catch (Exception e) {
log.error("脚本加载时发生异常.(Loader: {}, Path: {})\n{}",
loader.getClass().getName(),
scriptFile.getAbsolutePath(),
Throwables.getStackTraceAsString(e));
throw new InvocationTargetException(e);
}
ScriptInfo scriptInfo = loader.getScriptInfo(script);
if (scriptInfo == null) {
log.warn("脚本加载成功, 但加载器没有返回脚本信息, 该脚本已放弃.");
return false;
}
scripts.put(scriptInfo, script);
return true;
}
return false;
}
/**
* 从指定位置加载所有脚本.
*/
public void loadScripts() {
log.info("正在加载脚本...(Path: {})", scriptsLocation.getAbsolutePath());
File[] files = scriptsLocation.listFiles(File::isFile);
if (files == null) {
log.warn("脚本目录无法访问, 请检查程序是否有权限访问脚本目录.(Path: {})", scriptsLocation.getAbsolutePath());
return;
}
int loadCount = 0;
for (File scriptFile : files) {
try {
if (loadScript(scriptFile)) {
loadCount ++;
}
} catch (InvocationTargetException ignored) {
}
}
log.info("脚本已全部加载完成, 共成功加载了 {} 个脚本.", loadCount);
}
/**
* 通过脚本信息返回脚本对象.
* @param scriptInfo 脚本信息.
* @return 返回脚本对象.
* @throws NoSuchElementException 当指定的 ScriptInfo 没有对应脚本对象时抛出该异常.
* @throws NullPointerException 当 scriptInfo 为 {@code null} 时抛出该异常.
*/
public Script getScriptByScriptInfo(ScriptInfo scriptInfo) {
Objects.requireNonNull(scriptInfo);
if (!scripts.containsKey(scriptInfo)) {
throw new NoSuchElementException(scriptInfo.toString());
}
return scripts.get(scriptInfo);
}
}

View File

@ -0,0 +1,47 @@
package net.lamgc.oracle.sentry.script.groovy;
import net.lamgc.oracle.sentry.ComputeInstanceManager;
import net.lamgc.oracle.sentry.script.*;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import net.lamgc.oracle.sentry.script.groovy.trigger.*;
import net.lamgc.oracle.sentry.script.tools.http.ScriptHttpClient;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Groovy DSL 脚本的父类.
* @author LamGC
*/
@SuppressWarnings({"unused", "FieldCanBeLocal"})
public class GroovyDslDelegate extends Script {
private final GroovyScriptInfo scriptInfo = new GroovyScriptInfo();
private final ScriptHttpClient HTTP;
private final ComputeInstanceManager InstanceManager;
public GroovyDslDelegate(ScriptHttpClient httpClient, ComputeInstanceManager instanceManager) {
HTTP = httpClient;
InstanceManager = instanceManager;
}
private void trigger(String triggerName, Closure<?> closure){
DefaultGroovyMethods.with(GroovyTriggerProvider.INSTANCE.getTriggerByName(triggerName), closure);
}
/**
* 脚本的基本信息.
* @param scriptInfoClosure 配置了脚本信息的闭包对象.
*/
private void info(@DelegatesTo(GroovyScriptInfo.class) Closure<GroovyScriptInfo> scriptInfoClosure) {
DefaultGroovyMethods.with(scriptInfo, scriptInfoClosure);
}
@Override
public ScriptInfo getScriptInfo() {
return scriptInfo;
}
}

View File

@ -0,0 +1,18 @@
package net.lamgc.oracle.sentry.script.groovy;
import net.lamgc.oracle.sentry.script.ScriptInfo;
public class GroovyScriptInfo extends ScriptInfo {
public void artifact(String artifact) {
super.setArtifact(artifact);
}
public void group(String group) {
super.setGroup(group);
}
public void version(String version) {
super.setVersion(version);
}
}

View File

@ -0,0 +1,69 @@
package net.lamgc.oracle.sentry.script.groovy;
import com.google.common.base.Throwables;
import groovy.lang.GroovyClassLoader;
import groovy.util.DelegatingScript;
import net.lamgc.oracle.sentry.script.Script;
import net.lamgc.oracle.sentry.script.ScriptComponent;
import net.lamgc.oracle.sentry.script.ScriptInfo;
import net.lamgc.oracle.sentry.script.ScriptLoader;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author LamGC
*/
@SuppressWarnings("MapOrSetKeyShouldOverrideHashCodeEquals")
public class GroovyScriptLoader implements ScriptLoader {
private final static Logger log = LoggerFactory.getLogger(GroovyScriptLoader.class);
private final GroovyClassLoader scriptClassLoader;
private final Map<Script, ScriptInfo> scriptInfoMap = new ConcurrentHashMap<>();
public GroovyScriptLoader() {
CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
compilerConfiguration.setScriptBaseClass(DelegatingScript.class.getName());
this.scriptClassLoader = new GroovyClassLoader(GroovyClassLoader.class.getClassLoader(), compilerConfiguration);
}
@Override
public boolean canLoad(File scriptFile) {
return scriptFile.getName().endsWith(".groovy");
}
@Override
public Script loadScript(ScriptComponent context, File scriptFile) throws IOException {
Class<?> scriptClass = scriptClassLoader.parseClass(scriptFile);
if (!DelegatingScript.class.isAssignableFrom(scriptClass)) {
return null;
}
try {
Constructor<? extends DelegatingScript> constructor =
scriptClass.asSubclass(DelegatingScript.class).getConstructor();
DelegatingScript newScriptObject = constructor.newInstance();
GroovyDslDelegate dslDelegate = new GroovyDslDelegate(context.HTTP(), context.InstanceManager());
newScriptObject.setDelegate(dslDelegate);
newScriptObject.run();
scriptInfoMap.put(dslDelegate, dslDelegate.getScriptInfo());
return dslDelegate;
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
log.error("加载脚本时发生异常.(ScriptPath: {})\n{}", scriptFile.getAbsolutePath(), Throwables.getStackTraceAsString(e));
}
return null;
}
@Override
public ScriptInfo getScriptInfo(Script script) {
return scriptInfoMap.get(script);
}
}

View File

@ -0,0 +1,50 @@
package net.lamgc.oracle.sentry.script.groovy;
import com.google.common.base.Strings;
import net.lamgc.oracle.sentry.script.groovy.trigger.GroovyTrigger;
import net.lamgc.oracle.sentry.script.groovy.trigger.TriggerName;
import org.springframework.scheduling.Trigger;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author LamGC
*/
public class GroovyTriggerProvider {
private final Map<String, ServiceLoader.Provider<GroovyTrigger>> triggerProviderMap = new ConcurrentHashMap<>();
public final static GroovyTriggerProvider INSTANCE = new GroovyTriggerProvider();
private GroovyTriggerProvider() {
ServiceLoader<GroovyTrigger> loader = ServiceLoader.load(GroovyTrigger.class);
loader.stream().iterator().forEachRemaining(triggerProvider -> {
Class<? extends GroovyTrigger> triggerClass = triggerProvider.type();
if (!triggerClass.isAnnotationPresent(TriggerName.class)) {
return;
}
TriggerName triggerName = triggerClass.getAnnotation(TriggerName.class);
if (!Strings.isNullOrEmpty(triggerName.value())) {
String name = triggerName.value().toLowerCase();
if (triggerProviderMap.containsKey(name)) {
return;
}
triggerProviderMap.put(name, triggerProvider);
}
});
}
public GroovyTrigger getTriggerByName(String triggerName) {
if (!triggerProviderMap.containsKey(triggerName.toLowerCase())) {
throw new NoSuchElementException("The specified trigger could not be found: " + triggerName);
}
return triggerProviderMap.get(triggerName).get();
}
}

View File

@ -0,0 +1,19 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
import groovy.lang.DelegatesTo;
/**
* @author LamGC
*/
public interface GroovyTrigger {
/**
* 启动触发器.
* <p> 注意, 触发器执行 run 方法不可以阻塞方法返回.
* @param task 触发器需要执行的任务.
*/
void run(Runnable task);
}

View File

@ -0,0 +1,24 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 只执行一次的触发器, 执行后将不再执行该任务.
* @author LamGC
*/
@TriggerName("once")
public class OnceTrigger implements GroovyTrigger {
private final static ExecutorService executor = Executors.newFixedThreadPool(4,
new ThreadFactoryBuilder()
.setNameFormat("GroovyOnceExec-%d")
.build());
@Override
public void run(Runnable task) {
executor.execute(task);
}
}

View File

@ -0,0 +1,55 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
import com.google.common.base.Throwables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
/**
* @author LamGC
*/
@TriggerName("timer")
public class TimerTrigger implements GroovyTrigger {
private final static Logger log = LoggerFactory.getLogger(TimerTrigger.class);
private CronTrigger trigger;
private final TaskScheduler scheduler = new ThreadPoolTaskScheduler();
/**
* 设定定时时间.
* <p> 只允许在第一次执行时设置.
* @param expression Cron 时间表达式.
*/
public void time(String expression) {
if (trigger == null) {
trigger = new CronTrigger(expression);
}
}
@Override
public void run(Runnable runnable) {
if (trigger == null) {
if (!log.isDebugEnabled()) {
log.warn("脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).");
} else {
log.warn("{} - 脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).", this);
log.warn("{} - 脚本尚未设置 Cron 时间表达式, 任务将不会执行.\n{}", this, Throwables.getStackTraceAsString(new Exception()));
}
return;
} else if (runnable == null) {
if (!log.isDebugEnabled()) {
log.warn("脚本尚未设置 Cron 时间表达式, 任务将不会执行(堆栈信息请检查调试级别日志).");
} else {
log.warn("{} - 脚本尚未设置任务动作, 任务将不会执行(堆栈信息请检查调试级别日志).", this);
log.warn("{} - 脚本尚未设置任务动作, 任务将不会执行.\n{}", this, Throwables.getStackTraceAsString(new Exception()));
}
return;
}
scheduler.schedule(runnable, trigger);
}
}

View File

@ -0,0 +1,18 @@
package net.lamgc.oracle.sentry.script.groovy.trigger;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 触发器名称.
* @author LamGC
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TriggerName {
String value();
}

View File

@ -0,0 +1,40 @@
package net.lamgc.oracle.sentry.script.tools.http;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
*
* @author LamGC
*/
public class HttpAccess {
private final HttpClient client;
private final String url;
HttpAccess(HttpClient client, String url) {
this.client = client;
this.url = url;
}
public HttpAccessResponse get() throws IOException {
HttpGet request = new HttpGet(url);
HttpResponse response = client.execute(request);
return new HttpAccessResponse(response);
}
public HttpAccessResponse post(String body) throws IOException {
HttpPost request = new HttpPost(url);
request.setEntity(new StringEntity(body, StandardCharsets.UTF_8));
HttpResponse response = client.execute(request);
return new HttpAccessResponse(response);
}
}

View File

@ -0,0 +1,51 @@
package net.lamgc.oracle.sentry.script.tools.http;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.util.EntityUtils;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Http 响应.
* @author LamGC
*/
public final class HttpAccessResponse {
private final StatusLine statusLine;
private final Locale locale;
private final Map<String, String> headers = new ConcurrentHashMap<>();
private final HttpEntity entity;
HttpAccessResponse(HttpResponse response) {
this.statusLine = response.getStatusLine();
this.locale = response.getLocale();
for (Header header : response.getAllHeaders()) {
headers.put(header.getName(), header.getValue());
}
this.entity = response.getEntity();
}
public StatusLine getStatusLine() {
return statusLine;
}
public Locale getLocale() {
return locale;
}
public String getContentToString() throws IOException {
return EntityUtils.toString(entity);
}
public HttpEntity getEntity() {
return entity;
}
}

View File

@ -0,0 +1,21 @@
package net.lamgc.oracle.sentry.script.tools.http;
import org.apache.http.client.HttpClient;
public class ScriptHttpClient {
private final HttpClient httpClient;
public ScriptHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* 打开一个连接.
* @param url 要访问的 Url.
*/
public HttpAccess create(String url) {
return new HttpAccess(httpClient, url);
}
}