Commit 02897568 authored by liang.tang's avatar liang.tang
Browse files

magic-api

parents
Pipeline #222 failed with stages
in 0 seconds
package org.ssssssss.magicapi.core.servlet;
public interface MagicHttpSession {
Object getAttribute(String key);
void setAttribute(String key, Object value);
<T> T getSession();
}
package org.ssssssss.magicapi.core.servlet;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.function.Function;
public interface MagicRequestContextHolder {
default <R> R convert(Function<ServletRequestAttributes, R> function){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) requestAttributes);
return function.apply(servletRequestAttributes);
}
return null;
}
MagicHttpServletRequest getRequest();
MagicHttpServletResponse getResponse();
}
package org.ssssssss.magicapi.core.web;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.backup.service.MagicBackupService;
import org.ssssssss.magicapi.core.annotation.Valid;
import org.ssssssss.magicapi.core.config.Constants;
import org.ssssssss.magicapi.core.config.JsonCodeConstants;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.core.context.MagicUser;
import org.ssssssss.magicapi.core.exception.InvalidArgumentException;
import org.ssssssss.magicapi.core.exception.MagicLoginException;
import org.ssssssss.magicapi.core.interceptor.Authorization;
import org.ssssssss.magicapi.core.model.Group;
import org.ssssssss.magicapi.core.model.JsonBean;
import org.ssssssss.magicapi.core.model.MagicEntity;
import org.ssssssss.magicapi.core.service.MagicAPIService;
import org.ssssssss.magicapi.core.service.MagicResourceService;
import org.ssssssss.magicapi.core.servlet.MagicHttpServletRequest;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Controller 基类
*
* @author mxd
*/
public class MagicController implements JsonCodeConstants {
final MagicAPIService magicAPIService;
final MagicBackupService magicBackupService;
protected MagicConfiguration configuration;
public MagicController(MagicConfiguration configuration) {
this.configuration = configuration;
this.magicAPIService = configuration.getMagicAPIService();
this.magicBackupService = configuration.getMagicBackupService();
}
public void doValid(MagicHttpServletRequest request, Valid valid) {
if (valid != null) {
if (!valid.readonly() && configuration.getWorkspace().readonly()) {
throw new InvalidArgumentException(IS_READ_ONLY);
}
if (valid.authorization() != Authorization.NONE && !allowVisit(request, valid.authorization())) {
throw new InvalidArgumentException(PERMISSION_INVALID);
}
}
}
/**
* 判断是否有权限访问按钮
*/
boolean allowVisit(MagicHttpServletRequest request, Authorization authorization) {
if (authorization == null) {
return true;
}
MagicUser magicUser = (MagicUser) request.getAttribute(Constants.ATTRIBUTE_MAGIC_USER);
return configuration.getAuthorizationInterceptor().allowVisit(magicUser, request, authorization);
}
boolean allowVisit(MagicHttpServletRequest request, Authorization authorization, MagicEntity entity) {
if (authorization == null) {
return true;
}
MagicUser magicUser = (MagicUser) request.getAttribute(Constants.ATTRIBUTE_MAGIC_USER);
return configuration.getAuthorizationInterceptor().allowVisit(magicUser, request, authorization, entity);
}
boolean allowVisit(MagicHttpServletRequest request, Authorization authorization, Group group) {
if (authorization == null) {
return true;
}
MagicUser magicUser = (MagicUser) request.getAttribute(Constants.ATTRIBUTE_MAGIC_USER);
return configuration.getAuthorizationInterceptor().allowVisit(magicUser, request, authorization, group);
}
List<MagicEntity> entities(MagicHttpServletRequest request, Authorization authorization) {
MagicResourceService service = configuration.getMagicResourceService();
return service.tree()
.values()
.stream()
.flatMap(it -> it.flat().stream())
.filter(it -> !Constants.ROOT_ID.equals(it.getId()))
.filter(it -> allowVisit(request, authorization, it))
.flatMap(it -> service.listFiles(it.getId()).stream())
.filter(it -> allowVisit(request, authorization, it))
.filter(it -> Objects.nonNull(it.getScript()))
.collect(Collectors.toList());
}
@ExceptionHandler(MagicLoginException.class)
@ResponseBody
public JsonBean<Void> invalidLogin(MagicLoginException exception) {
return new JsonBean<>(401, exception.getMessage());
}
}
package org.ssssssss.magicapi.core.web;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.utils.Mapping;
public interface MagicControllerRegister {
void register(Mapping mapping, MagicConfiguration configuration);
}
package org.ssssssss.magicapi.core.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.core.exception.InvalidArgumentException;
import org.ssssssss.magicapi.core.model.JsonBean;
/**
* magic-api接口异常处理器
*
* @author mxd
*/
public interface MagicExceptionHandler {
Logger logger = LoggerFactory.getLogger(MagicExceptionHandler.class);
/**
* magic-api中的接口异常处理
*
* @param e 异常对象
* @return 返回json对象
*/
@ExceptionHandler(Exception.class)
@ResponseBody
default Object exceptionHandler(Exception e) {
logger.error("magic-api调用接口出错", e);
return new JsonBean<>(-1, e.getMessage());
}
/**
* magic-api中的接口异常处理
*
* @param e 异常对象
* @return 返回json对象
*/
@ExceptionHandler(InvalidArgumentException.class)
@ResponseBody
default Object exceptionHandler(InvalidArgumentException e) {
return new JsonBean<>(e.getCode(), e.getMessage());
}
}
package org.ssssssss.magicapi.core.web;
import org.springframework.web.bind.annotation.*;
import org.ssssssss.magicapi.core.config.Constants;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.core.exception.InvalidArgumentException;
import org.ssssssss.magicapi.core.interceptor.Authorization;
import org.ssssssss.magicapi.core.model.*;
import org.ssssssss.magicapi.core.resource.FileResource;
import org.ssssssss.magicapi.core.resource.Resource;
import org.ssssssss.magicapi.core.service.MagicDynamicRegistry;
import org.ssssssss.magicapi.core.service.MagicResourceService;
import org.ssssssss.magicapi.core.servlet.MagicHttpServletRequest;
import org.ssssssss.magicapi.utils.IoUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
public class MagicResourceController extends MagicController implements MagicExceptionHandler {
private final MagicResourceService service;
public MagicResourceController(MagicConfiguration configuration) {
super(configuration);
this.service = MagicConfiguration.getMagicResourceService();
}
@PostMapping("/resource/folder/save")
@ResponseBody
public JsonBean<String> saveFolder(@RequestBody Group group, MagicHttpServletRequest request) {
isTrue(allowVisit(request, Authorization.SAVE, group), PERMISSION_INVALID);
Resource resource = service.getResource();
if(resource instanceof FileResource){
isTrue(resource.exists(), FILE_PATH_NOT_EXISTS);
}
if (service.saveGroup(group)) {
return new JsonBean<>(group.getId());
}
return new JsonBean<>((String) null);
}
@PostMapping("/resource/folder/copy")
@ResponseBody
public JsonBean<String> saveFolder(String src, String target, MagicHttpServletRequest request) {
Group srcGroup = service.getGroup(src);
notNull(srcGroup, GROUP_NOT_FOUND);
isTrue(allowVisit(request, Authorization.VIEW, srcGroup), PERMISSION_INVALID);
Group targetGroup = srcGroup.copy();
targetGroup.setId(null);
targetGroup.setParentId(target);
targetGroup.setType(srcGroup.getType());
isTrue(allowVisit(request, Authorization.SAVE, targetGroup), PERMISSION_INVALID);
return new JsonBean<>(service.copyGroup(src, target));
}
@PostMapping("/resource/delete")
@ResponseBody
public JsonBean<Boolean> delete(String id, MagicHttpServletRequest request) {
Group group = service.getGroup(id);
if(group == null){
MagicEntity entity = service.file(id);
notNull(entity, FILE_NOT_FOUND);
isTrue(allowVisit(request, Authorization.DELETE, entity), PERMISSION_INVALID);
} else {
isTrue(allowVisit(request, Authorization.DELETE, group), PERMISSION_INVALID);
}
return new JsonBean<>(service.delete(id));
}
@PostMapping("/resource/file/{folder}/save")
@ResponseBody
public JsonBean<String> saveFile(@PathVariable("folder") String folder, String auto, MagicHttpServletRequest request) throws IOException {
byte[] bytes = IoUtils.bytes(request.getInputStream());
MagicEntity entity = configuration.getMagicDynamicRegistries().stream()
.map(MagicDynamicRegistry::getMagicResourceStorage)
.filter(it -> Objects.equals(it.folder(), folder))
.findFirst()
.orElseThrow(() -> new InvalidArgumentException(GROUP_NOT_FOUND))
.read(bytes);
isTrue(allowVisit(request, Authorization.SAVE, entity), PERMISSION_INVALID);
// 自动保存的代码,和旧版代码对比,如果一致,则不保存,直接返回。
if(entity.getId() != null && "1".equals(auto)){
MagicEntity oldInfo = service.file(entity.getId());
if(oldInfo != null && Objects.equals(oldInfo, entity)){
return new JsonBean<>(entity.getId());
}
}
if (MagicConfiguration.getMagicResourceService().saveFile(entity)) {
return new JsonBean<>(entity.getId());
}
return new JsonBean<>(null);
}
@GetMapping("/resource/file/{id}")
@ResponseBody
public JsonBean<MagicEntity> detail(@PathVariable("id") String id, MagicHttpServletRequest request) {
MagicEntity entity = MagicConfiguration.getMagicResourceService().file(id);
isTrue(allowVisit(request, Authorization.VIEW, entity), PERMISSION_INVALID);
return new JsonBean<>(MagicConfiguration.getMagicResourceService().file(id));
}
@PostMapping("/resource/move")
@ResponseBody
public JsonBean<Boolean> move(String src, String groupId, MagicHttpServletRequest request) {
Group group = service.getGroup(src);
if(group == null){
MagicEntity entity = service.file(src);
notNull(entity, FILE_NOT_FOUND);
entity = entity.copy();
entity.setGroupId(groupId);
isTrue(allowVisit(request, Authorization.SAVE, entity), PERMISSION_INVALID);
} else {
group = group.copy();
group.setParentId(groupId);
isTrue(allowVisit(request, Authorization.DELETE, group), PERMISSION_INVALID);
}
return new JsonBean<>(service.move(src, groupId));
}
@PostMapping("/resource/lock")
@ResponseBody
public JsonBean<Boolean> lock(String id, MagicHttpServletRequest request) {
MagicEntity entity = service.file(id);
notNull(entity, FILE_NOT_FOUND);
isTrue(allowVisit(request, Authorization.LOCK, entity), PERMISSION_INVALID);
return new JsonBean<>(service.lock(id));
}
@PostMapping("/resource/unlock")
@ResponseBody
public JsonBean<Boolean> unlock(String id, MagicHttpServletRequest request) {
MagicEntity entity = service.file(id);
notNull(entity, FILE_NOT_FOUND);
isTrue(allowVisit(request, Authorization.UNLOCK, entity), PERMISSION_INVALID);
return new JsonBean<>(service.unlock(id));
}
@GetMapping("/resource")
@ResponseBody
public JsonBean<Map<String, TreeNode<Attributes<Object>>>> resources(MagicHttpServletRequest request) {
Map<String, TreeNode<Group>> tree = service.tree();
Map<String, TreeNode<Attributes<Object>>> result = new HashMap<>();
tree.forEach((key, value) -> {
TreeNode<Attributes<Object>> node = process(value, request);
List<TreeNode<Attributes<Object>>> groups = node.getChildren();
if(groups.size() > 0){
List<TreeNode<Attributes<Object>>> nodes = groups.get(0).getChildren();
configuration.getMagicDynamicRegistries().stream()
.filter(it -> it.getMagicResourceStorage().folder().equals(key))
.findFirst()
.map(MagicDynamicRegistry::defaultMappings)
.ifPresent(mappings -> {
for (MagicEntity mapping : mappings) {
nodes.add(new TreeNode<>(mapping));
}
});
}
result.put(key, node);
});
return new JsonBean<>(result);
}
private TreeNode<Attributes<Object>> process(TreeNode<Group> groupNode, MagicHttpServletRequest request) {
TreeNode<Attributes<Object>> value = new TreeNode<>();
value.setNode(groupNode.getNode());
groupNode.getChildren().stream()
.filter(it -> allowVisit(request, Authorization.VIEW, it.getNode()))
.map(it -> process(it, request))
.forEach(value::addChild);
if (!Constants.ROOT_ID.equals(groupNode.getNode().getId())) {
service
.listFiles(groupNode.getNode().getId())
.stream()
.filter(it -> allowVisit(request, Authorization.VIEW, it))
.map(MagicEntity::simple)
.map((Function<MagicEntity, TreeNode<Attributes<Object>>>) TreeNode::new)
.forEach(value::addChild);
}
return value;
}
}
package org.ssssssss.magicapi.core.web;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.ssssssss.magicapi.core.annotation.Valid;
import org.ssssssss.magicapi.core.config.Constants;
import org.ssssssss.magicapi.core.config.MagicAPIProperties;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.core.context.MagicUser;
import org.ssssssss.magicapi.core.exception.MagicLoginException;
import org.ssssssss.magicapi.core.interceptor.Authorization;
import org.ssssssss.magicapi.core.model.*;
import org.ssssssss.magicapi.core.service.MagicAPIService;
import org.ssssssss.magicapi.core.servlet.MagicHttpServletRequest;
import org.ssssssss.magicapi.core.servlet.MagicHttpServletResponse;
import org.ssssssss.magicapi.modules.db.SQLModule;
import org.ssssssss.magicapi.utils.ClassScanner;
import org.ssssssss.magicapi.utils.IoUtils;
import org.ssssssss.magicapi.utils.SignUtils;
import org.ssssssss.magicapi.utils.WebUtils;
import org.ssssssss.script.MagicResourceLoader;
import org.ssssssss.script.MagicScriptEngine;
import org.ssssssss.script.ScriptClass;
import org.ssssssss.script.parsing.Span;
import org.ssssssss.script.parsing.Tokenizer;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MagicWorkbenchController extends MagicController implements MagicExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(MagicWorkbenchController.class);
private static final Pattern SINGLE_LINE_COMMENT_TODO = Pattern.compile("((TODO)|(todo)|(fixme)|(FIXME))[ \t]+[^\n]+");
private static final Pattern MULTI_LINE_COMMENT_TODO = Pattern.compile("((TODO)|(todo)|(fixme)|(FIXME))[ \t]+[^\n(?!*/)]+");
private final String secretKey;
private final List<Plugin> plugins;
private final MagicAPIProperties properties;
private String allClassTxt;
public MagicWorkbenchController(MagicConfiguration configuration, MagicAPIProperties properties, List<Plugin> plugins) {
super(configuration);
this.properties = properties;
this.plugins = plugins;
this.secretKey = properties.getSecretKey();
// 给前端添加代码提示
MagicScriptEngine.addScriptClass(SQLModule.class);
MagicScriptEngine.addScriptClass(MagicAPIService.class);
}
@GetMapping({"", "/"})
@Valid(requireLogin = false)
public String redirectIndex(MagicHttpServletRequest request) {
if (request.getRequestURI().endsWith("/")) {
return "redirect:./index.html";
}
return "redirect:" + properties.getWeb() + "/index.html";
}
@GetMapping("/config.json")
@Valid(requireLogin = false)
@ResponseBody
public MagicAPIProperties readConfig() {
return properties;
}
@GetMapping(value = "/classes.txt", produces = "text/plain")
@ResponseBody
@Valid(requireLogin = false)
private String readClass() {
if (allClassTxt == null) {
try {
allClassTxt = ClassScanner.compress(ClassScanner.scan());
} catch (Throwable t) {
logger.warn("扫描Class失败", t);
allClassTxt = "";
}
}
return allClassTxt;
}
/**
* 获取所有class
*/
@PostMapping("/classes")
@ResponseBody
@Valid(requireLogin = false)
public JsonBean<Map<String, Object>> classes() {
Map<String, ScriptClass> classMap = MagicScriptEngine.getScriptClassMap();
classMap.putAll(MagicResourceLoader.getModules());
Map<String, Object> values = new HashMap<>();
values.put("classes", classMap);
values.put("extensions", MagicScriptEngine.getExtensionScriptClass());
values.put("functions", MagicScriptEngine.getFunctions());
return new JsonBean<>(values);
}
/**
* 获取单个class
*
* @param className 类名
*/
@PostMapping("/class")
@ResponseBody
public JsonBean<Set<ScriptClass>> clazz(String className) {
if (StringUtils.isBlank(className)) {
return new JsonBean<>(Collections.emptySet());
}
return new JsonBean<>(MagicScriptEngine.getScriptClass(className));
}
/**
* 登录
*/
@PostMapping("/login")
@ResponseBody
@Valid(requireLogin = false)
public JsonBean<Boolean> login(String username, String password, MagicHttpServletRequest request, MagicHttpServletResponse response) throws MagicLoginException {
if (configuration.getAuthorizationInterceptor().requireLogin()) {
if (StringUtils.isBlank(username) && StringUtils.isBlank(password)) {
try {
configuration.getAuthorizationInterceptor().getUserByToken(request.getHeader(Constants.MAGIC_TOKEN_HEADER));
} catch (MagicLoginException ignored) {
return new JsonBean<>(false);
}
} else {
MagicUser user = configuration.getAuthorizationInterceptor().login(username, password);
response.setHeader(Constants.MAGIC_TOKEN_HEADER, user.getToken());
response.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, Constants.MAGIC_TOKEN_HEADER);
}
}
return new JsonBean<>(true);
}
@PostMapping("/user")
@ResponseBody
public JsonBean<MagicUser> user(MagicHttpServletRequest request) {
if (configuration.getAuthorizationInterceptor().requireLogin()) {
try {
return new JsonBean<>(configuration.getAuthorizationInterceptor().getUserByToken(request.getHeader(Constants.MAGIC_TOKEN_HEADER)));
} catch (MagicLoginException ignored) {
}
}
return new JsonBean<>(MagicUser.guest());
}
@PostMapping("/logout")
@ResponseBody
@Valid(requireLogin = false)
public JsonBean<Void> logout(MagicHttpServletRequest request) {
configuration.getAuthorizationInterceptor().logout(request.getHeader(Constants.MAGIC_TOKEN_HEADER));
return new JsonBean<>();
}
@GetMapping("/plugins")
@Valid(requireLogin = false)
@ResponseBody
public JsonBean<List<Plugin>> plugins() {
return new JsonBean<>(plugins);
}
@RequestMapping("/options")
@ResponseBody
@Valid(requireLogin = false)
public JsonBean<List<List<String>>> options() {
return new JsonBean<>(Stream.of(Options.values()).map(item -> Arrays.asList(item.getValue(), item.getName(), item.getDefaultValue())).collect(Collectors.toList()));
}
@GetMapping("/reload")
@ResponseBody
public JsonBean<Boolean> reload(MagicHttpServletRequest request) {
isTrue(allowVisit(request, Authorization.RELOAD), PERMISSION_INVALID);
MagicConfiguration.getMagicResourceService().refresh();
return new JsonBean<>(true);
}
@GetMapping("/search")
@ResponseBody
public JsonBean<List<Map<String, Object>>> search(String keyword, MagicHttpServletRequest request) {
if (StringUtils.isBlank(keyword)) {
return new JsonBean<>(Collections.emptyList());
}
return new JsonBean<>(entities(request, Authorization.VIEW)
.stream()
.filter(it -> it.getScript().contains(keyword))
.map(it -> {
String script = it.getScript();
int index = script.indexOf(keyword);
int endIndex = script.indexOf("\n", index + keyword.length());
index = script.lastIndexOf("\n", index) + 1;
Span span = new Span(script, index, endIndex == -1 ? script.length() : endIndex);
return new HashMap<String, Object>() {
{
put("id", it.getId());
put("text", span.getText().trim());
put("line", span.getLine().getLineNumber());
}
};
}).collect(Collectors.toList()));
}
@GetMapping("/todo")
@ResponseBody
@Valid
public JsonBean<List<Map<String, Object>>> todo(MagicHttpServletRequest request) {
List<MagicEntity> entities = entities(request, Authorization.VIEW);
List<Map<String, Object>> result = new ArrayList<>(entities.size());
for (MagicEntity entity : entities) {
try {
List<Span> comments = Tokenizer.tokenize(entity.getScript(), true).comments();
for (Span comment : comments) {
String text = comment.getText();
Pattern pattern = text.startsWith("//") ? SINGLE_LINE_COMMENT_TODO : MULTI_LINE_COMMENT_TODO;
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
result.add(new HashMap<String, Object>() {
{
put("id", entity.getId());
put("text", matcher.group(0).trim());
put("line", comment.getLine().getLineNumber());
}
});
}
}
} catch (Exception ignored) {
}
}
return new JsonBean<>(result);
}
@RequestMapping(value = "/config-js")
@Valid(requireLogin = false)
public void configJs(MagicHttpServletResponse response) throws IOException {
response.setContentType("application/javascript");
response.setCharacterEncoding("UTF-8");
byte[] bytes = "var MAGIC_EDITOR_CONFIG = {}".getBytes();
if (configuration.getEditorConfig() != null) {
try {
String path = configuration.getEditorConfig();
if (path.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
path = path.substring(ResourceUtils.CLASSPATH_URL_PREFIX.length());
bytes = IoUtils.bytes(new ClassPathResource(path).getInputStream());
}
File file = ResourceUtils.getFile(configuration.getEditorConfig());
bytes = Files.readAllBytes(Paths.get(file.toURI()));
} catch (IOException e) {
logger.warn("读取编辑器配置文件{}失败", configuration.getEditorConfig());
}
}
try (OutputStream stream = response.getOutputStream()) {
stream.write(bytes);
stream.flush();
}
}
@RequestMapping("/download")
@Valid(authorization = Authorization.DOWNLOAD)
public void download(String groupId, @RequestBody(required = false) List<SelectedResource> resources, MagicHttpServletRequest request, MagicHttpServletResponse response) throws IOException {
isTrue(allowVisit(request, Authorization.DOWNLOAD), PERMISSION_INVALID);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
ByteArrayOutputStream os = new ByteArrayOutputStream();
magicAPIService.download(groupId, resources, os);
String filename = "magic-api-all.zip";
if (StringUtils.isBlank(groupId)) {
filename = "magic-api-group.zip";
}
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(filename, "UTF-8"));
try (OutputStream stream = response.getOutputStream()) {
stream.write(os.toByteArray());
stream.flush();
}
}
@RequestMapping("/upload")
@Valid(readonly = false, authorization = Authorization.UPLOAD)
@ResponseBody
public JsonBean<Boolean> upload(MultipartFile file, String mode, MagicHttpServletRequest request) throws IOException {
notNull(file, FILE_IS_REQUIRED);
isTrue(allowVisit(request, Authorization.UPLOAD), PERMISSION_INVALID);
if (configuration.getMagicBackupService() != null) {
configuration.getMagicBackupService().doBackupAll("上传前,系统自动全量备份", WebUtils.currentUserName());
}
return new JsonBean<>(magicAPIService.upload(file.getInputStream(), mode));
}
@RequestMapping("/push")
@ResponseBody
@Valid(authorization = Authorization.PUSH)
public JsonBean<?> push(@RequestHeader("magic-push-target") String target, @RequestHeader("magic-push-secret-key") String secretKey,
@RequestHeader("magic-push-mode") String mode, @RequestBody List<SelectedResource> resources,
MagicHttpServletRequest request) {
isTrue(allowVisit(request, Authorization.PUSH), PERMISSION_INVALID);
return magicAPIService.push(target, secretKey, mode, resources);
}
@ResponseBody
@Valid(requireLogin = false)
public JsonBean<Void> receivePush(MultipartFile file, String mode, Long timestamp, String sign) throws IOException {
notNull(timestamp, SIGN_IS_INVALID);
notBlank(mode, SIGN_IS_INVALID);
notBlank(sign, SIGN_IS_INVALID);
notNull(file, SIGN_IS_INVALID);
byte[] bytes = IoUtils.bytes(file.getInputStream());
isTrue(sign.equals(SignUtils.sign(timestamp, secretKey, mode, bytes)), SIGN_IS_INVALID);
if (configuration.getMagicBackupService() != null) {
configuration.getMagicBackupService().doBackupAll("推送前,系统自动全量备份", WebUtils.currentUserName());
}
magicAPIService.upload(new ByteArrayInputStream(bytes), mode);
return new JsonBean<>();
}
}
package org.ssssssss.magicapi.core.web;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.core.annotation.Valid;
import org.ssssssss.magicapi.core.config.Constants;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.core.config.WebSocketSessionManager;
import org.ssssssss.magicapi.core.context.CookieContext;
import org.ssssssss.magicapi.core.context.RequestContext;
import org.ssssssss.magicapi.core.context.RequestEntity;
import org.ssssssss.magicapi.core.context.SessionContext;
import org.ssssssss.magicapi.core.exception.ValidateException;
import org.ssssssss.magicapi.core.interceptor.RequestInterceptor;
import org.ssssssss.magicapi.core.interceptor.ResultProvider;
import org.ssssssss.magicapi.core.logging.MagicLoggerContext;
import org.ssssssss.magicapi.core.model.*;
import org.ssssssss.magicapi.core.service.impl.RequestMagicDynamicRegistry;
import org.ssssssss.magicapi.core.servlet.MagicHttpServletRequest;
import org.ssssssss.magicapi.core.servlet.MagicHttpServletResponse;
import org.ssssssss.magicapi.modules.servlet.ResponseModule;
import org.ssssssss.magicapi.utils.PatternUtils;
import org.ssssssss.magicapi.utils.ScriptManager;
import org.ssssssss.script.MagicScriptContext;
import org.ssssssss.script.MagicScriptDebugContext;
import org.ssssssss.script.exception.MagicScriptAssertException;
import org.ssssssss.script.exception.MagicScriptException;
import org.ssssssss.script.functions.ObjectConvertExtension;
import org.ssssssss.script.parsing.Span;
import org.ssssssss.script.parsing.ast.literal.BooleanLiteral;
import org.ssssssss.script.reflection.JavaInvoker;
import java.io.IOException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import static org.springframework.http.HttpHeaders.*;
import static org.ssssssss.magicapi.core.config.Constants.*;
import static org.ssssssss.magicapi.core.config.MessageType.EXCEPTION;
/**
* 请求入口处理
*
* @author mxd
*/
public class RequestHandler extends MagicController {
private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class);
private static final Map<String, Object> EMPTY_MAP = new HashMap<>();
private static final List<String> DEFAULT_ALLOW_READ_RESPONSE_HEADERS = Arrays.asList(
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_METHODS,
CONTENT_TYPE, DATE, SERVER, SET_COOKIE, CONNECTION, CONTENT_LENGTH, CONTENT_ENCODING, TRANSFER_ENCODING, VARY);
private final ResultProvider resultProvider;
private final RequestMagicDynamicRegistry requestMagicDynamicRegistry;
public RequestHandler(MagicConfiguration configuration, RequestMagicDynamicRegistry requestMagicDynamicRegistry) {
super(configuration);
this.requestMagicDynamicRegistry = requestMagicDynamicRegistry;
this.resultProvider = configuration.getResultProvider();
}
/**
* 测试入口、实际请求入口
*
* @param request HttpServletRequest
* @param response HttpServletResponse
* @param pathVariables 路径变量
* @param parameters 表单参数&URL参数
* @return 返回请求结果
* @throws Throwable 处理失败抛出的异常
*/
@ResponseBody
@Valid(requireLogin = false)
public Object invoke(MagicHttpServletRequest request, MagicHttpServletResponse response,
@PathVariable(required = false) Map<String, Object> pathVariables,
@RequestHeader(required = false) Map<String, Object> defaultHeaders,
@RequestParam(required = false) Map<String, Object> parameters) throws Throwable {
String clientId = null;
Map<String, Object> headers = new LinkedCaseInsensitiveMap<>();
headers.putAll(defaultHeaders);
boolean requestedFromTest = configuration.isEnableWeb() && (clientId = request.getHeader(HEADER_REQUEST_CLIENT_ID)) != null && request.getHeader(HEADER_REQUEST_SCRIPT_ID) != null;
RequestEntity requestEntity = RequestEntity.create()
.info(requestMagicDynamicRegistry.getApiInfoFromRequest(request))
.request(request)
.response(response)
.requestedFromTest(requestedFromTest)
// 兼容 spring boot 3.0
.pathVariables(new HashMap<>(pathVariables))
.parameters(parameters);
ApiInfo info = requestEntity.getApiInfo();
if (info == null) {
logger.error("{}找不到对应接口", request.getRequestURI());
return afterCompletion(requestEntity, buildResult(requestEntity, API_NOT_FOUND, "接口不存在"));
}
requestEntity.setHeaders(headers);
List<Path> paths = new ArrayList<>(info.getPaths());
MagicConfiguration.getMagicResourceService().getGroupsByFileId(info.getId())
.stream()
.flatMap(it -> it.getPaths().stream())
.filter(it -> !paths.contains(it))
.forEach(paths::add);
Object bodyValue = readRequestBody(requestEntity.getRequest());
requestEntity.setRequestBody(bodyValue);
String scriptName = MagicConfiguration.getMagicResourceService().getScriptName(info);
MagicScriptContext context = createMagicScriptContext(scriptName, requestEntity);
requestEntity.setMagicScriptContext(context);
try {
boolean disabledUnknownParameter = CONST_STRING_TRUE.equalsIgnoreCase(info.getOptionValue(Options.DISABLED_UNKNOWN_PARAMETER));
// 验证参数
doValidate(scriptName, "参数", info.getParameters(), parameters, PARAMETER_INVALID, disabledUnknownParameter);
Object wrap = requestEntity.getApiInfo().getOptionValue(Options.WRAP_REQUEST_PARAMETERS.getValue());
if (wrap != null && StringUtils.isNotBlank(wrap.toString())) {
context.set(wrap.toString(), requestEntity.getParameters());
}
String defaultDataSourceValue = requestEntity.getApiInfo().getOptionValue(Options.DEFAULT_DATA_SOURCE.getValue());
if (defaultDataSourceValue != null) {
context.set(Options.DEFAULT_DATA_SOURCE.getValue(), defaultDataSourceValue);
}
context.putMapIntoContext(requestEntity.getParameters());
// 验证 path
doValidate(scriptName, "path", paths, requestEntity.getPathVariables(), PATH_VARIABLE_INVALID, disabledUnknownParameter);
context.putMapIntoContext(requestEntity.getPathVariables());
// 设置 cookie 变量
context.set(VAR_NAME_COOKIE, new CookieContext(requestEntity.getRequest()));
// 验证 header
doValidate(scriptName, "header", info.getHeaders(), headers, HEADER_INVALID, disabledUnknownParameter);
// 设置 header 变量
context.set(VAR_NAME_HEADER, headers);
// 设置 session 变量
context.set(VAR_NAME_SESSION, new SessionContext(requestEntity.getRequest().getSession()));
// 设置 path 变量
context.set(VAR_NAME_PATH_VARIABLE, requestEntity.getPathVariables());
// 设置 body 变量
if (bodyValue != null) {
context.set(VAR_NAME_REQUEST_BODY, bodyValue);
}
BaseDefinition requestBody = info.getRequestBodyDefinition();
if (requestBody != null && !CONST_STRING_TRUE.equalsIgnoreCase(info.getOptionValue(Options.DISABLED_VALIDATE_REQUEST_BODY)) && !CollectionUtils.isEmpty(requestBody.getChildren())) {
requestBody.setName(StringUtils.defaultIfBlank(requestBody.getName(), "root"));
doValidate(scriptName, VAR_NAME_REQUEST_BODY, Collections.singletonList(requestBody), new HashMap<String, Object>() {{
put(requestBody.getName(), bodyValue);
}}, BODY_INVALID, disabledUnknownParameter);
}
} catch (ValidateException e) {
return afterCompletion(requestEntity, resultProvider.buildResult(requestEntity, RESPONSE_CODE_INVALID, e.getMessage()));
} catch (Throwable root) {
return processException(requestEntity, root);
}
RequestContext.setRequestEntity(requestEntity);
Object value;
// 执行前置拦截器
if ((value = doPreHandle(requestEntity)) != null) {
return afterCompletion(requestEntity, value);
}
if (requestedFromTest) {
DebugRequest debugRequest = requestEntity.getDebugRequest();
String sessionAndScriptId = debugRequest.getRequestedClientId() + debugRequest.getRequestedScriptId();
try {
if (context instanceof MagicScriptDebugContext) {
WebSocketSessionManager.addMagicScriptContext(sessionAndScriptId, (MagicScriptDebugContext) context);
}
MagicLoggerContext.SESSION.set(clientId);
return invokeRequest(requestEntity);
} finally {
MagicLoggerContext.remove();
WebSocketSessionManager.removeMagicScriptContext(sessionAndScriptId);
}
} else {
return invokeRequest(requestEntity);
}
}
private Object buildResult(RequestEntity requestEntity, JsonCode code, Object data) {
return resultProvider.buildResult(requestEntity, code.getCode(), code.getMessage(), data);
}
private void removeUnknownKey(Map<String, Object> src, List<? extends BaseDefinition> definitions) {
if (!src.isEmpty()) {
Map<String, Object> newMap = new HashMap<>(definitions.size());
for (BaseDefinition definition : definitions) {
newMap.put(definition.getName(), src.get(definition.getName()));
}
src.clear();
src.putAll(newMap);
}
}
private boolean doValidateBody(String comment, BaseDefinition parameter, Map<String, Object> parameters, JsonCode jsonCode, Class<?> target) {
if (!parameter.isRequired() && parameters.isEmpty()) {
return true;
}
if (parameter.isRequired() && !BooleanLiteral.isTrue(parameters.get(parameter.getName()))) {
throw new ValidateException(jsonCode, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]为必填项", comment, parameter.getName())));
}
Object value = parameters.get(parameter.getName());
if (value != null && !target.isAssignableFrom(value.getClass())) {
throw new ValidateException(jsonCode, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]数据类型错误", comment, parameter.getName())));
}
return false;
}
private Map<String, Object> doValidate(String scriptName, String comment, List<? extends BaseDefinition> validateParameters, Map<String, Object> parameters, JsonCode jsonCode, boolean disabledUnknownParameter) {
parameters = parameters != null ? parameters : EMPTY_MAP;
if (CollectionUtils.isEmpty(validateParameters)) {
return parameters;
}
if (disabledUnknownParameter) {
removeUnknownKey(parameters, validateParameters);
}
for (BaseDefinition parameter : validateParameters) {
if (parameter.getDataType() == DataType.Any) {
continue;
}
// 针对requestBody多层级的情况
if (DataType.Object == parameter.getDataType()) {
if (doValidateBody(comment, parameter, parameters, jsonCode, Map.class)) {
continue;
}
doValidate(scriptName, VAR_NAME_REQUEST_BODY, parameter.getChildren(), (Map) parameters.get(parameter.getName()), jsonCode, disabledUnknownParameter);
} else if (DataType.Array == parameter.getDataType()) {
if (doValidateBody(comment, parameter, parameters, jsonCode, List.class)) {
continue;
}
List<Object> list = (List) parameters.get(parameter.getName());
if (list != null) {
List<Map<String, Object>> newList = list.stream().map(it -> doValidate(scriptName, VAR_NAME_REQUEST_BODY, parameter.getChildren(), new HashMap<String, Object>() {{
put(Constants.EMPTY, it);
}}, jsonCode, disabledUnknownParameter)).collect(Collectors.toList());
for (int i = 0, size = newList.size(); i < size; i++) {
list.set(i, newList.get(i).get(Constants.EMPTY));
}
}
} else if (StringUtils.isNotBlank(parameter.getName()) || parameters.containsKey(parameter.getName())) {
boolean isFile = parameter.getDataType() == DataType.MultipartFile || parameter.getDataType() == DataType.MultipartFiles;
String requestValue = StringUtils.defaultIfBlank(Objects.toString(parameters.get(parameter.getName()), Constants.EMPTY), Objects.toString(parameter.getDefaultValue(), Constants.EMPTY));
if (StringUtils.isBlank(requestValue) && !isFile) {
if (!parameter.isRequired()) {
continue;
}
throw new ValidateException(jsonCode, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]为必填项", comment, parameter.getName())));
}
try {
Object value = convertValue(parameter.getDataType(), parameter.getName(), requestValue);
if (isFile && parameter.isRequired()) {
if (value == null || (parameter.getDataType() == DataType.MultipartFiles && ((List<?>) value).isEmpty())) {
throw new ValidateException(jsonCode, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]为必填项", comment, parameter.getName())));
}
}
// 正则验证
if (VALIDATE_TYPE_PATTERN.equals(parameter.getValidateType())) {
String expression = parameter.getExpression();
if (StringUtils.isNotBlank(expression) && !PatternUtils.match(Objects.toString(value, Constants.EMPTY), expression)) {
throw new ValidateException(jsonCode, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]不满足正则表达式", comment, parameter.getName())));
}
}
parameters.put(parameter.getName(), value);
} catch (ValidateException ve) {
throw ve;
} catch (Exception e) {
throw new ValidateException(jsonCode, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]不合法", comment, parameter.getName())));
}
}
}
// 取出表达式验证的参数
List<BaseDefinition> validates = validateParameters.stream().filter(it -> VALIDATE_TYPE_EXPRESSION.equals(it.getValidateType()) && StringUtils.isNotBlank(it.getExpression())).collect(Collectors.toList());
for (BaseDefinition parameter : validates) {
MagicScriptContext context = new MagicScriptContext();
// 将其他参数也放置脚本中,以实现“依赖”的情况
context.putMapIntoContext(parameters);
Object value = parameters.get(parameter.getName());
if (value != null) {
context.setScriptName(scriptName);
// 设置自身变量
context.set(EXPRESSION_DEFAULT_VAR_NAME, value);
if (!BooleanLiteral.isTrue(ScriptManager.executeExpression(parameter.getExpression(), context))) {
throw new ValidateException(jsonCode, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]不满足表达式", comment, parameter.getName())));
}
}
}
return parameters;
}
/**
* 转换参数类型
*/
private Object convertValue(DataType dataType, String name, String value) {
if (dataType == null) {
return value;
}
try {
if (dataType.isNumber()) {
BigDecimal decimal = ObjectConvertExtension.asDecimal(value, null);
if (decimal == null) {
throw new IllegalArgumentException();
}
return dataType.getInvoker().invoke0(decimal, null, EMPTY_OBJECT_ARRAY);
} else {
JavaInvoker<Method> invoker = dataType.getInvoker();
if (invoker != null) {
List<Object> params = new ArrayList<>();
if (dataType.isNeedName()) {
params.add(name);
}
if (dataType.isNeedValue()) {
params.add(value);
}
return invoker.invoke0(null, null, params.toArray());
}
}
return value;
} catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
}
private Object invokeRequest(RequestEntity requestEntity) throws Throwable {
try {
MagicScriptContext context = requestEntity.getMagicScriptContext();
Object result = ScriptManager.executeScript(requestEntity.getApiInfo().getScript(), context);
Object value = result;
// 执行后置拦截器
if ((value = doPostHandle(requestEntity, value)) != null) {
return afterCompletion(requestEntity, value);
}
// 对返回结果包装处理
return afterCompletion(requestEntity, response(requestEntity, result));
} catch (Throwable root) {
return processException(requestEntity, root);
} finally {
RequestContext.remove();
}
}
private Object processException(RequestEntity requestEntity, Throwable root) throws Throwable {
MagicScriptException se = null;
Throwable parent = root;
do {
if (parent instanceof MagicScriptAssertException) {
MagicScriptAssertException sae = (MagicScriptAssertException) parent;
return afterCompletion(requestEntity, resultProvider.buildResult(requestEntity, sae.getCode(), sae.getMessage()), root);
}
if (parent instanceof MagicScriptException) {
se = (MagicScriptException) parent;
}
} while ((parent = parent.getCause()) != null);
if (se != null && requestEntity.isRequestedFromTest()) {
Span.Line line = se.getLine();
WebSocketSessionManager.sendByClientId(requestEntity.getDebugRequest().getRequestedClientId(), EXCEPTION, Arrays.asList(
requestEntity.getDebugRequest().getRequestedScriptId(),
se.getSimpleMessage(),
line == null ? null : Arrays.asList(line.getLineNumber(), line.getEndLineNumber(), line.getStartCol(), line.getEndCol())
));
}
if (configuration.isThrowException()) {
afterCompletion(requestEntity, null, root);
throw root;
}
logger.error("接口{}请求出错", requestEntity.getRequest().getRequestURI(), root);
return afterCompletion(requestEntity, resultProvider.buildException(requestEntity, root), root);
}
/**
* 读取RequestBody
*/
private Object readRequestBody(MagicHttpServletRequest request) throws IOException {
if (configuration.getHttpMessageConverters() != null && request.getContentType() != null) {
MediaType mediaType = MediaType.valueOf(request.getContentType());
Class clazz = Object.class;
try {
for (HttpMessageConverter<?> converter : configuration.getHttpMessageConverters()) {
if (converter.canRead(clazz, mediaType)) {
return converter.read(clazz, request.getHttpInputMessage());
}
}
} catch (HttpMessageNotReadableException ignored) {
return null;
}
}
return null;
}
/**
* 构建 MagicScriptContext
*/
private MagicScriptContext createMagicScriptContext(String scriptName, RequestEntity requestEntity) {
DebugRequest debugRequest = requestEntity.getDebugRequest();
List<Integer> breakpoints = debugRequest.getRequestedBreakpoints();
// 构建脚本上下文
MagicScriptContext context;
// TODO 安全校验
if (requestEntity.isRequestedFromDebug() && breakpoints.size() > 0) {
context = debugRequest.createMagicScriptContext(configuration.getDebugTimeout());
} else {
context = new MagicScriptContext();
}
context.setScriptName(scriptName);
return context;
}
/**
* 包装返回结果
*/
private Object response(RequestEntity requestEntity, Object value) {
if (value instanceof ResponseEntity) {
return value;
} else if (value instanceof ResponseModule.NullValue) {
return null;
}
return resultProvider.buildResult(requestEntity, value);
}
/**
* 执行后置拦截器
*/
private Object doPostHandle(RequestEntity requestEntity, Object value) throws Exception {
for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
Object target = requestInterceptor.postHandle(requestEntity, value);
if (target != null) {
return afterCompletion(requestEntity, target);
}
}
return null;
}
private Object afterCompletion(RequestEntity requestEntity, Object returnValue) {
return afterCompletion(requestEntity, returnValue, null);
}
private Object afterCompletion(RequestEntity requestEntity, Object returnValue, Throwable throwable) {
for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
try {
requestInterceptor.afterCompletion(requestEntity, returnValue, throwable);
} catch (Exception e) {
logger.warn("执行afterCompletion出现出错", e);
}
}
Set<String> exposeHeaders = new HashSet<>(16);
if (returnValue instanceof ResponseEntity) {
exposeHeaders.addAll(((ResponseEntity<?>) returnValue).getHeaders().keySet());
}
if (requestEntity.isRequestedFromTest()) {
MagicHttpServletResponse response = requestEntity.getResponse();
exposeHeaders.addAll(response.getHeaderNames());
exposeHeaders.addAll(DEFAULT_ALLOW_READ_RESPONSE_HEADERS);
}
if (!exposeHeaders.isEmpty()) {
requestEntity.getResponse().setHeader(ACCESS_CONTROL_EXPOSE_HEADERS, String.join(",", exposeHeaders));
}
return returnValue;
}
/**
* 执行前置拦截器
*/
private Object doPreHandle(RequestEntity requestEntity) throws Exception {
for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
Object value = requestInterceptor.preHandle(requestEntity);
if (value != null) {
return value;
}
}
return null;
}
}
package org.ssssssss.magicapi.datasource.model;
import org.ssssssss.magicapi.core.model.MagicEntity;
public class DataSourceInfo extends MagicEntity {
/**
* URL
*/
private String url;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 数据源key
*/
private String key;
/**
* 最多返回条数
*/
private int maxRows = -1;
/**
* 驱动类
*/
private String driverClassName;
/**
* 连接池类型
*/
private String type;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public int getMaxRows() {
return maxRows;
}
public void setMaxRows(int maxRows) {
this.maxRows = maxRows;
}
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public MagicEntity simple() {
DataSourceInfo dataSourceInfo = new DataSourceInfo();
super.simple(dataSourceInfo);
dataSourceInfo.setKey(this.key);
return dataSourceInfo;
}
@Override
public MagicEntity copy() {
DataSourceInfo dataSourceInfo = new DataSourceInfo();
super.copyTo(dataSourceInfo);
dataSourceInfo.setUsername(this.username);
dataSourceInfo.setPassword(this.password);
dataSourceInfo.setUrl(this.url);
dataSourceInfo.setDriverClassName(this.driverClassName);
dataSourceInfo.setType(this.type);
dataSourceInfo.setMaxRows(this.maxRows);
dataSourceInfo.setKey(this.key);
return dataSourceInfo;
}
}
package org.ssssssss.magicapi.datasource.model;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.ssssssss.magicapi.modules.db.dialect.DialectAdapter;
import org.ssssssss.magicapi.core.exception.MagicAPIException;
import org.ssssssss.magicapi.modules.db.dialect.Dialect;
import org.ssssssss.magicapi.utils.Assert;
import org.ssssssss.magicapi.utils.IoUtils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.*;
/**
* 动态数据源对象
*
* @author mxd
*/
public class MagicDynamicDataSource {
private static final Logger logger = LoggerFactory.getLogger(MagicDynamicDataSource.class);
private final Map<String, MagicDynamicDataSource.DataSourceNode> dataSourceMap = new HashMap<>();
/**
* 注册默认数据源
*/
public void put(DataSource dataSource) {
put(null, dataSource);
}
/**
* 注册数据源(可以运行时注册)
*
* @param dataSourceKey 数据源Key
*/
public void put(String dataSourceKey, DataSource dataSource) {
put(dataSourceKey, dataSource, -1);
}
/**
* 注册数据源(可以运行时注册)
*
* @param dataSourceKey 数据源Key
* @param maxRows 最大返回行数
*/
public void put(String dataSourceKey, DataSource dataSource, int maxRows) {
put(null, dataSourceKey, dataSourceKey, dataSource, maxRows);
}
/**
* 注册数据源(可以运行时注册)
*
* @param id 数据源ID
* @param dataSourceKey 数据源Key
* @param datasourceName 数据源名称
*/
public void put(String id, String dataSourceKey, String datasourceName, DataSource dataSource, int maxRows) {
if (dataSourceKey == null) {
dataSourceKey = "";
}
logger.info("注册数据源:{}", StringUtils.isNotBlank(dataSourceKey) ? dataSourceKey : "default");
DataSourceNode node = this.dataSourceMap.put(dataSourceKey, new DataSourceNode(dataSource, dataSourceKey, datasourceName, id, maxRows));
if (node != null) {
node.close();
}
if (id != null) {
String finalDataSourceKey = dataSourceKey;
this.dataSourceMap.entrySet().stream()
.filter(it -> id.equals(it.getValue().getId()) && !finalDataSourceKey.equals(it.getValue().getKey()))
.findFirst()
.ifPresent(it -> {
logger.info("移除旧数据源:{}", it.getValue().getKey());
this.dataSourceMap.remove(it.getValue().getKey()).close();
});
}
}
/**
* 获取全部数据源
*/
public List<String> datasources() {
return new ArrayList<>(this.dataSourceMap.keySet());
}
public boolean isEmpty() {
return this.dataSourceMap.isEmpty();
}
/**
* 获取全部数据源
*/
public Collection<DataSourceNode> datasourceNodes() {
return this.dataSourceMap.values();
}
/**
* 删除数据源
*
* @param datasourceKey 数据源Key
*/
public boolean delete(String datasourceKey) {
boolean result = false;
// 检查参数是否合法
if (datasourceKey != null && !datasourceKey.isEmpty()) {
DataSourceNode node = this.dataSourceMap.remove(datasourceKey);
result = node != null;
if (result) {
node.close();
}
}
logger.info("删除数据源:{}:{}", datasourceKey, result ? "成功" : "失败");
return result;
}
/**
* 获取默认数据源
*/
public MagicDynamicDataSource.DataSourceNode getDataSource() {
return getDataSource(null);
}
/**
* 获取数据源
*
* @param datasourceKey 数据源Key
*/
public MagicDynamicDataSource.DataSourceNode getDataSource(String datasourceKey) {
if (datasourceKey == null) {
datasourceKey = "";
}
MagicDynamicDataSource.DataSourceNode dataSourceNode = dataSourceMap.get(datasourceKey);
Assert.isNotNull(dataSourceNode, String.format("找不到数据源%s", datasourceKey));
return dataSourceNode;
}
/**
* 设置默认数据源
*/
public void setDefault(DataSource dataSource) {
put(dataSource);
}
/**
* 设置默认数据源
*
* @param maxRows 最大返回行数
*/
public void setDefault(DataSource dataSource, int maxRows) {
put(null, null, null, dataSource, maxRows);
}
public void add(String dataSourceKey, DataSource dataSource) {
put(dataSourceKey, dataSource);
}
public void add(String dataSourceKey, DataSource dataSource, int maxRows) {
put(null, dataSourceKey, dataSourceKey, dataSource, maxRows);
}
public static class DataSourceNode {
private final String id;
private final String key;
private final String name;
/**
* 事务管理器
*/
private final DataSourceTransactionManager dataSourceTransactionManager;
private final JdbcTemplate jdbcTemplate;
private final DataSource dataSource;
private Dialect dialect;
DataSourceNode(DataSource dataSource, String key, String name, String id, int maxRows) {
this.dataSource = dataSource;
this.key = key;
this.name = name;
this.id = id;
this.dataSourceTransactionManager = new DataSourceTransactionManager(this.dataSource);
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.jdbcTemplate.setMaxRows(maxRows);
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getKey() {
return key;
}
public JdbcTemplate getJdbcTemplate() {
return this.jdbcTemplate;
}
public DataSourceTransactionManager getDataSourceTransactionManager() {
return dataSourceTransactionManager;
}
public Dialect getDialect(DialectAdapter dialectAdapter) {
if (this.dialect == null) {
Connection connection = null;
try {
connection = this.dataSource.getConnection();
this.dialect = dialectAdapter.getDialectFromConnection(connection);
if (this.dialect == null) {
throw new MagicAPIException("自动获取数据库方言失败");
}
} catch (Exception e) {
throw new MagicAPIException("自动获取数据库方言失败", e);
} finally {
DataSourceUtils.releaseConnection(connection, this.dataSource);
}
}
return dialect;
}
public DataSource getDataSource() {
return dataSource;
}
public void close() {
IoUtils.closeDataSource(this.dataSource);
}
}
}
package org.ssssssss.magicapi.datasource.service;
import org.ssssssss.magicapi.datasource.model.DataSourceInfo;
/**
* 数据源加解密
*
* @since 1.7.0
*/
public interface DataSourceEncryptProvider {
/**
* 加密
* @param dataSourceInfo 数据源信息
*/
void encrypt(DataSourceInfo dataSourceInfo);
/**
* 解密
* @param dataSourceInfo 数据源信息
*/
void decrypt(DataSourceInfo dataSourceInfo);
}
package org.ssssssss.magicapi.datasource.service;
import org.ssssssss.magicapi.datasource.model.DataSourceInfo;
import org.ssssssss.magicapi.core.config.JsonCodeConstants;
import org.ssssssss.magicapi.core.model.MagicEntity;
import org.ssssssss.magicapi.core.service.MagicResourceService;
import org.ssssssss.magicapi.core.service.MagicResourceStorage;
import org.ssssssss.magicapi.utils.IoUtils;
import org.ssssssss.magicapi.utils.JsonUtils;
import java.util.Objects;
public class DataSourceInfoMagicResourceStorage implements MagicResourceStorage<DataSourceInfo>, JsonCodeConstants {
private MagicResourceService magicResourceService;
@Override
public String folder() {
return "datasource";
}
@Override
public String suffix() {
return ".json";
}
@Override
public Class<DataSourceInfo> magicClass() {
return DataSourceInfo.class;
}
@Override
public boolean requirePath() {
return false;
}
@Override
public boolean requiredScript() {
return false;
}
@Override
public boolean allowRoot() {
return true;
}
@Override
public String buildMappingKey(DataSourceInfo info) {
return String.format("%s-%s", info.getKey(), info.getUpdateTime());
}
@Override
public void validate(DataSourceInfo entity) {
notBlank(entity.getUrl(), DS_URL_REQUIRED);
notBlank(entity.getKey(), DS_KEY_REQUIRED);
isTrue(IoUtils.validateFileName(entity.getKey()), DATASOURCE_KEY_INVALID);
boolean noneMatchKey = magicResourceService.listFiles("datasource:0").stream()
.map(it -> (DataSourceInfo)it)
.filter(it -> !it.getId().equals(entity.getId()))
.noneMatch(it -> Objects.equals(it.getKey(), entity.getKey()));
isTrue(noneMatchKey, DS_KEY_CONFLICT);
}
@Override
public void setMagicResourceService(MagicResourceService magicResourceService) {
this.magicResourceService = magicResourceService;
}
@Override
public DataSourceInfo read(byte[] bytes) {
return JsonUtils.readValue(bytes, DataSourceInfo.class);
}
@Override
public byte[] write(MagicEntity entity) {
return JsonUtils.toJsonBytes(entity);
}
}
package org.ssssssss.magicapi.datasource.service;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.context.event.EventListener;
import org.springframework.util.ClassUtils;
import org.ssssssss.magicapi.core.config.Constants;
import org.ssssssss.magicapi.datasource.model.MagicDynamicDataSource;
import org.ssssssss.magicapi.core.event.FileEvent;
import org.ssssssss.magicapi.datasource.model.DataSourceInfo;
import org.ssssssss.magicapi.core.service.AbstractMagicDynamicRegistry;
import org.ssssssss.magicapi.core.service.MagicResourceStorage;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class DataSourceMagicDynamicRegistry extends AbstractMagicDynamicRegistry<DataSourceInfo> {
private final MagicDynamicDataSource magicDynamicDataSource;
private static final Logger logger = LoggerFactory.getLogger(DataSourceMagicDynamicRegistry.class);
private static final ClassLoader CLASSLOADER = DataSourceMagicDynamicRegistry.class.getClassLoader();
// copy from DataSourceBuilder
private static final String[] DATA_SOURCE_TYPE_NAMES = new String[]{
"com.zaxxer.hikari.HikariDataSource",
"org.apache.tomcat.jdbc.pool.DataSource",
"org.apache.commons.dbcp2.BasicDataSource"};
public DataSourceMagicDynamicRegistry(MagicResourceStorage<DataSourceInfo> magicResourceStorage, MagicDynamicDataSource magicDynamicDataSource) {
super(magicResourceStorage);
this.magicDynamicDataSource = magicDynamicDataSource;
}
@EventListener(condition = "#event.type == 'datasource'")
public void onFileEvent(FileEvent event) {
try {
processEvent(event);
} catch (Exception e) {
logger.error("注册数据源失败", e);
}
}
@Override
protected boolean register(MappingNode<DataSourceInfo> mappingNode) {
DataSourceInfo info = mappingNode.getEntity();
Map<String, Object> properties = new HashMap<>(info.getProperties());
if(Constants.ES_DRIVER.equals(info.getDriverClassName())){
properties.put("defaultReadOnly", true);
}
properties.put("url", info.getUrl());
properties.put("username", info.getUsername());
properties.put("password", info.getPassword());
if (StringUtils.isBlank(info.getDriverClassName())) {
String driverClass = DatabaseDriver.fromJdbcUrl(info.getUrl()).getDriverClassName();
properties.put("driverClassName", driverClass);
} else {
properties.put("driverClassName", info.getDriverClassName());
}
DataSource datasource = createDataSource(getDataSourceType(info.getType()), properties);
magicDynamicDataSource.put(info.getId(), info.getKey(), info.getName(), datasource, info.getMaxRows());
return true;
}
@Override
protected void unregister(MappingNode<DataSourceInfo> mappingNode) {
magicDynamicDataSource.delete(mappingNode.getMappingKey());
}
// copy from DataSourceBuilder
private DataSource createDataSource(Class<? extends DataSource> dataSourceType, Map<String, Object> properties) {
DataSource dataSource = BeanUtils.instantiateClass(dataSourceType);
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
aliases.addAliases("url", "jdbc-url");
aliases.addAliases("username", "user");
Binder binder = new Binder(source.withAliases(aliases));
binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(dataSource));
return dataSource;
}
@SuppressWarnings("unchecked")
private Class<? extends DataSource> getDataSourceType(String datasourceType) {
if (StringUtils.isNotBlank(datasourceType)) {
try {
return (Class<? extends DataSource>) ClassUtils.forName(datasourceType, CLASSLOADER);
} catch (Exception ignored) {
}
}
for (String name : DATA_SOURCE_TYPE_NAMES) {
try {
return (Class<? extends DataSource>) ClassUtils.forName(name, CLASSLOADER);
} catch (Exception ignored) {
// ignored
}
}
return null;
}
@Override
public List<DataSourceInfo> defaultMappings() {
return magicDynamicDataSource.datasourceNodes().stream().filter(it -> it.getId() == null).map(it -> {
DataSourceInfo dataSourceInfo = new DataSourceInfo();
dataSourceInfo.setName(StringUtils.defaultIfBlank(it.getName(), StringUtils.defaultIfBlank(it.getKey(), "默认数据源")));
dataSourceInfo.setKey(it.getKey());
return dataSourceInfo;
}).collect(Collectors.toList());
}
}
package org.ssssssss.magicapi.datasource.web;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.core.web.MagicExceptionHandler;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.datasource.model.DataSourceInfo;
import org.ssssssss.magicapi.core.model.JsonBean;
import org.ssssssss.magicapi.core.web.MagicController;
import org.ssssssss.magicapi.utils.JdbcUtils;
import java.sql.Connection;
public class MagicDataSourceController extends MagicController implements MagicExceptionHandler {
public MagicDataSourceController(MagicConfiguration configuration) {
super(configuration);
}
@RequestMapping("/datasource/jdbc/test")
@ResponseBody
public JsonBean<String> test(@RequestBody DataSourceInfo properties) {
try {
Connection connection = JdbcUtils.getConnection(properties.getDriverClassName(), properties.getUrl(), properties.getUsername(), properties.getPassword());
JdbcUtils.close(connection);
} catch (Exception e) {
return new JsonBean<>(e.getMessage());
}
return new JsonBean<>("ok");
}
}
package org.ssssssss.magicapi.function.model;
import com.fasterxml.jackson.core.type.TypeReference;
import org.ssssssss.magicapi.core.model.MagicEntity;
import org.ssssssss.magicapi.core.model.Parameter;
import org.ssssssss.magicapi.core.model.PathMagicEntity;
import org.ssssssss.magicapi.utils.JsonUtils;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class FunctionInfo extends PathMagicEntity {
private String description;
private String returnType;
private String mappingPath;
private List<Parameter> parameters = Collections.emptyList();
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public void setParameter(String parameter) {
try {
this.parameters = JsonUtils.readValue(Objects.toString(parameter, "[]"), new TypeReference<List<Parameter>>() {
});
} catch (Throwable ignored) {
}
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getMappingPath() {
return mappingPath;
}
public void setMappingPath(String mappingPath) {
this.mappingPath = mappingPath;
}
public String getReturnType() {
return returnType;
}
public void setReturnType(String returnType) {
this.returnType = returnType;
}
public List<Parameter> getParameters() {
return parameters;
}
public void setParameters(List<Parameter> parameters) {
this.parameters = parameters;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FunctionInfo functionInfo = (FunctionInfo) o;
return Objects.equals(id, functionInfo.id) &&
Objects.equals(path, functionInfo.path) &&
Objects.equals(script, functionInfo.script) &&
Objects.equals(name, functionInfo.name) &&
Objects.equals(groupId, functionInfo.groupId) &&
Objects.equals(description, functionInfo.description) &&
Objects.equals(parameters, functionInfo.parameters) &&
Objects.equals(returnType, functionInfo.returnType);
}
public FunctionInfo copy() {
FunctionInfo info = new FunctionInfo();
super.copyTo(info);
info.setDescription(this.description);
info.setParameters(this.parameters);
info.setMappingPath(this.mappingPath);
info.setReturnType(this.returnType);
return info;
}
@Override
public MagicEntity simple() {
FunctionInfo info = new FunctionInfo();
super.simple(info);
return info;
}
@Override
public int hashCode() {
return Objects.hash(id, path, script, name, groupId, parameters, description, returnType);
}
}
package org.ssssssss.magicapi.function.service;
import org.ssssssss.magicapi.function.model.FunctionInfo;
import org.ssssssss.magicapi.core.service.AbstractPathMagicResourceStorage;
public class FunctionInfoMagicResourceStorage extends AbstractPathMagicResourceStorage<FunctionInfo> {
@Override
public String folder() {
return "function";
}
@Override
public Class<FunctionInfo> magicClass() {
return FunctionInfo.class;
}
@Override
public String buildMappingKey(FunctionInfo info) {
return buildMappingKey(info, magicResourceService.getGroupPath(info.getGroupId()));
}
@Override
public void validate(FunctionInfo entity) {
notBlank(entity.getPath(), FUNCTION_PATH_REQUIRED);
}
}
package org.ssssssss.magicapi.function.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.core.event.FileEvent;
import org.ssssssss.magicapi.core.event.GroupEvent;
import org.ssssssss.magicapi.function.model.FunctionInfo;
import org.ssssssss.magicapi.core.model.Parameter;
import org.ssssssss.magicapi.core.service.MagicResourceStorage;
import org.ssssssss.magicapi.utils.ScriptManager;
import org.ssssssss.magicapi.core.service.AbstractMagicDynamicRegistry;
import org.ssssssss.script.MagicResourceLoader;
import org.ssssssss.script.MagicScriptContext;
import org.ssssssss.script.exception.MagicExitException;
import org.ssssssss.script.runtime.ExitValue;
import java.util.List;
import java.util.function.Function;
public class FunctionMagicDynamicRegistry extends AbstractMagicDynamicRegistry<FunctionInfo> {
private static final Logger logger = LoggerFactory.getLogger(FunctionMagicDynamicRegistry.class);
public FunctionMagicDynamicRegistry(MagicResourceStorage<FunctionInfo> magicResourceStorage) {
super(magicResourceStorage);
MagicResourceLoader.addFunctionLoader(this::lookupLambdaFunction);
}
private Object lookupLambdaFunction(MagicScriptContext context, String path) {
FunctionInfo functionInfo = getMapping(path);
if (functionInfo != null) {
String scriptName = MagicConfiguration.getMagicResourceService().getScriptName(functionInfo);
List<Parameter> parameters = functionInfo.getParameters();
return (Function<Object[], Object>) objects -> {
MagicScriptContext functionContext = new MagicScriptContext(context.getRootVariables());
functionContext.setScriptName(scriptName);
if (objects != null) {
for (int i = 0, len = objects.length, size = parameters.size(); i < len && i < size; i++) {
functionContext.set(parameters.get(i).getName(), objects[i]);
}
}
Object value = ScriptManager.executeScript(functionInfo.getScript(), functionContext);
if (value instanceof ExitValue) {
throw new MagicExitException((ExitValue) value);
}
return value;
};
}
return null;
}
@EventListener(condition = "#event.type == 'function'")
public void onFileEvent(FileEvent event) {
processEvent(event);
}
@EventListener(condition = "#event.type == 'function'")
public void onGroupEvent(GroupEvent event) {
processEvent(event);
}
@Override
protected boolean register(MappingNode<FunctionInfo> mappingNode) {
logger.debug("注册函数:{}", mappingNode.getMappingKey());
return true;
}
@Override
protected void unregister(MappingNode<FunctionInfo> mappingNode) {
logger.debug("取消注册函数:{}", mappingNode.getMappingKey());
}
}
package org.ssssssss.magicapi.jsr223;
import org.ssssssss.magicapi.core.exception.MagicAPIException;
import org.ssssssss.magicapi.jsr223.LanguageProvider;
import javax.script.Compilable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.SimpleBindings;
import java.util.Map;
/**
* JSR223规范支持
*
* @author mxd
*/
public class JSR223LanguageProvider implements LanguageProvider {
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
@Override
public boolean support(String languageName) {
return scriptEngineManager.getEngineByName(languageName) != null;
}
@Override
public Object execute(String languageName, String script, Map<String, Object> context) throws Exception {
ScriptEngine scriptEngine = scriptEngineManager.getEngineByName(languageName);
if (scriptEngine instanceof Compilable) {
Compilable compilable = (Compilable) scriptEngine;
try {
return compilable.compile(script).eval(new SimpleBindings(context));
} catch (Exception e) {
throw new MagicAPIException(String.format("编译%s出错", languageName), e);
}
} else {
return scriptEngine.eval(script, new SimpleBindings(context));
}
}
}
package org.ssssssss.magicapi.jsr223;
import java.util.Map;
/**
* 自定义语言接口
*
* @author mxd
*/
public interface LanguageProvider {
/**
* 是否支持该语言
*
* @param languageName 语言名
* @return 是否支持
*/
boolean support(String languageName);
/**
* 执行具体脚本
*
* @param languageName 语言类型
* @param script 脚本内容
* @param context 当前环境中的变量信息
* @return 执行结果
* @throws Exception 执行过程中抛出的异常
*/
Object execute(String languageName, String script, Map<String, Object> context) throws Exception;
}
package org.ssssssss.magicapi.modules;
import org.ssssssss.script.MagicScriptContext;
import java.beans.Transient;
public interface DynamicModule<T> {
@Transient
T getDynamicModule(MagicScriptContext context);
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment