'迁移目录'

This commit is contained in:
mxd
2021-05-22 08:39:09 +08:00
parent 51bf328227
commit eca3dcef93
127 changed files with 11742 additions and 0 deletions

215
magic-api/pom.xml Normal file
View File

@@ -0,0 +1,215 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api</artifactId>
<version>1.1.3</version>
<packaging>jar</packaging>
<name>magic-api</name>
<description>auto generate http api</description>
<url>https://ssssssss.org/</url>
<inceptionYear>2020</inceptionYear>
<licenses>
<license>
<name>The MIT License (MIT)</name>
<url>https://github.com/ssssssss-team/magic-api/blob/master/LICENSE</url>
</license>
</licenses>
<developers>
<developer>
<name>jmxd</name>
<email>838425805@qq.com</email>
</developer>
</developers>
<scm>
<url>https://gitee.com/ssssssss-team/magic-api</url>
<connection>scm:git:https://gitee.com/ssssssss-team/magic-api.git</connection>
<developerConnection>scm:git:https://gitee.com/ssssssss-team/magic-api.git</developerConnection>
</scm>
<properties>
<spring-boot.version>2.4.5</spring-boot.version>
<magic-script.version>1.4.5</magic-script.version>
<commons-compress.version>1.20</commons-compress.version>
<commons-io.version>2.7</commons-io.version>
<commons-text.version>1.6</commons-text.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version>
<spring-boot-starter-log4j.version>1.3.8.RELEASE</spring-boot-starter-log4j.version>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.ssssssss</groupId>
<artifactId>magic-script</artifactId>
<version>${magic-script.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j</artifactId>
<version>${spring-boot-starter-log4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>${commons-beanutils.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>${commons-compress.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<!--Compiler-->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<!-- Source -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<!-- Java Doc -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<additionalOptions>
<additionalOption>-Xdoclint:none</additionalOption>
</additionalOptions>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- GPG -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement>
<snapshotRepository>
<id>oss</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>oss</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
</repository>
</distributionManagement>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,60 @@
package org.ssssssss.magicapi.adapter;
import org.springframework.jdbc.core.RowMapper;
import org.ssssssss.magicapi.provider.ColumnMapperProvider;
import org.ssssssss.magicapi.provider.impl.*;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class ColumnMapperAdapter {
private final Map<String, RowMapper<Map<String, Object>>> columnMapRowMappers = new HashMap<>();
private final Map<String, Function<String, String>> rowMapColumnMappers = new HashMap<>();
private RowMapper<Map<String, Object>> mapRowColumnMapper;
private Function<String, String> rowMapColumnMapper;
public ColumnMapperAdapter() {
setDefault(new DefaultColumnMapperProvider());
add(new CamelColumnMapperProvider());
add(new PascalColumnMapperProvider());
add(new LowerColumnMapperProvider());
add(new UpperColumnMapperProvider());
}
public void add(ColumnMapperProvider columnMapperProvider) {
columnMapRowMappers.put(columnMapperProvider.name(), columnMapperProvider.getColumnMapRowMapper());
rowMapColumnMappers.put(columnMapperProvider.name(), columnMapperProvider.getRowMapColumnMapper());
}
public void setDefault(ColumnMapperProvider columnMapperProvider) {
this.mapRowColumnMapper = columnMapperProvider.getColumnMapRowMapper();
this.rowMapColumnMapper = columnMapperProvider.getRowMapColumnMapper();
add(columnMapperProvider);
}
public void setDefault(String name) {
this.mapRowColumnMapper = getColumnMapRowMapper(name);
this.rowMapColumnMapper = getRowMapColumnMapper(name);
}
public RowMapper<Map<String, Object>> getDefaultColumnMapRowMapper() {
return this.mapRowColumnMapper;
}
public Function<String, String> getDefaultRowMapColumnMapper() {
return this.rowMapColumnMapper;
}
public RowMapper<Map<String, Object>> getColumnMapRowMapper(String name) {
return columnMapRowMappers.getOrDefault(name, mapRowColumnMapper);
}
public Function<String, String> getRowMapColumnMapper(String name) {
return rowMapColumnMappers.getOrDefault(name, rowMapColumnMapper);
}
}

View File

@@ -0,0 +1,57 @@
package org.ssssssss.magicapi.adapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.ssssssss.magicapi.dialect.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class DialectAdapter {
private static final Logger logger = LoggerFactory.getLogger(DialectAdapter.class);
private final List<Dialect> dialectList = new ArrayList<>();
/**
* 缓存已解析的方言
*/
private final Map<String, Dialect> dialectMap = new ConcurrentHashMap<>();
public DialectAdapter() {
add(new MySQLDialect());
add(new OracleDialect());
add(new PostgreSQLDialect());
add(new ClickhouseDialect());
add(new DB2Dialect());
add(new SQLServerDialect());
add(new SQLServer2005Dialect());
add(new DmDialect());
}
public void add(Dialect dialect) {
this.dialectList.add(dialect);
}
/**
* 获取数据库方言
*/
public Dialect getDialectFromUrl(String fromUrl) {
Dialect cached = dialectMap.get(fromUrl);
if (cached == null && !dialectMap.containsKey(fromUrl)) {
for (Dialect dialect : dialectList) {
if (dialect.match(fromUrl)) {
cached = dialect;
break;
}
}
if (cached == null) {
logger.warn(String.format("magic-api在%s中无法获取dialect", fromUrl));
}
dialectMap.put(fromUrl, cached);
}
return cached;
}
}

View File

@@ -0,0 +1,156 @@
package org.ssssssss.magicapi.adapter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public interface Resource {
/**
* 是否是只读
*/
default boolean readonly() {
return false;
}
/**
* 是否存在
*/
default boolean exists() {
return false;
}
/**
* 是否是目录
*/
default boolean isDirectory() {
return false;
}
/**
* 删除
*/
default boolean delete() {
return false;
}
/**
* 创建目录
*/
default boolean mkdir() {
return false;
}
/**
* 重命名
*/
default boolean renameTo(Resource resource) {
return false;
}
/**
* 写入
*/
default boolean write(String content) {
return false;
}
/**
* 写入
*/
default boolean write(byte[] bytes) {
return false;
}
default String separator() {
return null;
}
default void processExport(ZipOutputStream zos, String path, Resource directory, List<Resource> resources, List<String> excludes) throws IOException {
for (Resource resource : resources) {
String fullName = directory.getAbsolutePath();
if (!fullName.endsWith(separator())) {
fullName += separator();
}
fullName += resource.name();
if (resource.isDirectory()) {
fullName += separator();
}
if (fullName.equals(resource.getAbsolutePath()) && !excludes.contains(resource.name())) {
if (resource.isDirectory()) {
String newPath = path + resource.name() + "/";
zos.putNextEntry(new ZipEntry(newPath));
zos.closeEntry();
processExport(zos, newPath, resource, resources, excludes);
} else {
zos.putNextEntry(new ZipEntry(path + resource.name()));
zos.write(resource.read());
zos.closeEntry();
}
}
}
}
default void export(OutputStream os, String... excludes) throws IOException {
ZipOutputStream zos = new ZipOutputStream(os);
processExport(zos, "", this, resources(), Arrays.asList(excludes == null ? new String[0] : excludes));
zos.close();
}
/**
* 读取
*/
byte[] read();
/**
* 读取当前资源下的所有内容,主要是缓存作用。
*/
default void readAll() {
}
/**
* 获取子目录
*/
default Resource getDirectory(String name) {
return getResource(name);
}
/**
* 获取子资源
*/
Resource getResource(String name);
/**
* 获取资源名
*/
String name();
/**
* 获取子资源集合
*/
List<Resource> resources();
/**
* 父级资源
*/
Resource parent();
/**
* 目录
*/
List<Resource> dirs();
/**
* 遍历文件
*/
List<Resource> files(String suffix);
/**
* 获取所在位置
*/
String getAbsolutePath();
}

View File

@@ -0,0 +1,61 @@
package org.ssssssss.magicapi.adapter;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.ResourceUtils;
import org.ssssssss.magicapi.adapter.resource.FileResource;
import org.ssssssss.magicapi.adapter.resource.JarResource;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
public abstract class ResourceAdapter {
public static final String SPRING_BOOT_CLASS_PATH = "BOOT-INF/classes/";
private static PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
public static Resource getResource(String location, boolean readonly) throws IOException {
if (location == null) {
return null;
}
org.springframework.core.io.Resource resource;
if (location.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
resource = resolver.getResource(location);
if (resource.exists()) {
return resolveResource(resource, true);
} else {
throw new FileNotFoundException(String.format("%s not found", resource.getDescription()));
}
} else {
resource = resolver.getResource(location);
if (!resource.exists()) {
resource = resolver.getResource(ResourceUtils.FILE_URL_PREFIX + location);
}
}
return resolveResource(resource, readonly);
}
private static Resource resolveResource(org.springframework.core.io.Resource resource, boolean readonly) throws IOException {
URL url = resource.getURI().toURL();
if (url.getProtocol().equals(ResourceUtils.URL_PROTOCOL_JAR)) {
JarURLConnection connection = (JarURLConnection) url.openConnection();
boolean springBootClassPath = connection.getClass().getName().equals("org.springframework.boot.loader.jar.JarURLConnection");
String entryName = (springBootClassPath ? SPRING_BOOT_CLASS_PATH : "") + connection.getEntryName();
JarFile jarFile = connection.getJarFile();
List<JarEntry> entries = jarFile.stream().filter(it -> it.getName().startsWith(entryName)).collect(Collectors.toList());
if (entries.isEmpty()) {
entries = jarFile.stream().filter(it -> it.getName().startsWith(connection.getEntryName())).collect(Collectors.toList());
return new JarResource(jarFile, connection.getEntryName(), entries, springBootClassPath);
}
return new JarResource(jarFile, entryName, entries, springBootClassPath);
} else {
return new FileResource(resource.getFile(), readonly);
}
}
}

View File

@@ -0,0 +1,158 @@
package org.ssssssss.magicapi.adapter.resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.utils.Assert;
import org.ssssssss.magicapi.utils.IoUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
public class DatabaseResource extends KeyValueResource {
private static final Logger logger = LoggerFactory.getLogger(DatabaseResource.class);
private final JdbcTemplate template;
private final String tableName;
private Map<String, String> cachedContent = new ConcurrentHashMap<>();
public DatabaseResource(JdbcTemplate template, String tableName) {
this(template, tableName, false);
}
public DatabaseResource(JdbcTemplate template, String tableName, boolean readonly) {
this(template, tableName, "/magic-api", readonly);
}
public DatabaseResource(JdbcTemplate template, String tableName, String path, boolean readonly) {
this(template, tableName, path, readonly, null);
}
public DatabaseResource(JdbcTemplate template, String tableName, String path, boolean readonly, KeyValueResource parent) {
super("/", path, readonly, parent);
this.template = template;
this.tableName = tableName;
}
public DatabaseResource(JdbcTemplate template, String tableName, String path, boolean readonly, Map<String, String> cachedContent, KeyValueResource parent) {
this(template, tableName, path, readonly, parent);
this.cachedContent = cachedContent;
}
@Override
public byte[] read() {
String value = this.cachedContent.get(path);
if (value == null) {
String sql = String.format("select file_content from %s where file_path = ?", tableName);
value = template.queryForObject(sql, String.class, this.path);
}
return value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8);
}
@Override
public void readAll() {
this.cachedContent.clear();
String sql = String.format("select file_path, file_content from %s where file_path like '%s%%'", tableName, this.path);
SqlRowSet sqlRowSet = template.queryForRowSet(sql);
while (sqlRowSet.next()) {
Object object = sqlRowSet.getObject(2);
String content = null;
if (object instanceof String) {
content = object.toString();
} else if (object instanceof byte[]) {
content = new String((byte[]) object, StandardCharsets.UTF_8);
} else if (object instanceof Blob) {
Blob blob = (Blob) object;
try (InputStream is = blob.getBinaryStream()) {
content = new String(IoUtils.bytes(is), StandardCharsets.UTF_8);
} catch (SQLException | IOException ex) {
logger.error("读取content失败", ex);
}
} else if (object instanceof Clob) {
Clob clob = (Clob) object;
try {
content = clob.getSubString(1, (int) clob.length());
} catch (SQLException ex) {
logger.error("读取content失败", ex);
}
}
Assert.isNotNull(content, "读取content失败请检查列类型是否正确");
this.cachedContent.put(sqlRowSet.getString(1), content);
}
}
@Override
public boolean exists() {
if (this.cachedContent.containsKey(this.path)) {
return true;
}
String sql = String.format("select count(*) from %s where file_path = ?", tableName);
Long value = template.queryForObject(sql, Long.class, this.path);
return value != null && value > 0;
}
@Override
public boolean write(String content) {
String sql = String.format("update %s set file_content = ? where file_path = ?", tableName);
if (exists()) {
if (template.update(sql, content, this.path) > 0) {
this.cachedContent.put(this.path, content);
return true;
}
}
sql = String.format("insert into %s (file_path,file_content) values(?,?)", tableName);
if (template.update(sql, this.path, content) > 0) {
this.cachedContent.put(this.path, content);
return true;
}
return false;
}
@Override
public Set<String> keys() {
String sql = String.format("select file_path from %s where file_path like '%s%%'", tableName, isDirectory() ? this.path : (this.path + separator));
return new HashSet<>(template.queryForList(sql, String.class));
}
@Override
public boolean renameTo(Map<String, String> renameKeys) {
List<Object[]> args = renameKeys.entrySet().stream().map(entry -> new Object[]{entry.getValue(), entry.getKey()}).collect(Collectors.toList());
String sql = String.format("update %s set file_path = ? where file_path = ?", tableName);
if (Arrays.stream(template.batchUpdate(sql, args)).sum() > 0) {
renameKeys.forEach((oldKey, newKey) -> this.cachedContent.put(newKey, this.cachedContent.remove(oldKey)));
return true;
}
return false;
}
@Override
public boolean delete() {
String sql = String.format("delete from %s where file_path = ? or file_path like '%s%%'", tableName, isDirectory() ? this.path : this.path + separator);
if (template.update(sql, this.path) > 0) {
this.cachedContent.remove(this.path);
return true;
}
return false;
}
@Override
public Function<String, Resource> mappedFunction() {
return it -> new DatabaseResource(template, tableName, it, readonly, this.cachedContent, this);
}
@Override
public String toString() {
return String.format("db://%s/%s", tableName, Objects.toString(this.path, ""));
}
}

View File

@@ -0,0 +1,135 @@
package org.ssssssss.magicapi.adapter.resource;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.utils.IoUtils;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class FileResource implements Resource {
private final boolean readonly;
private File file;
public FileResource(File file, boolean readonly) {
this.file = file;
this.readonly = readonly;
}
@Override
public boolean readonly() {
return this.readonly;
}
@Override
public boolean exists() {
return this.file.exists();
}
@Override
public boolean delete() {
return !readonly() && IoUtils.delete(this.file);
}
@Override
public boolean isDirectory() {
return this.file.isDirectory();
}
@Override
public boolean mkdir() {
return !readonly() && this.file.mkdirs();
}
@Override
public byte[] read() {
return IoUtils.bytes(this.file);
}
@Override
public boolean renameTo(Resource resource) {
if (!this.readonly()) {
File target = ((FileResource) resource).file;
if (this.file.renameTo(target)) {
this.file = target;
return true;
}
}
return false;
}
@Override
public Resource getResource(String name) {
return new FileResource(new File(this.file, name), this.readonly);
}
@Override
public String name() {
return this.file.getName();
}
@Override
public List<Resource> resources() {
File[] files = this.file.listFiles();
return files == null ? Collections.emptyList() : Arrays.stream(files).map(it -> new FileResource(it, this.readonly)).collect(Collectors.toList());
}
@Override
public Resource parent() {
return new FileResource(this.file.getParentFile(), this.readonly);
}
@Override
public List<Resource> dirs() {
return IoUtils.dirs(this.file).stream().map(it -> new FileResource(it, this.readonly)).collect(Collectors.toList());
}
@Override
public boolean write(byte[] bytes) {
return !readonly() && IoUtils.write(this.file, bytes);
}
@Override
public boolean write(String content) {
return !readonly() && IoUtils.write(this.file, content);
}
@Override
public List<Resource> files(String suffix) {
return IoUtils.files(this.file, suffix).stream().map(it -> new FileResource(it, this.readonly)).collect(Collectors.toList());
}
@Override
public String getAbsolutePath() {
return this.file.getAbsolutePath();
}
@Override
public String toString() {
return String.format("file://%s", this.file.getAbsolutePath());
}
@Override
public void processExport(ZipOutputStream zos, String path, Resource directory, List<Resource> resources, List<String> excludes) throws IOException {
for (Resource resource : resources) {
if (resource.parent().getAbsolutePath().equals(directory.getAbsolutePath()) && !excludes.contains(resource.name())) {
if (resource.isDirectory()) {
String newPath = path + resource.name() + "/";
zos.putNextEntry(new ZipEntry(newPath));
zos.closeEntry();
processExport(zos, newPath, resource, resource.resources(), excludes);
} else {
zos.putNextEntry(new ZipEntry(path + resource.name()));
zos.write(resource.read());
zos.closeEntry();
}
}
}
}
}

View File

@@ -0,0 +1,135 @@
package org.ssssssss.magicapi.adapter.resource;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.adapter.ResourceAdapter;
import org.ssssssss.magicapi.utils.IoUtils;
import org.ssssssss.magicapi.utils.PathUtils;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
public class JarResource implements Resource {
private final JarFile jarFile;
private final ZipEntry entry;
private final List<JarEntry> entries;
private final String entryName;
private final boolean inSpringBoot;
private JarResource parent = this;
public JarResource(JarFile jarFile, String entryName, List<JarEntry> entries, boolean inSpringBoot) {
this.jarFile = jarFile;
this.entryName = entryName;
this.inSpringBoot = inSpringBoot;
this.entry = getEntry(this.entryName);
this.entries = entries;
}
public JarResource(JarFile jarFile, String entryName, List<JarEntry> entries, JarResource parent, boolean inSpringBoot) {
this(jarFile, entryName, entries, inSpringBoot);
this.parent = parent;
}
@Override
public String separator() {
return "/";
}
@Override
public boolean readonly() {
return true;
}
@Override
public byte[] read() {
try {
return IoUtils.bytes(this.jarFile.getInputStream(entry));
} catch (IOException e) {
return new byte[0];
}
}
@Override
public boolean isDirectory() {
return this.entry.isDirectory();
}
@Override
public boolean exists() {
return this.entry != null;
}
protected ZipEntry getEntry(String name) {
if (inSpringBoot && name.startsWith(ResourceAdapter.SPRING_BOOT_CLASS_PATH)) {
name = name.substring(ResourceAdapter.SPRING_BOOT_CLASS_PATH.length());
}
return this.jarFile.getEntry(name);
}
@Override
public Resource getResource(String name) {
String entryName = PathUtils.replaceSlash(this.entryName + "/" + name);
String prefix = PathUtils.replaceSlash(entryName + "/");
return new JarResource(this.jarFile, entryName, entries.stream()
.filter(it -> it.getName().startsWith(prefix))
.collect(Collectors.toList()), this, this.inSpringBoot);
}
@Override
public String name() {
String name = this.entryName;
if (isDirectory()) {
name = name.substring(0, name.length() - 1);
}
int index = name.lastIndexOf("/");
return index > -1 ? name.substring(index + 1) : name;
}
@Override
public Resource parent() {
return this.parent;
}
@Override
public List<Resource> dirs() {
return resources().stream().filter(Resource::isDirectory).collect(Collectors.toList());
}
@Override
public List<Resource> files(String suffix) {
return this.entries.stream().filter(it -> it.getName().endsWith(suffix))
.map(entry -> new JarResource(jarFile, entry.getName(), Collections.emptyList(), this.inSpringBoot))
.collect(Collectors.toList());
}
@Override
public List<Resource> resources() {
String prefix = PathUtils.replaceSlash(this.entryName + "/");
return entries.stream()
.filter(it -> it.getName().startsWith(prefix))
.map(entry -> new JarResource(jarFile, entry.getName(), entries.stream()
.filter(item -> item.getName().startsWith(PathUtils.replaceSlash(entry.getName() + "/")))
.collect(Collectors.toList()), this.inSpringBoot)
)
.collect(Collectors.toList());
}
@Override
public String getAbsolutePath() {
return this.jarFile.getName() + "/" + this.entryName;
}
@Override
public String toString() {
return String.format("jar://%s", this.entryName);
}
}

View File

@@ -0,0 +1,155 @@
package org.ssssssss.magicapi.adapter.resource;
import org.ssssssss.magicapi.adapter.Resource;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public abstract class KeyValueResource implements Resource {
protected String separator;
protected String path;
protected KeyValueResource parent;
protected boolean readonly = false;
public KeyValueResource(String separator, String path, KeyValueResource parent) {
this.separator = separator;
this.path = path;
this.parent = parent;
}
public KeyValueResource(String separator, String path, boolean readonly, KeyValueResource parent) {
this.separator = separator;
this.path = path;
this.parent = parent;
this.readonly = readonly;
}
@Override
public String separator() {
return this.separator;
}
@Override
public boolean isDirectory() {
return this.path.endsWith(separator);
}
@Override
public boolean readonly() {
return this.readonly;
}
@Override
public final boolean renameTo(Resource resource) {
if (readonly()) {
return false;
}
if (resource.getAbsolutePath().equalsIgnoreCase(this.getAbsolutePath())) {
return true;
}
if (!(resource instanceof KeyValueResource)) {
throw new IllegalArgumentException("无法将" + this.getAbsolutePath() + "重命名为:" + resource.getAbsolutePath());
}
KeyValueResource targetResource = (KeyValueResource) resource;
// 判断移动的是否是文件夹
if (resource.isDirectory()) {
Set<String> oldKeys = this.keys();
Map<String, String> mappings = new HashMap<>(oldKeys.size());
int keyLen = this.path.length();
oldKeys.forEach(oldKey -> mappings.put(oldKey, targetResource.path + oldKey.substring(keyLen)));
return renameTo(mappings);
} else {
return renameTo(Collections.singletonMap(this.path, targetResource.path));
}
}
@Override
public boolean delete() {
if (readonly()) {
return false;
}
if (isDirectory()) {
return this.keys().stream().allMatch(this::deleteByKey);
}
return deleteByKey(getAbsolutePath());
}
protected boolean deleteByKey(String key) {
return false;
}
/**
* 需要做修改的key原key: 新key
*/
protected abstract boolean renameTo(Map<String, String> renameKeys);
@Override
public String name() {
String name = this.path;
if (isDirectory()) {
name = this.path.substring(0, name.length() - 1);
}
int index = name.lastIndexOf(separator);
return index > -1 ? name.substring(index + 1) : name;
}
@Override
public Resource getResource(String name) {
name = (isDirectory() ? this.path : this.path + separator) + name;
return mappedFunction().apply(name);
}
@Override
public Resource getDirectory(String name) {
return getResource(name + separator);
}
@Override
public boolean mkdir() {
if (!isDirectory()) {
this.path += separator;
}
return write("this is directory");
}
@Override
public Resource parent() {
return this.parent;
}
@Override
public boolean write(byte[] bytes) {
return !readonly() && write(new String(bytes, StandardCharsets.UTF_8));
}
@Override
public List<Resource> resources() {
return keys().stream().map(mappedFunction()).collect(Collectors.toList());
}
protected abstract Function<String, Resource> mappedFunction();
protected abstract Set<String> keys();
@Override
public List<Resource> dirs() {
return resources().stream().filter(Resource::isDirectory).collect(Collectors.toList());
}
@Override
public List<Resource> files(String suffix) {
return resources().stream().filter(it -> it.name().endsWith(suffix)).collect(Collectors.toList());
}
@Override
public String getAbsolutePath() {
return this.path;
}
}

View File

@@ -0,0 +1,114 @@
package org.ssssssss.magicapi.adapter.resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.ssssssss.magicapi.adapter.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
public class RedisResource extends KeyValueResource {
private static final Logger logger = LoggerFactory.getLogger(RedisResource.class);
private final StringRedisTemplate redisTemplate;
private final Map<String, String> cachedContent = new ConcurrentHashMap<>();
public RedisResource(StringRedisTemplate redisTemplate, String path, boolean readonly, RedisResource parent) {
super(":", path, readonly, parent);
this.redisTemplate = redisTemplate;
}
public RedisResource(StringRedisTemplate redisTemplate, String path, boolean readonly) {
this(redisTemplate, path, readonly, null);
}
@Override
public void readAll() {
this.cachedContent.clear();
List<String> keys = new ArrayList<>(keys());
List<String> values = redisTemplate.opsForValue().multiGet(keys);
if (values != null) {
for (int i = 0, size = keys.size(); i < size; i++) {
this.cachedContent.put(keys.get(i), values.get(i));
}
}
}
@Override
public byte[] read() {
String value = this.cachedContent.get(path);
if (value == null) {
value = redisTemplate.opsForValue().get(path);
}
return value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8);
}
@Override
public boolean write(String content) {
this.redisTemplate.opsForValue().set(this.path, content);
this.cachedContent.put(this.path, content);
return true;
}
@Override
protected boolean renameTo(Map<String, String> renameKeys) {
renameKeys.forEach(this.redisTemplate::rename);
renameKeys.forEach((oldKey, newKey) -> this.cachedContent.put(newKey, this.cachedContent.remove(oldKey)));
return true;
}
@Override
public boolean exists() {
if (this.cachedContent.containsKey(this.path)) {
return true;
}
return Boolean.TRUE.equals(this.redisTemplate.hasKey(this.path));
}
@Override
protected boolean deleteByKey(String key) {
if (Boolean.TRUE.equals(this.redisTemplate.delete(key))) {
this.cachedContent.remove(key);
return true;
}
return false;
}
@Override
protected Function<String, Resource> mappedFunction() {
return (it) -> new RedisResource(this.redisTemplate, it, readonly, this);
}
@Override
protected Set<String> keys() {
Set<String> keys = this.redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
ScanOptions options = new ScanOptions.ScanOptionsBuilder()
.count(Long.MAX_VALUE)
.match((isDirectory() ? this.path : (this.path + separator)) + "*")
.build();
Set<String> returnKeys = new HashSet<>();
try (Cursor<byte[]> cursor = connection.scan(options)) {
while (cursor.hasNext()) {
returnKeys.add(new String(cursor.next()));
}
} catch (IOException e) {
logger.error("扫描key出错", e);
}
return returnKeys;
});
return keys == null ? Collections.emptySet() : keys;
}
@Override
public String toString() {
return String.format("redis://%s", getAbsolutePath());
}
}

View File

@@ -0,0 +1,122 @@
package org.ssssssss.magicapi.adapter.resource;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.ssssssss.magicapi.adapter.Resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ZipResource implements Resource {
private final Map<String, byte[]> cachedContent;
private String path = "";
private Resource parent;
public ZipResource(InputStream is) throws IOException {
cachedContent = new HashMap<>();
try (ZipArchiveInputStream zis = new ZipArchiveInputStream(is)) {
ArchiveEntry entry;
byte[] buf = new byte[4096];
int len = -1;
while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
while ((len = zis.read(buf, 0, buf.length)) != -1) {
os.write(buf, 0, len);
}
cachedContent.put(entry.getName(), os.toByteArray());
}
}
}
ZipResource(String name, Map<String, byte[]> cachedContent, Resource parent) {
this.path = name;
this.cachedContent = cachedContent;
this.parent = parent;
}
@Override
public boolean readonly() {
return true;
}
@Override
public boolean exists() {
return this.cachedContent.containsKey(this.path);
}
@Override
public byte[] read() {
return cachedContent.getOrDefault(this.path, new byte[0]);
}
@Override
public Resource getResource(String name) {
return new ZipResource(this.path + name, this.cachedContent, this);
}
@Override
public Resource getDirectory(String name) {
return new ZipResource(this.path + name + "/", this.cachedContent, this);
}
@Override
public boolean isDirectory() {
return this.path.isEmpty() || this.path.endsWith("/");
}
@Override
public String name() {
String name = this.path;
if (isDirectory()) {
name = this.path.substring(0, name.length() - 1);
}
int index = name.lastIndexOf("/");
return index > -1 ? name.substring(index + 1) : name;
}
@Override
public List<Resource> resources() {
throw new UnsupportedOperationException();
}
@Override
public Resource parent() {
return this.parent;
}
@Override
public List<Resource> dirs() {
int len = this.path.length();
return this.cachedContent.keySet().stream()
.filter(it -> it.endsWith("/") && it.startsWith(this.path) && it.indexOf("/", len + 1) == it.length() - 1)
.map(it -> this.getDirectory(it.substring(len, it.length() - 1)))
.collect(Collectors.toList());
}
@Override
public List<Resource> files(String suffix) {
if (isDirectory()) {
int len = this.path.length();
return this.cachedContent.keySet().stream()
.filter(it -> it.startsWith(this.path) && it.endsWith(suffix) && it.indexOf("/", len) == -1)
.map(it -> this.getResource(it.substring(len)))
.collect(Collectors.toList());
}
return Collections.emptyList();
}
@Override
public String getAbsolutePath() {
return this.path;
}
}

View File

@@ -0,0 +1,133 @@
package org.ssssssss.magicapi.cache;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class DefaultSqlCache extends LinkedHashMap<String, DefaultSqlCache.ExpireNode<Object>> implements SqlCache {
private final String separator = ":";
private final int capacity;
private final long expire;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public DefaultSqlCache(int capacity, long expire) {
super((int) Math.ceil(capacity / 0.75) + 1, 0.75f, true);
// 容量
this.capacity = capacity;
// 固定过期时间
this.expire = expire;
}
@Override
public void put(String name, String key, Object value) {
// 封装成过期时间节点
put(name, key, value, this.expire);
}
@Override
public void put(String name, String key, Object value, long ttl) {
long expireTime = ttl > 0 ? (System.currentTimeMillis() + ttl) :
(this.expire > -1 ? System.currentTimeMillis() + this.expire : Long.MAX_VALUE);
lock.writeLock().lock();
try {
// 封装成过期时间节点
put(name + separator + key, new ExpireNode<>(expireTime, value));
} finally {
lock.writeLock().unlock();
}
}
@Override
public Object get(String name, String key) {
key = name + separator + key;
lock.readLock().lock();
ExpireNode<Object> expireNode;
try {
expireNode = super.get(key);
} finally {
lock.readLock().unlock();
}
if (expireNode == null) {
return null;
}
// 惰性删除过期的
// if (this.expire > -1L && expireNode.expire < System.currentTimeMillis()) {
if (expireNode.expire < System.currentTimeMillis()) {
try {
lock.writeLock().lock();
super.remove(key);
} finally {
lock.writeLock().unlock();
}
return null;
}
return expireNode.value;
}
@Override
public void delete(String name) {
try {
lock.writeLock().lock();
Iterator<Map.Entry<String, ExpireNode<Object>>> iterator = super.entrySet().iterator();
String prefix = name + separator;
// 清除所有key前缀为name + separator的缓存
while (iterator.hasNext()) {
Map.Entry<String, ExpireNode<Object>> entry = iterator.next();
if (entry.getKey().startsWith(prefix)) {
iterator.remove();
}
}
} finally {
lock.writeLock().unlock();
}
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, ExpireNode<Object>> eldest) {
if (this.expire > -1L && size() > capacity) {
clean();
}
// lru淘汰
return size() > this.capacity;
}
/**
* 清理已过期的数据
*/
private void clean() {
try {
lock.writeLock().lock();
Iterator<Map.Entry<String, ExpireNode<Object>>> iterator = super.entrySet().iterator();
long now = System.currentTimeMillis();
while (iterator.hasNext()) {
Map.Entry<String, ExpireNode<Object>> next = iterator.next();
// 判断是否过期
if (next.getValue().expire < now) {
iterator.remove();
}
}
} finally {
lock.writeLock().unlock();
}
}
/**
* 过期时间节点
*/
static class ExpireNode<V> {
long expire;
V value;
ExpireNode(long expire, V value) {
this.expire = expire;
this.value = value;
}
}
}

View File

@@ -0,0 +1,54 @@
package org.ssssssss.magicapi.cache;
import org.ssssssss.magicapi.utils.MD5Utils;
import java.util.Arrays;
/**
* SQL缓存接口
*/
public interface SqlCache {
/**
* 计算key
*/
default String buildSqlCacheKey(String sql, Object[] params) {
return MD5Utils.encrypt(sql + ":" + Arrays.toString(params));
}
/**
* 存入缓存
*
* @param name 名字
* @param key key
* @param value 值
*/
void put(String name, String key, Object value);
/**
* 存入缓存
*
* @param name 名字
* @param key key
* @param value 值
* @param ttl 有效期
*/
void put(String name, String key, Object value, long ttl);
/**
* 获取缓存
*
* @param name 名字
* @param key key
*/
<T> T get(String name, String key);
/**
* 删除缓存
*
* @param name 名字
*/
void delete(String name);
}

View File

@@ -0,0 +1,239 @@
package org.ssssssss.magicapi.config;
import org.springframework.http.converter.HttpMessageConverter;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.controller.RequestHandler;
import org.ssssssss.magicapi.interceptor.AuthorizationInterceptor;
import org.ssssssss.magicapi.interceptor.RequestInterceptor;
import org.ssssssss.magicapi.provider.*;
import java.util.ArrayList;
import java.util.List;
public class MagicConfiguration {
/**
* 拦截器
*/
private final List<RequestInterceptor> requestInterceptors = new ArrayList<>();
/**
* 接口映射
*/
private MappingHandlerMapping mappingHandlerMapping;
/**
* 函数管理
*/
private MagicFunctionManager magicFunctionManager;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 编辑器配置文件
*/
private String editorConfig;
/**
* 接口查询Service
*/
private ApiServiceProvider apiServiceProvider;
/**
* 分组查询Service
*/
private GroupServiceProvider groupServiceProvider;
/**
* 函数查询Service
*/
private FunctionServiceProvider functionServiceProvider;
/**
* 动态数据源
*/
private MagicDynamicDataSource magicDynamicDataSource;
private MagicAPIService magicAPIService;
/**
* 请求出错时,是否抛出异常
*/
private boolean throwException = false;
/**
* 结果处理器
*/
private ResultProvider resultProvider;
private Resource workspace;
private List<HttpMessageConverter<?>> httpMessageConverters = new ArrayList<>();
private AuthorizationInterceptor authorizationInterceptor;
/**
* debug 超时时间
*/
private int debugTimeout;
private boolean enableWeb = false;
public void addRequestInterceptor(RequestInterceptor requestInterceptor) {
this.requestInterceptors.add(requestInterceptor);
}
public MappingHandlerMapping getMappingHandlerMapping() {
return mappingHandlerMapping;
}
public void setMappingHandlerMapping(MappingHandlerMapping mappingHandlerMapping) {
this.mappingHandlerMapping = mappingHandlerMapping;
}
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 AuthorizationInterceptor getAuthorizationInterceptor() {
return authorizationInterceptor;
}
public void setAuthorizationInterceptor(AuthorizationInterceptor authorizationInterceptor) {
this.authorizationInterceptor = authorizationInterceptor;
}
public List<RequestInterceptor> getRequestInterceptors() {
return requestInterceptors;
}
public ApiServiceProvider getApiServiceProvider() {
return apiServiceProvider;
}
public void setApiServiceProvider(ApiServiceProvider apiServiceProvider) {
this.apiServiceProvider = apiServiceProvider;
}
public GroupServiceProvider getGroupServiceProvider() {
return groupServiceProvider;
}
public void setGroupServiceProvider(GroupServiceProvider groupServiceProvider) {
this.groupServiceProvider = groupServiceProvider;
}
public MagicDynamicDataSource getMagicDynamicDataSource() {
return magicDynamicDataSource;
}
public void setMagicDynamicDataSource(MagicDynamicDataSource magicDynamicDataSource) {
this.magicDynamicDataSource = magicDynamicDataSource;
}
public boolean isThrowException() {
return throwException;
}
public void setThrowException(boolean throwException) {
this.throwException = throwException;
}
public ResultProvider getResultProvider() {
return resultProvider;
}
public void setResultProvider(ResultProvider resultProvider) {
this.resultProvider = resultProvider;
}
public List<HttpMessageConverter<?>> getHttpMessageConverters() {
return httpMessageConverters;
}
public void setHttpMessageConverters(List<HttpMessageConverter<?>> httpMessageConverters) {
this.httpMessageConverters = httpMessageConverters;
}
public int getDebugTimeout() {
return debugTimeout;
}
public void setDebugTimeout(int debugTimeout) {
this.debugTimeout = debugTimeout;
}
public boolean isEnableWeb() {
return enableWeb;
}
public void setEnableWeb(boolean enableWeb) {
this.enableWeb = enableWeb;
}
public FunctionServiceProvider getFunctionServiceProvider() {
return functionServiceProvider;
}
public void setFunctionServiceProvider(FunctionServiceProvider functionServiceProvider) {
this.functionServiceProvider = functionServiceProvider;
}
public MagicFunctionManager getMagicFunctionManager() {
return magicFunctionManager;
}
public void setMagicFunctionManager(MagicFunctionManager magicFunctionManager) {
this.magicFunctionManager = magicFunctionManager;
}
public String getEditorConfig() {
return editorConfig;
}
public void setEditorConfig(String editorConfig) {
this.editorConfig = editorConfig;
}
public Resource getWorkspace() {
return workspace;
}
public void setWorkspace(Resource workspace) {
this.workspace = workspace;
}
public MagicAPIService getMagicAPIService() {
return magicAPIService;
}
public void setMagicAPIService(MagicAPIService magicAPIService) {
this.magicAPIService = magicAPIService;
}
/**
* 打印banner
*/
public void printBanner() {
System.out.println(" __ __ _ _ ____ ___ ");
System.out.println(" | \\/ | __ _ __ _ (_) ___ / \\ | _ \\|_ _|");
System.out.println(" | |\\/| | / _` | / _` || | / __| / _ \\ | |_) || | ");
System.out.println(" | | | || (_| || (_| || || (__ / ___ \\ | __/ | | ");
System.out.println(" |_| |_| \\__,_| \\__, ||_| \\___|/_/ \\_\\|_| |___|");
System.out.println(" |___/ " + RequestHandler.class.getPackage().getImplementationVersion());
}
}

View File

@@ -0,0 +1,44 @@
package org.ssssssss.magicapi.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.ssssssss.magicapi.model.Constants;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MagicCorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
public void process(HttpServletRequest request, HttpServletResponse response) {
String value = request.getHeader("Origin");
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, StringUtils.isBlank(value) ? "*" : value);
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
value = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
if (StringUtils.isNotBlank(value)) {
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, value);
}
value = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, StringUtils.isBlank(value) ? "GET,POST,OPTIONS,PUT,DELETE" : value);
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (StringUtils.isNotBlank(Constants.HEADER_REQUEST_SESSION)) {
process(request, (HttpServletResponse) resp);
}
chain.doFilter(req, resp);
}
}

View File

@@ -0,0 +1,233 @@
package org.ssssssss.magicapi.config;
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.adapter.DialectAdapter;
import org.ssssssss.magicapi.dialect.Dialect;
import org.ssssssss.magicapi.exception.MagicAPIException;
import org.ssssssss.magicapi.utils.Assert;
import org.ssssssss.magicapi.utils.IoUtils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.*;
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 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.getDialectFromUrl(connection.getMetaData().getURL());
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);
}
}
}

View File

@@ -0,0 +1,7 @@
package org.ssssssss.magicapi.config;
/**
* 函数,主要用于脚本中直接可使用的函数,如 now();
*/
public interface MagicFunction {
}

View File

@@ -0,0 +1,212 @@
package org.ssssssss.magicapi.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.ssssssss.magicapi.model.FunctionInfo;
import org.ssssssss.magicapi.model.Group;
import org.ssssssss.magicapi.model.Parameter;
import org.ssssssss.magicapi.model.TreeNode;
import org.ssssssss.magicapi.provider.FunctionServiceProvider;
import org.ssssssss.magicapi.provider.GroupServiceProvider;
import org.ssssssss.magicapi.script.ScriptManager;
import org.ssssssss.magicapi.utils.PathUtils;
import org.ssssssss.script.MagicResourceLoader;
import org.ssssssss.script.MagicScriptContext;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
public class MagicFunctionManager {
private static final Logger logger = LoggerFactory.getLogger(MagicFunctionManager.class);
private static Map<String, FunctionInfo> mappings = new ConcurrentHashMap<>();
private GroupServiceProvider groupServiceProvider;
private FunctionServiceProvider functionServiceProvider;
private TreeNode<Group> groups;
public MagicFunctionManager(GroupServiceProvider groupServiceProvider, FunctionServiceProvider functionServiceProvider) {
this.groupServiceProvider = groupServiceProvider;
this.functionServiceProvider = functionServiceProvider;
}
public void registerFunctionLoader() {
MagicResourceLoader.addFunctionLoader((path) -> {
FunctionInfo info = mappings.get(path);
if (info != null) {
List<Parameter> parameters = info.getParameters();
return (Function<Object[], Object>) objects -> {
MagicScriptContext context = MagicScriptContext.get();
try {
MagicScriptContext functionContext = new MagicScriptContext(context.getRootVariables());
MagicScriptContext.set(functionContext);
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]);
}
}
return ScriptManager.executeScript(info.getScript(), functionContext);
} finally {
MagicScriptContext.set(context);
}
};
}
return null;
});
}
/**
* 加载所有分组
*/
public synchronized void loadGroup() {
groups = groupServiceProvider.functionGroupTree();
}
public void registerAllFunction() {
loadGroup();
functionServiceProvider.listWithScript().stream()
.filter(it -> groupServiceProvider.getFullPath(it.getGroupId()) != null)
.forEach(this::register);
}
public boolean hasRegister(FunctionInfo info) {
String path = PathUtils.replaceSlash(Objects.toString(groupServiceProvider.getFullPath(info.getGroupId()), "") + "/" + info.getPath());
FunctionInfo functionInfo = mappings.get(path);
return functionInfo != null && !Objects.equals(info.getId(), functionInfo.getId());
}
public boolean hasRegister(Set<String> paths) {
return paths.stream().anyMatch(mappings::containsKey);
}
/**
* 函数移动
*/
public boolean move(String id, String groupId) {
FunctionInfo info = mappings.get(id);
if (info == null) {
return false;
}
String path = Objects.toString(groupServiceProvider.getFullPath(groupId), "");
FunctionInfo functionInfo = mappings.get(PathUtils.replaceSlash(path + "/" + info.getPath()));
if (functionInfo != null && !Objects.equals(functionInfo.getId(), id)) {
return false;
}
unregister(id);
info.setGroupId(groupId);
register(info);
return true;
}
public void register(FunctionInfo functionInfo) {
if (functionInfo == null) {
return;
}
FunctionInfo oldFunctionInfo = mappings.get(functionInfo.getId());
if (oldFunctionInfo != null) {
// 完全一致时不用注册
if (functionInfo.equals(oldFunctionInfo)) {
return;
}
// 如果路径不一致,则需要取消注册
if (!Objects.equals(functionInfo.getPath(), oldFunctionInfo.getPath())) {
unregister(functionInfo.getId());
}
}
String path = Objects.toString(groupServiceProvider.getFullPath(functionInfo.getGroupId()), "");
mappings.put(functionInfo.getId(), functionInfo);
path = PathUtils.replaceSlash(path + "/" + functionInfo.getPath());
functionInfo.setMappingPath(path);
mappings.put(path, functionInfo);
logger.info("注册函数:[{}:{}]", functionInfo.getName(), path);
}
public Collection<FunctionInfo> getFunctionInfos() {
return mappings.values();
}
private boolean hasConflict(TreeNode<Group> group, String newPath) {
// 获取要移动的接口
List<FunctionInfo> infos = mappings.values().stream()
.filter(info -> Objects.equals(info.getGroupId(), group.getNode().getId()))
.distinct()
.collect(Collectors.toList());
// 判断是否有冲突
for (FunctionInfo info : infos) {
if (mappings.containsKey(PathUtils.replaceSlash(newPath + "/" + info.getPath()))) {
return true;
}
}
for (TreeNode<Group> child : group.getChildren()) {
if (hasConflict(child, newPath + "/" + Objects.toString(child.getNode().getPath(), ""))) {
return true;
}
}
return false;
}
public boolean checkGroup(Group group) {
TreeNode<Group> oldTree = groups.findTreeNode((item) -> item.getId().equals(group.getId()));
// 如果只改了名字,则不做任何操作
if (Objects.equals(oldTree.getNode().getParentId(), group.getParentId()) &&
Objects.equals(oldTree.getNode().getPath(), group.getPath())) {
return true;
}
// 新的接口分组路径
String newPath = Objects.toString(groupServiceProvider.getFullPath(group.getParentId()), "");
// 检测冲突
return !hasConflict(oldTree, newPath + "/" + Objects.toString(group.getPath(), ""));
}
private void recurseUpdateGroup(TreeNode<Group> node, boolean updateGroupId) {
mappings.values().stream()
.filter(info -> Objects.equals(info.getGroupId(), node.getNode().getId()))
.distinct()
.collect(Collectors.toList())
.forEach(info -> {
unregister(info.getId());
if (updateGroupId) {
info.setGroupId(node.getNode().getId());
}
register(info);
});
for (TreeNode<Group> child : node.getChildren()) {
recurseUpdateGroup(child, false);
}
}
public boolean updateGroup(String groupId) {
loadGroup(); // 重新加载分组
TreeNode<Group> groupTreeNode = groups.findTreeNode((item) -> item.getId().equals(groupId));
recurseUpdateGroup(groupTreeNode, true);
return functionServiceProvider.reload(groupId);
}
public void deleteGroup(List<String> groupIds) {
mappings.values().stream()
.filter(info -> groupIds.contains(info.getGroupId()))
.distinct()
.collect(Collectors.toList())
.forEach(info -> unregister(info.getId()));
// 刷新分组缓存
loadGroup();
}
public void unregister(String id) {
FunctionInfo functionInfo = mappings.remove(id);
if (functionInfo != null) {
mappings.remove(functionInfo.getMappingPath());
logger.info("取消注册函数:[{},{}]", functionInfo.getName(), functionInfo.getMappingPath());
}
}
public void enableRefresh(int interval) {
if (interval > 0) {
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(this::registerAllFunction, interval, interval, TimeUnit.SECONDS);
}
}
}

View File

@@ -0,0 +1,15 @@
package org.ssssssss.magicapi.config;
import org.ssssssss.script.annotation.UnableCall;
/**
* 模块主要用于import指令import时根据模块名获取当前类如import assert;
*/
public interface MagicModule {
/**
* 获取模块名
*/
@UnableCall
String getModuleName();
}

View File

@@ -0,0 +1,46 @@
package org.ssssssss.magicapi.config;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.ssssssss.magicapi.controller.MagicController;
import org.ssssssss.magicapi.exception.MagicLoginException;
import org.ssssssss.magicapi.interceptor.AuthorizationInterceptor;
import org.ssssssss.magicapi.model.Constants;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* /magic/web相关接口的拦截器
*/
public class MagicWebRequestInterceptor implements HandlerInterceptor {
private final MagicCorsFilter magicCorsFilter;
private AuthorizationInterceptor authorizationInterceptor;
public MagicWebRequestInterceptor(MagicCorsFilter magicCorsFilter, AuthorizationInterceptor authorizationInterceptor) {
this.magicCorsFilter = magicCorsFilter;
this.authorizationInterceptor = authorizationInterceptor;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws MagicLoginException {
HandlerMethod handlerMethod;
if (handler instanceof HandlerMethod) {
handlerMethod = (HandlerMethod) handler;
handler = handlerMethod.getBean();
if (handler instanceof MagicController) {
if (magicCorsFilter != null) {
magicCorsFilter.process(request, response);
}
Valid valid = handlerMethod.getMethodAnnotation(Valid.class);
if (authorizationInterceptor.requireLogin() && (valid == null || valid.requireLogin())) {
request.setAttribute(Constants.ATTRIBUTE_MAGIC_USER, authorizationInterceptor.getUserByToken(request.getHeader(Constants.MAGIC_TOKEN_HEADER)));
}
((MagicController) handler).doValid(request, valid);
}
}
return true;
}
}

View File

@@ -0,0 +1,527 @@
package org.ssssssss.magicapi.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.ssssssss.magicapi.controller.RequestHandler;
import org.ssssssss.magicapi.model.ApiInfo;
import org.ssssssss.magicapi.model.Group;
import org.ssssssss.magicapi.model.TreeNode;
import org.ssssssss.magicapi.provider.ApiServiceProvider;
import org.ssssssss.magicapi.provider.GroupServiceProvider;
import org.ssssssss.magicapi.utils.Mapping;
import org.ssssssss.magicapi.utils.PathUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 请求映射
*/
public class MappingHandlerMapping {
/**
* 已缓存的映射信息
*/
private static final Map<String, MappingNode> mappings = new ConcurrentHashMap<>();
private static final Logger logger = LoggerFactory.getLogger(MappingHandlerMapping.class);
/**
* 接口分组
*/
private static TreeNode<Group> groups;
/**
* 请求到达时处理的方法
*/
private final Method method = RequestHandler.class.getDeclaredMethod("invoke", HttpServletRequest.class, HttpServletResponse.class, Map.class, Map.class);
/**
* 统一接口前缀
*/
private final String prefix;
/**
* 是否覆盖应用接口
*/
private final boolean allowOverride;
/**
* 缓存已映射的接口信息
*/
private final List<ApiInfo> apiInfos = Collections.synchronizedList(new ArrayList<>());
private Mapping mappingHelper;
/**
* 请求处理器
*/
private Object handler;
/**
* 接口信息读取
*/
private ApiServiceProvider magicApiService;
/**
* 分组信息读取
*/
private GroupServiceProvider groupServiceProvider;
public MappingHandlerMapping(String prefix, boolean allowOverride) throws NoSuchMethodException {
this.prefix = prefix;
this.allowOverride = allowOverride;
}
/**
* 根据request获取对应的接口信息
*/
public static ApiInfo getMappingApiInfo(HttpServletRequest request) {
NativeWebRequest webRequest = new ServletWebRequest(request);
// 找到注册的路径
String requestMapping = (String) webRequest.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
// 根据请求方法和路径获取接口信息
return getMappingApiInfo(buildMappingKey(request.getMethod(), requestMapping));
}
/**
* 根据绑定的key获取接口信息
*/
private static ApiInfo getMappingApiInfo(String key) {
return mappings.get(key).getInfo();
}
/**
* 构建缓存map的key
*
* @param requestMethod 请求方法
* @param requestMapping 请求路径
*/
private static String buildMappingKey(String requestMethod, String requestMapping) {
if (!StringUtils.isEmpty(requestMapping) && !requestMapping.startsWith("/")) {
requestMapping = "/" + requestMapping;
}
return Objects.toString(requestMethod, "GET").toUpperCase() + ":" + requestMapping;
}
public static Group findGroup(String groupId) {
TreeNode<Group> node = groups.findTreeNode(it -> it.getId().equals(groupId));
return node != null ? node.getNode() : null;
}
public static List<Group> findGroups(String groupId) {
List<Group> groups = new ArrayList<>();
Group group;
while (!"0".equals(groupId) && (group = MappingHandlerMapping.findGroup(groupId)) != null) {
groups.add(group);
groupId = group.getParentId();
}
return groups;
}
public void setRequestMappingHandlerMapping(RequestMappingHandlerMapping requestMappingHandlerMapping) {
this.mappingHelper = Mapping.create(requestMappingHandlerMapping);
}
public void setHandler(Object handler) {
this.handler = handler;
}
public void setMagicApiService(ApiServiceProvider magicApiService) {
this.magicApiService = magicApiService;
}
public void setGroupServiceProvider(GroupServiceProvider groupServiceProvider) {
this.groupServiceProvider = groupServiceProvider;
}
public List<ApiInfo> getApiInfos() {
return apiInfos;
}
/**
* 加载所有分组
*/
public synchronized void loadGroup() {
groups = groupServiceProvider.apiGroupTree();
}
/**
* 注册请求
*/
public void registerAllMapping() {
try {
loadGroup();
List<ApiInfo> list = magicApiService.listWithScript();
if (list != null) {
list = list.stream().filter(it -> groupServiceProvider.getFullPath(it.getGroupId()) != null).collect(Collectors.toList());
for (ApiInfo info : list) {
try {
// 当接口存在时,刷新缓存
registerMapping(info, true);
} catch (Exception e) {
logger.error("接口:{}注册失败", info.getName(), e);
}
}
List<String> resistedList = list.stream().map(ApiInfo::getId).collect(Collectors.toList());
Iterator<ApiInfo> iterator = apiInfos.iterator();
while (iterator.hasNext()) {
String oldId = iterator.next().getId();
// 当接口不存在时,取消注册接口
if (!resistedList.contains(oldId)) {
unregisterMapping(oldId, false);
iterator.remove();
}
}
}
} catch (Exception e) {
logger.info("注册接口映射失败", e);
}
}
/**
* 根据请求方法和路径获取接口信息
*
* @param method 请求方法
* @param requestMapping 请求路径
*/
public ApiInfo getApiInfo(String method, String requestMapping) {
MappingNode mappingNode = mappings.get(buildMappingKey(method, concatPath("", requestMapping)));
return mappingNode == null ? null : mappingNode.getInfo();
}
private boolean hasConflict(TreeNode<Group> group, String newPath) {
// 获取要移动的接口
List<ApiInfo> infos = apiInfos.stream().filter(info -> Objects.equals(info.getGroupId(), group.getNode().getId())).collect(Collectors.toList());
// 判断是否有冲突
for (ApiInfo info : infos) {
String path = concatPath(newPath, "/" + info.getPath());
String mappingKey = buildMappingKey(info.getMethod(), path);
MappingNode mappingNode = mappings.get(mappingKey);
if (mappingNode != null) {
if (mappingNode.getInfo().equals(info)) {
continue;
}
return true;
}
if (!allowOverride) {
Map<RequestMappingInfo, HandlerMethod> handlerMethods = this.mappingHelper.getHandlerMethods();
if (handlerMethods.get(getRequestMapping(info.getMethod(), path)) != null) {
return true;
}
}
}
for (TreeNode<Group> child : group.getChildren()) {
if (hasConflict(child, newPath + "/" + Objects.toString(child.getNode().getPath(), ""))) {
return true;
}
}
return false;
}
/**
* 检测是否允许修改
*/
public boolean checkGroup(Group group) {
TreeNode<Group> oldTree = groups.findTreeNode((item) -> item.getId().equals(group.getId()));
// 如果没移动目录且没改路径,则只需要判断名字是否冲突
boolean parentIdEquals = Objects.equals(oldTree.getNode().getParentId(), group.getParentId());
boolean nameEquals = Objects.equals(oldTree.getNode().getName(), group.getName());
if (parentIdEquals && Objects.equals(oldTree.getNode().getPath(), group.getPath())) {
return nameEquals || !groupServiceProvider.exists(group);
}
// 检测名字是否冲突
if ((!parentIdEquals || !nameEquals) && groupServiceProvider.exists(group)) {
return false;
}
// 新的接口分组路径
String newPath = groupServiceProvider.getFullPath(group.getParentId());
// 检测冲突
return !hasConflict(oldTree, newPath + "/" + Objects.toString(group.getPath(), ""));
}
public boolean hasRegister(Set<String> paths) {
return paths.stream().anyMatch(mappings::containsKey);
}
/**
* 删除分组
*/
public void deleteGroup(List<String> groupIds) {
// 找到对应的所有接口
List<ApiInfo> deleteInfos = apiInfos.stream().filter(info -> groupIds.contains(info.getGroupId())).collect(Collectors.toList());
for (ApiInfo info : deleteInfos) {
unregisterMapping(info.getId(), true);
}
// 全部删除
apiInfos.removeAll(deleteInfos);
// 刷新分组缓存
loadGroup();
}
/**
* 修改分组
*/
public boolean updateGroup(String groupId) {
loadGroup(); // 重新加载分组
TreeNode<Group> groupTreeNode = groups.findTreeNode((item) -> item.getId().equals(groupId));
recurseUpdateGroup(groupTreeNode, true);
return magicApiService.reload(groupId);
}
private void recurseUpdateGroup(TreeNode<Group> node, boolean updateGroupId) {
apiInfos.stream().filter(info -> Objects.equals(info.getGroupId(), node.getNode().getId())).forEach(info -> {
unregisterMapping(info.getId(), false);
if (updateGroupId) {
info.setGroupId(node.getNode().getId());
}
registerMapping(info, false);
});
for (TreeNode<Group> child : node.getChildren()) {
recurseUpdateGroup(child, false);
}
}
/**
* 判断是否已注册
*/
public boolean hasRegisterMapping(ApiInfo info) {
if (info.getId() != null) {
MappingNode mappingNode = mappings.get(info.getId());
ApiInfo oldInfo = mappingNode == null ? null : mappingNode.getInfo();
if (oldInfo != null
&& Objects.equals(oldInfo.getGroupId(), info.getGroupId())
&& Objects.equals(oldInfo.getMethod(), info.getMethod())
&& Objects.equals(oldInfo.getPath(), info.getPath())) {
return false;
}
}
String mappingKey = getMappingKey(info);
if (mappings.containsKey(mappingKey)) {
return !mappings.get(mappingKey).getInfo().getId().equals(info.getId());
}
if (!allowOverride) {
Map<RequestMappingInfo, HandlerMethod> handlerMethods = this.mappingHelper.getHandlerMethods();
return handlerMethods.get(getRequestMapping(info)) != null;
}
return false;
}
/**
* 接口移动
*/
public boolean move(String id, String groupId) {
MappingNode mappingNode = mappings.get(id);
if (mappingNode == null) {
return false;
}
ApiInfo copy = mappingNode.getInfo().copy();
copy.setGroupId(groupId);
if (hasRegisterMapping(copy)) {
return false;
}
unregisterMapping(id, true);
registerMapping(copy, true);
return true;
}
/**
* 注册请求映射
*/
public void registerMapping(ApiInfo info, boolean delete) {
if(info == null){
return;
}
// 先判断是否已注册,如果已注册,则先取消注册在进行注册。
MappingNode mappingNode = mappings.get(info.getId());
String newMappingKey = getMappingKey(info);
if (mappingNode != null) {
ApiInfo oldInfo = mappingNode.getInfo();
String oldMappingKey = mappingNode.getMappingKey();
// URL 路径一致时,刷新脚本内容即可
if (Objects.equals(oldMappingKey, newMappingKey)) {
if (!info.equals(oldInfo)) {
mappingNode.setInfo(info);
mappings.get(newMappingKey).setInfo(info);
if (delete) {
refreshCache(info);
}
logger.info("刷新接口:{},{}", info.getName(), newMappingKey);
}
return;
}
// URL不一致时需要取消注册旧接口重新注册新接口
logger.info("取消注册接口:{},{}", oldInfo.getName(), oldMappingKey);
// 取消注册
mappings.remove(oldMappingKey);
mappingHelper.unregister(getRequestMapping(oldInfo));
}
mappingNode = new MappingNode(info);
mappingNode.setMappingKey(newMappingKey);
// 注册
RequestMappingInfo requestMapping = getRequestMapping(info);
mappingNode.setRequestMappingInfo(requestMapping);
mappingNode.setInfo(info);
// 如果与应用冲突
if (!overrideApplicationMapping(requestMapping)) {
logger.error("接口{},{}与应用冲突,无法注册", info.getName(), newMappingKey);
return;
}
logger.info("注册接口:{},{}", info.getName(), newMappingKey);
mappings.put(info.getId(), mappingNode);
mappings.put(newMappingKey, mappingNode);
registerMapping(requestMapping, handler, method);
if (delete) { // 刷新缓存
refreshCache(info);
}
}
private void refreshCache(ApiInfo info) {
apiInfos.removeIf(i -> i.getId().equalsIgnoreCase(info.getId()));
apiInfos.add(info);
}
private void registerMapping(RequestMappingInfo requestMapping, Object handler, Method method) {
mappingHelper.register(requestMapping, handler, method);
}
/**
* 取消注册请求映射
*/
public void unregisterMapping(String id, boolean delete) {
MappingNode mappingNode = mappings.remove(id);
if (mappingNode != null) {
ApiInfo info = mappingNode.getInfo();
logger.info("取消注册接口:{}", info.getName());
mappings.remove(mappingNode.getMappingKey());
mappingHelper.unregister(mappingNode.getRequestMappingInfo());
if (delete) { //刷新缓存
apiInfos.removeIf(i -> i.getId().equalsIgnoreCase(info.getId()));
}
}
}
/**
* 根据接口信息获取绑定map的key
*/
private String getMappingKey(ApiInfo info) {
return buildMappingKey(info.getMethod(), getRequestPath(info.getGroupId(), info.getPath()));
}
/**
* 处理前缀
*
* @param groupId 分组ID
* @param path 请求路径
*/
public String getRequestPath(String groupId, String path) {
return concatPath(groupServiceProvider.getFullPath(groupId), path);
}
public void registerController(Object target, String base) {
Method[] methods = target.getClass().getDeclaredMethods();
for (Method method : methods) {
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
if (requestMapping != null) {
String[] paths = Stream.of(requestMapping.value()).map(value -> base + value).toArray(String[]::new);
mappingHelper.register(RequestMappingInfo.paths(paths).build(), target, method);
}
}
}
private String concatPath(String groupPath, String path) {
path = groupPath + "/" + path;
if (prefix != null) {
path = prefix + "/" + path;
}
path = PathUtils.replaceSlash(path);
if (path.startsWith("/")) {
return path.substring(1);
}
return path;
}
/**
* 覆盖应用接口
*/
private boolean overrideApplicationMapping(RequestMappingInfo requestMapping) {
if (mappingHelper.getHandlerMethods().containsKey(requestMapping)) {
if (!allowOverride) {
// 不允许覆盖
return false;
}
logger.warn("取消注册应用接口:{}", requestMapping);
// 取消注册原接口
mappingHelper.unregister(requestMapping);
}
return true;
}
/**
* 根据接口信息构建 RequestMappingInfo
*/
private RequestMappingInfo getRequestMapping(ApiInfo info) {
return RequestMappingInfo.paths(getRequestPath(info.getGroupId(), info.getPath())).methods(RequestMethod.valueOf(info.getMethod().toUpperCase())).build();
}
/**
* 根据接口信息构建 RequestMappingInfo
*/
private RequestMappingInfo getRequestMapping(String method, String path) {
return RequestMappingInfo.paths(path).methods(RequestMethod.valueOf(method.toUpperCase())).build();
}
public void enableRefresh(int interval) {
if (interval > 0) {
logger.info("启动自动刷新magic-api");
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(this::registerAllMapping, interval, interval, TimeUnit.SECONDS);
}
}
static class MappingNode {
private ApiInfo info;
private String mappingKey;
private RequestMappingInfo requestMappingInfo;
public MappingNode(ApiInfo info) {
this.info = info;
}
public ApiInfo getInfo() {
return info;
}
public void setInfo(ApiInfo info) {
this.info = info;
}
public String getMappingKey() {
return mappingKey;
}
public void setMappingKey(String mappingKey) {
this.mappingKey = mappingKey;
}
public RequestMappingInfo getRequestMappingInfo() {
return requestMappingInfo;
}
public void setRequestMappingInfo(RequestMappingInfo requestMappingInfo) {
this.requestMappingInfo = requestMappingInfo;
}
}
}

View File

@@ -0,0 +1,26 @@
package org.ssssssss.magicapi.config;
import org.ssssssss.magicapi.interceptor.Authorization;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
/**
* 验证是否有该权限
*/
Authorization authorization() default Authorization.NONE;
/**
* 验证是否是只读模式
*/
boolean readonly() default true;
/**
* 验证是否需要登录
*/
boolean requireLogin() default true;
}

View File

@@ -0,0 +1,27 @@
package org.ssssssss.magicapi.context;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
/**
* Cookie Context 用于脚本中获取cookie信息
*/
public class CookieContext extends HashMap<String, String> {
private final Cookie[] cookies;
public CookieContext(HttpServletRequest request) {
this.cookies = request.getCookies();
}
@Override
public String get(Object key) {
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase("" + key)) {
return cookie.getValue();
}
}
return null;
}
}

View File

@@ -0,0 +1,28 @@
package org.ssssssss.magicapi.context;
import org.ssssssss.magicapi.model.RequestEntity;
import javax.servlet.http.HttpServletRequest;
public class RequestContext {
private static final ThreadLocal<RequestEntity> REQUEST_ENTITY_THREAD_LOCAL = new InheritableThreadLocal<>();
public static HttpServletRequest getHttpServletRequest() {
RequestEntity requestEntity = REQUEST_ENTITY_THREAD_LOCAL.get();
return requestEntity == null ? null : requestEntity.getRequest();
}
public static RequestEntity getRequestEntity() {
return REQUEST_ENTITY_THREAD_LOCAL.get();
}
public static void setRequestEntity(RequestEntity requestEntity) {
REQUEST_ENTITY_THREAD_LOCAL.set(requestEntity);
}
public static void remove() {
REQUEST_ENTITY_THREAD_LOCAL.remove();
}
}

View File

@@ -0,0 +1,28 @@
package org.ssssssss.magicapi.context;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
/**
* Session Context 用于脚本中获取Session信息
*/
public class SessionContext extends HashMap<String, Object> {
private final HttpSession session;
public SessionContext(HttpSession session) {
this.session = session;
}
@Override
public Object get(Object key) {
return session != null ? session.getAttribute(key.toString()) : null;
}
@Override
public Object put(String key, Object value) {
Object oldValue = session.getAttribute(key);
session.setAttribute(key, value);
return oldValue;
}
}

View File

@@ -0,0 +1,112 @@
package org.ssssssss.magicapi.controller;
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.config.MagicConfiguration;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.model.ApiInfo;
import org.ssssssss.magicapi.model.JsonBean;
import org.ssssssss.magicapi.provider.ApiServiceProvider;
import org.ssssssss.magicapi.provider.MagicAPIService;
import java.util.List;
/**
* 接口相关操作
*/
public class MagicAPIController extends MagicController implements MagicExceptionHandler {
private final ApiServiceProvider apiServiceProvider;
private final MagicAPIService magicAPIService;
public MagicAPIController(MagicConfiguration configuration) {
super(configuration);
this.apiServiceProvider = configuration.getApiServiceProvider();
this.magicAPIService = configuration.getMagicAPIService();
}
/**
* 删除接口
*
* @param id 接口ID
*/
@RequestMapping("/delete")
@ResponseBody
@Valid(readonly = false, authorization = Authorization.DELETE)
public JsonBean<Boolean> delete(String id) {
return new JsonBean<>(magicAPIService.deleteApi(id));
}
/**
* 查询所有接口
*/
@RequestMapping("/list")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<List<ApiInfo>> list() {
return new JsonBean<>(magicAPIService.apiList());
}
/**
* 查询接口详情
*
* @param id 接口ID
*/
@RequestMapping("/get")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<ApiInfo> get(String id) {
return new JsonBean<>(magicAPIService.getApiInfo(id));
}
/**
* 查询历史记录
*
* @param id 接口ID
*/
@RequestMapping("/backups")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<List<Long>> backups(String id) {
return new JsonBean<>(apiServiceProvider.backupList(id));
}
/**
* 获取历史记录
*
* @param id 接口ID
* @param timestamp 时间点
*/
@RequestMapping("/backup/get")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<ApiInfo> backups(String id, Long timestamp) {
return new JsonBean<>(apiServiceProvider.backupInfo(id, timestamp));
}
/**
* 移动接口
*/
@RequestMapping("/api/move")
@ResponseBody
@Valid(readonly = false, authorization = Authorization.SAVE)
public JsonBean<Boolean> apiMove(String id, String groupId) {
return new JsonBean<>(magicAPIService.moveApi(id, groupId));
}
/**
* 保存接口
*
* @param info 接口信息
*/
@RequestMapping("/save")
@ResponseBody
@Valid(readonly = false, authorization = Authorization.SAVE)
public JsonBean<String> save(@RequestBody ApiInfo info) {
return new JsonBean<>(magicAPIService.saveApi(info));
}
}

View File

@@ -0,0 +1,52 @@
package org.ssssssss.magicapi.controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.config.MagicConfiguration;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.exception.InvalidArgumentException;
import org.ssssssss.magicapi.exception.MagicLoginException;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.interceptor.MagicUser;
import org.ssssssss.magicapi.model.Constants;
import org.ssssssss.magicapi.model.JsonBean;
import org.ssssssss.magicapi.model.JsonCodeConstants;
import javax.servlet.http.HttpServletRequest;
public class MagicController implements JsonCodeConstants {
MagicConfiguration configuration;
MagicController(MagicConfiguration configuration) {
this.configuration = configuration;
}
public void doValid(HttpServletRequest 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(HttpServletRequest request, Authorization authorization) {
if (authorization == null) {
return true;
}
MagicUser magicUser = (MagicUser) request.getAttribute(Constants.ATTRIBUTE_MAGIC_USER);
return configuration.getAuthorizationInterceptor().allowVisit(magicUser, request, authorization);
}
@ExceptionHandler(MagicLoginException.class)
@ResponseBody
public JsonBean<Void> invalidLogin(MagicLoginException exception) {
return new JsonBean<>(-1, exception.getMessage());
}
}

View File

@@ -0,0 +1,217 @@
package org.ssssssss.magicapi.controller;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.apache.commons.lang3.StringUtils;
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.jdbc.datasource.DataSourceUtils;
import org.springframework.util.ClassUtils;
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.adapter.Resource;
import org.ssssssss.magicapi.config.MagicConfiguration;
import org.ssssssss.magicapi.config.MagicDynamicDataSource;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.exception.InvalidArgumentException;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.model.Constants;
import org.ssssssss.magicapi.model.JsonBean;
import org.ssssssss.magicapi.utils.IoUtils;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.script.functions.ObjectConvertExtension;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MagicDataSourceController extends MagicController implements MagicExceptionHandler {
private static final ClassLoader classLoader = MagicDataSourceController.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"};
private final Resource resource;
public MagicDataSourceController(MagicConfiguration configuration) {
super(configuration);
resource = configuration.getWorkspace().getDirectory(Constants.PATH_DATASOURCE);
if (!resource.exists()) {
resource.mkdir();
}
}
/**
* 查询数据源列表
*/
@RequestMapping("/datasource/list")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<List<Map<String, Object>>> list() {
List<Map<String, Object>> list = configuration.getMagicDynamicDataSource().datasourceNodes().stream().map(it -> {
Map<String, Object> row = new HashMap<>();
row.put("id", it.getId()); // id为空的则认为是不可修改的
row.put("key", it.getKey()); // 如果为null 说明是主数据源
row.put("name", it.getName());
return row;
}).collect(Collectors.toList());
return new JsonBean<>(list);
}
@RequestMapping("/datasource/test")
@ResponseBody
public JsonBean<String> test(@RequestBody Map<String, String> properties) {
DataSource dataSource = null;
try {
dataSource = createDataSource(properties);
Connection connection = dataSource.getConnection();
DataSourceUtils.doCloseConnection(connection, dataSource);
} catch (Exception e) {
return new JsonBean<>(e.getMessage());
} finally {
IoUtils.closeDataSource(dataSource);
}
return new JsonBean<>();
}
/**
* 保存数据源
*
* @param properties 数据源配置信息
*/
@RequestMapping("/datasource/save")
@Valid(readonly = false, authorization = Authorization.DATASOURCE_SAVE)
@ResponseBody
public JsonBean<String> save(@RequestBody Map<String, String> properties) {
String key = properties.get("key");
// 校验key是否符合规则
notBlank(key, DATASOURCE_KEY_REQUIRED);
isTrue(IoUtils.validateFileName(key), DATASOURCE_KEY_INVALID);
String name = properties.getOrDefault("name", key);
String id = properties.get("id");
Stream<String> keyStream;
if (StringUtils.isBlank(id)) {
keyStream = configuration.getMagicDynamicDataSource().datasources().stream();
} else {
keyStream = configuration.getMagicDynamicDataSource().datasourceNodes().stream()
.filter(it -> !id.equals(it.getId()))
.map(MagicDynamicDataSource.DataSourceNode::getKey);
}
String dsId = StringUtils.isBlank(id) ? UUID.randomUUID().toString().replace("-", "") : id;
// 验证是否有冲突
isTrue(keyStream.noneMatch(key::equals), DATASOURCE_KEY_EXISTS);
int maxRows = ObjectConvertExtension.asInt(properties.get("maxRows"), -1);
properties.remove("id");
// 注册数据源
configuration.getMagicDynamicDataSource().put(dsId, key, name, createDataSource(properties), maxRows);
properties.put("id", dsId);
resource.getResource(dsId + ".json").write(JsonUtils.toJsonString(properties));
return new JsonBean<>(dsId);
}
/**
* 删除数据源
*
* @param id 数据源ID
*/
@RequestMapping("/datasource/delete")
@Valid(readonly = false, authorization = Authorization.DATASOURCE_DELETE)
@ResponseBody
public JsonBean<Boolean> delete(String id) {
// 查询数据源是否存在
Optional<MagicDynamicDataSource.DataSourceNode> dataSourceNode = configuration.getMagicDynamicDataSource().datasourceNodes().stream()
.filter(it -> id.equals(it.getId()))
.findFirst();
isTrue(dataSourceNode.isPresent(), DATASOURCE_NOT_FOUND);
Resource resource = this.resource.getResource(id + ".json");
// 删除数据源
isTrue(resource.delete(), DATASOURCE_NOT_FOUND);
// 取消注册数据源
dataSourceNode.ifPresent(it -> configuration.getMagicDynamicDataSource().delete(it.getKey()));
return new JsonBean<>(true);
}
@RequestMapping("/datasource/detail")
@Valid(authorization = Authorization.DATASOURCE_VIEW)
@ResponseBody
public JsonBean<Object> detail(String id) {
Resource resource = this.resource.getResource(id + ".json");
byte[] bytes = resource.read();
isTrue(bytes != null && bytes.length > 0, DATASOURCE_NOT_FOUND);
return new JsonBean<>(JsonUtils.readValue(bytes, LinkedHashMap.class));
}
// 启动之后注册数据源
public void registerDataSource() {
resource.readAll();
List<Resource> resources = resource.files(".json");
// 删除旧的数据源
configuration.getMagicDynamicDataSource().datasourceNodes().stream()
.filter(it -> it.getId() != null)
.map(MagicDynamicDataSource.DataSourceNode::getKey)
.collect(Collectors.toList())
.forEach(it -> configuration.getMagicDynamicDataSource().delete(it));
TypeFactory factory = TypeFactory.defaultInstance();
for (Resource item : resources) {
Map<String, String> properties = JsonUtils.readValue(item.read(), factory.constructMapType(HashMap.class, String.class, String.class));
if (properties != null) {
String key = properties.get("key");
String name = properties.getOrDefault("name", key);
String dsId = properties.remove("id");
int maxRows = ObjectConvertExtension.asInt(properties.get("maxRows"), -1);
configuration.getMagicDynamicDataSource().put(dsId, key, name, createDataSource(properties), maxRows);
}
}
}
// copy from DataSourceBuilder
private DataSource createDataSource(Map<String, String> properties) {
Class<? extends DataSource> dataSourceType = getDataSourceType(properties.get("type"));
if (!properties.containsKey("driverClassName")
&& properties.containsKey("url")) {
String url = properties.get("url");
String driverClass = DatabaseDriver.fromJdbcUrl(url).getDriverClassName();
properties.put("driverClassName", driverClass);
}
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 e) {
throw new InvalidArgumentException(DATASOURCE_TYPE_NOT_FOUND.format(datasourceType));
}
}
for (String name : DATA_SOURCE_TYPE_NAMES) {
try {
return (Class<? extends DataSource>) ClassUtils.forName(name, classLoader);
} catch (Exception ignored) {
}
}
throw new InvalidArgumentException(DATASOURCE_TYPE_NOT_SET);
}
}

View File

@@ -0,0 +1,26 @@
package org.ssssssss.magicapi.controller;
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.exception.InvalidArgumentException;
import org.ssssssss.magicapi.model.JsonBean;
public interface MagicExceptionHandler {
Logger logger = LoggerFactory.getLogger(MagicExceptionHandler.class);
@ExceptionHandler(Exception.class)
@ResponseBody
default Object exceptionHandler(Exception e) {
logger.error("magic-api调用接口出错", e);
return new JsonBean<>(-1, e.getMessage());
}
@ExceptionHandler(InvalidArgumentException.class)
@ResponseBody
default Object exceptionHandler(InvalidArgumentException e) {
return new JsonBean<>(e.getCode(), e.getMessage());
}
}

View File

@@ -0,0 +1,77 @@
package org.ssssssss.magicapi.controller;
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.config.MagicConfiguration;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.model.FunctionInfo;
import org.ssssssss.magicapi.model.JsonBean;
import org.ssssssss.magicapi.provider.FunctionServiceProvider;
import org.ssssssss.magicapi.provider.MagicAPIService;
import java.util.List;
public class MagicFunctionController extends MagicController implements MagicExceptionHandler {
private final FunctionServiceProvider functionService;
private final MagicAPIService magicAPIService;
public MagicFunctionController(MagicConfiguration configuration) {
super(configuration);
this.functionService = configuration.getFunctionServiceProvider();
this.magicAPIService = configuration.getMagicAPIService();
}
@RequestMapping("/function/list")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<List<FunctionInfo>> list() {
return new JsonBean<>(magicAPIService.functionList());
}
@RequestMapping("/function/get")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<FunctionInfo> get(String id) {
return new JsonBean<>(magicAPIService.getFunctionInfo(id));
}
@RequestMapping("/function/backup/get")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<FunctionInfo> backups(String id, Long timestamp) {
return new JsonBean<>(functionService.backupInfo(id, timestamp));
}
@RequestMapping("/function/backups")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<List<Long>> backups(String id) {
return new JsonBean<>(functionService.backupList(id));
}
@RequestMapping("/function/move")
@ResponseBody
@Valid(readonly = false, authorization = Authorization.SAVE)
public JsonBean<Boolean> move(String id, String groupId) {
return new JsonBean<>(magicAPIService.moveFunction(id, groupId));
}
@RequestMapping("/function/save")
@ResponseBody
@Valid(readonly = false, authorization = Authorization.SAVE)
public JsonBean<String> save(@RequestBody FunctionInfo functionInfo) {
return new JsonBean<>(magicAPIService.saveFunction(functionInfo));
}
@RequestMapping("/function/delete")
@ResponseBody
@Valid(readonly = false, authorization = Authorization.DELETE)
public JsonBean<Boolean> delete(String id) {
return new JsonBean<>(magicAPIService.deleteFunction(id));
}
}

View File

@@ -0,0 +1,66 @@
package org.ssssssss.magicapi.controller;
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.config.MagicConfiguration;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.model.Group;
import org.ssssssss.magicapi.model.JsonBean;
import org.ssssssss.magicapi.provider.MagicAPIService;
import java.util.List;
public class MagicGroupController extends MagicController implements MagicExceptionHandler {
private final MagicAPIService magicAPIService;
public MagicGroupController(MagicConfiguration configuration) {
super(configuration);
this.magicAPIService = configuration.getMagicAPIService();
}
/**
* 删除分组
*/
@RequestMapping("/group/delete")
@ResponseBody
@Valid(readonly = false, authorization = Authorization.DELETE)
public JsonBean<Boolean> deleteGroup(String groupId) {
return new JsonBean<>(magicAPIService.deleteGroup(groupId));
}
/**
* 修改分组
*/
@RequestMapping("/group/update")
@ResponseBody
@Valid(readonly = false, authorization = Authorization.SAVE)
public synchronized JsonBean<Boolean> groupUpdate(@RequestBody Group group) {
if (magicAPIService.updateGroup(group)) {
return new JsonBean<>(true);
}
return new JsonBean<>(GROUP_CONFLICT);
}
/**
* 查询所有分组
*/
@RequestMapping("/group/list")
@ResponseBody
@Valid(authorization = Authorization.VIEW)
public JsonBean<List<Group>> groupList(String type) {
return new JsonBean<>(magicAPIService.groupList(type));
}
/**
* 创建分组
*/
@RequestMapping("/group/create")
@ResponseBody
@Valid(readonly = false, authorization = Authorization.SAVE)
public JsonBean<String> createGroup(@RequestBody Group group) {
return new JsonBean<>(magicAPIService.createGroup(group));
}
}

View File

@@ -0,0 +1,289 @@
package org.ssssssss.magicapi.controller;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.adapter.resource.ZipResource;
import org.ssssssss.magicapi.config.MagicConfiguration;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.exception.MagicLoginException;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.interceptor.MagicUser;
import org.ssssssss.magicapi.logging.MagicLoggerContext;
import org.ssssssss.magicapi.model.*;
import org.ssssssss.magicapi.modules.ResponseModule;
import org.ssssssss.magicapi.modules.SQLModule;
import org.ssssssss.magicapi.provider.GroupServiceProvider;
import org.ssssssss.magicapi.provider.MagicAPIService;
import org.ssssssss.magicapi.provider.StoreServiceProvider;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.magicapi.utils.PathUtils;
import org.ssssssss.script.MagicResourceLoader;
import org.ssssssss.script.MagicScriptEngine;
import org.ssssssss.script.ScriptClass;
import org.ssssssss.script.parsing.Span;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
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);
public MagicWorkbenchController(MagicConfiguration configuration) {
super(configuration);
// 给前端添加代码提示
MagicScriptEngine.addScriptClass(SQLModule.class);
MagicScriptEngine.addScriptClass(MagicAPIService.class);
}
/**
* 获取所有class
*/
@RequestMapping("/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 类名
*/
@RequestMapping("/class")
@ResponseBody
public JsonBean<List<ScriptClass>> clazz(String className) {
return new JsonBean<>(MagicScriptEngine.getScriptClass(className));
}
/**
* 登录
*/
@RequestMapping("/login")
@ResponseBody
@Valid(requireLogin = false)
public JsonBean<Boolean> login(String username, String password, HttpServletRequest request, HttpServletResponse 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);
}
/**
* 创建控制台输出
*/
@RequestMapping("/console")
@Valid(requireLogin = false)
public SseEmitter console() throws IOException {
String sessionId = UUID.randomUUID().toString().replace("-", "");
SseEmitter emitter = MagicLoggerContext.createEmitter(sessionId);
emitter.send(SseEmitter.event().data(sessionId).name("create"));
return emitter;
}
@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()));
}
@RequestMapping("/search")
@ResponseBody
public JsonBean<List<Map<String, Object>>> search(String keyword, String type) {
if (StringUtils.isBlank(keyword)) {
return new JsonBean<>(Collections.emptyList());
}
List<MagicEntity> entities = new ArrayList<>();
if (!"2".equals(type)) {
entities.addAll(configuration.getMappingHandlerMapping().getApiInfos());
}
if (!"1".equals(type)) {
entities.addAll(configuration.getMagicFunctionManager().getFunctionInfos());
}
return new JsonBean<>(entities.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());
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());
put("type", it instanceof ApiInfo ? 1 : 2);
}
};
}).collect(Collectors.toList()));
}
@RequestMapping(value = "/config-js")
@ResponseBody
@Valid(requireLogin = false)
public ResponseEntity<?> configJs() {
ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok().contentType(MediaType.parseMediaType("application/javascript"));
if (configuration.getEditorConfig() != null) {
try {
String path = configuration.getEditorConfig();
if (path.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
path = path.substring(ResourceUtils.CLASSPATH_URL_PREFIX.length());
return responseBuilder.body(new InputStreamResource(new ClassPathResource(path).getInputStream()));
}
File file = ResourceUtils.getFile(configuration.getEditorConfig());
return responseBuilder.body(Files.readAllBytes(Paths.get(file.toURI())));
} catch (IOException e) {
logger.warn("读取编辑器配置文件{}失败", configuration.getEditorConfig());
}
}
return responseBuilder.body("var MAGIC_EDITOR_CONFIG = {}".getBytes());
}
@RequestMapping("/download")
@Valid(authorization = Authorization.DOWNLOAD)
@ResponseBody
public ResponseEntity<?> download(String groupId) throws IOException {
if (StringUtils.isBlank(groupId)) {
return download(configuration.getWorkspace(), "magic-api-all.zip");
} else {
Resource resource = configuration.getGroupServiceProvider().getGroupResource(groupId);
notNull(resource, GROUP_NOT_FOUND);
return download(resource, "magic-api-group.zip");
}
}
@RequestMapping("/upload")
@Valid(readonly = false, authorization = Authorization.UPLOAD)
@ResponseBody
public JsonBean<Boolean> upload(MultipartFile file, String mode) throws IOException {
notNull(file, FILE_IS_REQUIRED);
ZipResource root = new ZipResource(file.getInputStream());
Set<String> apiPaths = new HashSet<>();
Set<String> functionPaths = new HashSet<>();
Set<Group> groups = new HashSet<>();
Set<ApiInfo> apiInfos = new HashSet<>();
Set<FunctionInfo> functionInfos = new HashSet<>();
// 检查上传资源中是否有冲突
isTrue(readPaths(groups, apiPaths, functionPaths, apiInfos, functionInfos, "/", root), UPLOAD_PATH_CONFLICT);
// 判断是否是强制上传
if (!"force".equals(mode)) {
// 检测与已注册的接口和函数是否有冲突
isTrue(!configuration.getMappingHandlerMapping().hasRegister(apiPaths), UPLOAD_PATH_CONFLICT.format("接口"));
isTrue(!configuration.getMagicFunctionManager().hasRegister(apiPaths), UPLOAD_PATH_CONFLICT.format("函数"));
}
Resource item = root.getResource("group.json");
GroupServiceProvider groupServiceProvider = configuration.getGroupServiceProvider();
if (item.exists()) {
Group group = groupServiceProvider.readGroup(item);
// 检查分组是否存在
isTrue("0".equals(group.getParentId()) || groupServiceProvider.getGroupResource(group.getParentId()).exists(), GROUP_NOT_FOUND);
groups.removeIf(it -> it.getId().equalsIgnoreCase(group.getId()));
}
for (Group group : groups) {
Resource groupResource = groupServiceProvider.getGroupResource(group.getId());
if (groupResource != null && groupResource.exists()) {
groupServiceProvider.update(group);
} else {
groupServiceProvider.insert(group);
}
}
Resource backups = configuration.getWorkspace().getDirectory(Constants.PATH_BACKUPS);
// 保存
write(configuration.getApiServiceProvider(), backups, apiInfos);
write(configuration.getFunctionServiceProvider(), backups, functionInfos);
// 重新注册
configuration.getMappingHandlerMapping().registerAllMapping();
configuration.getMagicFunctionManager().registerAllFunction();
return new JsonBean<>(SUCCESS, true);
}
private <T extends MagicEntity> void write(StoreServiceProvider<T> provider, Resource backups, Set<T> infos) {
for (T info : infos) {
Resource resource = configuration.getGroupServiceProvider().getGroupResource(info.getGroupId());
resource = resource.getResource(info.getName() + ".ms");
byte[] content = provider.serialize(info);
resource.write(content);
Resource directory = backups.getDirectory(info.getId());
if (!directory.exists()) {
directory.mkdir();
}
directory.getResource(System.currentTimeMillis() + ".ms").write(content);
resource.write(content);
}
}
private boolean readPaths(Set<Group> groups, Set<String> apiPaths, Set<String> functionPaths, Set<ApiInfo> apiInfos, Set<FunctionInfo> functionInfos, String parentPath, Resource root) {
Resource resource = root.getResource("group.json");
String path = "";
if (resource.exists()) {
Group group = JsonUtils.readValue(resource.read(), Group.class);
groups.add(group);
path = Objects.toString(group.getPath(), "");
boolean isApi = Constants.GROUP_TYPE_API.equals(group.getType());
for (Resource file : root.files(".ms")) {
boolean conflict;
if (isApi) {
ApiInfo info = configuration.getApiServiceProvider().deserialize(file.read());
apiInfos.add(info);
conflict = !apiPaths.add(Objects.toString(info.getMethod(), "GET") + ":" + PathUtils.replaceSlash(parentPath + "/" + path + "/" + info.getPath()));
} else {
FunctionInfo info = configuration.getFunctionServiceProvider().deserialize(file.read());
functionInfos.add(info);
conflict = !functionPaths.add(PathUtils.replaceSlash(parentPath + "/" + path + "/" + info.getPath()));
}
if (conflict) {
return false;
}
}
}
for (Resource directory : root.dirs()) {
if (!readPaths(groups, apiPaths, functionPaths, apiInfos, functionInfos, PathUtils.replaceSlash(parentPath + "/" + path), directory)) {
return false;
}
}
return true;
}
private ResponseEntity<?> download(Resource resource, String filename) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
resource.export(os, Constants.PATH_BACKUPS);
return ResponseModule.download(os.toByteArray(), filename);
}
}

View File

@@ -0,0 +1,503 @@
package org.ssssssss.magicapi.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import org.springframework.core.io.InputStreamSource;
import org.springframework.http.HttpHeaders;
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.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.ssssssss.magicapi.config.MagicConfiguration;
import org.ssssssss.magicapi.config.MappingHandlerMapping;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.context.CookieContext;
import org.ssssssss.magicapi.context.RequestContext;
import org.ssssssss.magicapi.context.SessionContext;
import org.ssssssss.magicapi.interceptor.RequestInterceptor;
import org.ssssssss.magicapi.logging.LogInfo;
import org.ssssssss.magicapi.logging.MagicLoggerContext;
import org.ssssssss.magicapi.model.*;
import org.ssssssss.magicapi.modules.ResponseModule;
import org.ssssssss.magicapi.provider.ResultProvider;
import org.ssssssss.magicapi.script.ScriptManager;
import org.ssssssss.magicapi.utils.PatternUtils;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import static org.ssssssss.magicapi.model.Constants.*;
public class RequestHandler extends MagicController {
private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class);
private final ResultProvider resultProvider;
public RequestHandler(MagicConfiguration configuration) {
super(configuration);
this.resultProvider = configuration.getResultProvider();
}
/**
* 测试入口、实际请求入口
*/
@ResponseBody
@Valid(requireLogin = false) // 无需验证是否要登录
public Object invoke(HttpServletRequest request, HttpServletResponse response,
@PathVariable(required = false) Map<String, Object> pathVariables,
@RequestParam(required = false) Map<String, Object> parameters) throws Throwable {
RequestEntity requestEntity = new RequestEntity(request, response, isRequestedFromTest(request), parameters, pathVariables);
if (requestEntity.isRequestedFromTest()) {
response.setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_TRUE);
response.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HEADER_RESPONSE_WITH_MAGIC_API);
}
if (requestEntity.getApiInfo() == null) {
logger.error("{}找不到对应接口", request.getRequestURI());
return buildResult(requestEntity, API_NOT_FOUND, "接口不存在");
}
// 验证
Object value = doValidate(requestEntity, "参数", requestEntity.getApiInfo().getParameters(), parameters);
if (value != null) {
return requestEntity.isRequestedFromTest() ? new JsonBean<>(PARAMETER_INVALID, value) : value;
}
Map<String, Object> headers = new HashMap<String, Object>() {
@Override
public Object get(Object key) {
return request.getHeader(key.toString());
}
};
requestEntity.setHeaders(headers);
// 验证 header
value = doValidate(requestEntity, "header", requestEntity.getApiInfo().getHeaders(), headers);
if (value != null) {
return requestEntity.isRequestedFromTest() ? new JsonBean<>(HEADER_INVALID, value) : value;
}
List<Path> paths = new ArrayList<>(requestEntity.getApiInfo().getPaths());
MappingHandlerMapping.findGroups(requestEntity.getApiInfo().getGroupId())
.stream()
.flatMap(it -> it.getPaths().stream())
.filter(it -> !paths.contains(it))
.forEach(paths::add);
// 验证 path
value = doValidate(requestEntity, "path", paths, requestEntity.getPathVariables());
if (value != null) {
return requestEntity.isRequestedFromTest() ? new JsonBean<>(PATH_VARIABLE_INVALID, value) : value;
}
MagicScriptContext context = createMagicScriptContext(requestEntity);
requestEntity.setMagicScriptContext(context);
RequestContext.setRequestEntity(requestEntity);
// 执行前置拦截器
if ((value = doPreHandle(requestEntity)) != null) {
if (requestEntity.isRequestedFromTest()) {
// 修正前端显示,当拦截器返回时,原样输出显示
response.setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
}
return value;
}
if (requestEntity.isRequestedFromTest()) {
return isRequestedFromContinue(request) ? invokeContinueRequest(requestEntity) : invokeTestRequest(requestEntity);
}
return invokeRequest(requestEntity);
}
private Object buildResult(RequestEntity requestEntity, JsonCode code, Object data) {
return resultProvider.buildResult(requestEntity, code.getCode(), code.getMessage(), data);
}
private <T extends BaseDefinition> Object doValidate(RequestEntity requestEntity, String comment, List<T> validateParameters, Map<String, Object> parameters) {
for (BaseDefinition parameter : validateParameters) {
if (StringUtils.isNotBlank(parameter.getName())) {
String requestValue = StringUtils.defaultIfBlank(Objects.toString(parameters.get(parameter.getName()), EMPTY), Objects.toString(parameter.getDefaultValue(), EMPTY));
if (StringUtils.isBlank(requestValue)) {
if (!parameter.isRequired()) {
continue;
}
return resultProvider.buildResult(requestEntity, RESPONSE_CODE_INVALID, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]为必填项", comment, parameter.getName())));
}
try {
Object value = convertValue(parameter.getDataType(), parameter.getName(), requestValue);
if (VALIDATE_TYPE_PATTERN.equals(parameter.getValidateType())) { // 正则验证
String expression = parameter.getExpression();
if (StringUtils.isNotBlank(expression) && !PatternUtils.match(Objects.toString(value, EMPTY), expression)) {
return resultProvider.buildResult(requestEntity, RESPONSE_CODE_INVALID, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]不满足正则表达式", comment, parameter.getName())));
}
}
parameters.put(parameter.getName(), value);
} catch (Exception e) {
return resultProvider.buildResult(requestEntity, RESPONSE_CODE_INVALID, 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);
// 设置自身变量
context.set(EXPRESSION_DEFAULT_VAR_NAME, parameters.get(parameter.getName()));
if (!BooleanLiteral.isTrue(ScriptManager.executeExpression(parameter.getExpression(), context))) {
return resultProvider.buildResult(requestEntity, RESPONSE_CODE_INVALID, StringUtils.defaultIfBlank(parameter.getError(), String.format("%s[%s]不满足表达式", comment, parameter.getName())));
}
}
return null;
}
/**
* 转换参数类型
*/
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);
} 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();
}
}
private Object invokeContinueRequest(RequestEntity requestEntity) throws Exception {
HttpServletRequest request = requestEntity.getRequest();
String sessionId = getRequestedSessionId(request);
MagicScriptDebugContext context = MagicScriptDebugContext.getDebugContext(sessionId);
if (context == null) {
return new JsonBean<>(DEBUG_SESSION_NOT_FOUND, buildResult(requestEntity, DEBUG_SESSION_NOT_FOUND, null));
}
// 重置断点
context.setBreakpoints(getRequestedBreakpoints(request));
// 步进
context.setStepInto(CONST_STRING_TRUE.equalsIgnoreCase(request.getHeader(HEADER_REQUEST_STEP_INTO)));
try {
context.singal(); //等待语句执行到断点或执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
if (context.isRunning()) { //判断是否执行完毕
return new JsonBodyBean<>(1000, context.getId(), resultProvider.buildResult(requestEntity, 1000, context.getId()), context.getDebugInfo());
} else if (context.isException()) {
return resolveThrowable(requestEntity, (Throwable) context.getReturnValue());
}
Object value = context.getReturnValue();
// 执行后置拦截器
if ((value = doPostHandle(requestEntity, value)) != null) {
// 修正前端显示,当拦截器返回时,原样输出显示
requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
// 后置拦截器不包裹
return value;
}
return convertResult(requestEntity, context.getReturnValue());
}
private Object invokeTestRequest(RequestEntity requestEntity) {
try {
// 初始化debug操作
MagicScriptDebugContext context = initializeDebug(requestEntity);
Object result = ScriptManager.executeScript(requestEntity.getApiInfo().getScript(), requestEntity.getMagicScriptContext());
if (context.isRunning()) {
return new JsonBodyBean<>(1000, context.getId(), resultProvider.buildResult(requestEntity, 1000, context.getId(), result), result);
} else if (context.isException()) { //判断是否出现异常
return resolveThrowable(requestEntity, (Throwable) context.getReturnValue());
}
Object value = result;
// 执行后置拦截器
if ((value = doPostHandle(requestEntity, value)) != null) {
// 修正前端显示,当拦截器返回时,原样输出显示
requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
// 后置拦截器不包裹
return value;
}
return convertResult(requestEntity, result);
} catch (Exception e) {
return resolveThrowable(requestEntity, e);
}
}
private Object invokeRequest(RequestEntity requestEntity) throws Throwable {
HttpServletRequest request = requestEntity.getRequest();
try {
Object result = ScriptManager.executeScript(requestEntity.getApiInfo().getScript(), requestEntity.getMagicScriptContext());
Object value = result;
// 执行后置拦截器
if ((value = doPostHandle(requestEntity, value)) != null) {
return value;
}
// 对返回结果包装处理
return response(requestEntity, result);
} catch (Throwable root) {
Throwable parent = root;
do {
if (parent instanceof MagicScriptAssertException) {
MagicScriptAssertException sae = (MagicScriptAssertException) parent;
return resultProvider.buildResult(requestEntity, sae.getCode(), sae.getMessage());
}
} while ((parent = parent.getCause()) != null);
if (configuration.isThrowException()) {
throw root;
}
logger.error("接口{}请求出错", request.getRequestURI(), root);
return resultProvider.buildResult(requestEntity, RESPONSE_CODE_EXCEPTION, "系统内部出现错误");
} finally {
RequestContext.remove();
}
}
/**
* 转换请求结果
*/
private Object convertResult(RequestEntity requestEntity, Object result) throws IOException {
if (result instanceof ResponseEntity) {
ResponseEntity<?> entity = (ResponseEntity<?>) result;
List<String> headers = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : entity.getHeaders().entrySet()) {
String key = entry.getKey();
for (String value : entry.getValue()) {
headers.add(HEADER_PREFIX_FOR_TEST + key);
requestEntity.getResponse().addHeader(HEADER_PREFIX_FOR_TEST + key, value);
}
}
headers.add(HEADER_RESPONSE_WITH_MAGIC_API);
// 允许前端读取自定义的header跨域情况
requestEntity.getResponse().setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, String.join(",", headers));
if (entity.getHeaders().isEmpty()) {
return ResponseEntity.ok(new JsonBean<>(entity.getBody()));
}
return ResponseEntity.ok(new JsonBean<>(convertToBase64(entity.getBody())));
} else if (result instanceof ResponseModule.NullValue) {
// 对于return response.end() 的特殊处理
return new JsonBean<>(RESPONSE_CODE_SUCCESS, "empty.");
}
return new JsonBean<>(resultProvider.buildResult(requestEntity, result));
}
/**
* 将结果转为base64
*/
private String convertToBase64(Object value) throws IOException {
if (value instanceof String || value instanceof Number) {
return convertToBase64(value.toString().getBytes());
} else if (value instanceof byte[]) {
return Base64.getEncoder().encodeToString((byte[]) value);
} else if (value instanceof InputStream) {
return convertToBase64(IOUtils.toByteArray((InputStream) value));
} else if (value instanceof InputStreamSource) {
InputStreamSource iss = (InputStreamSource) value;
return convertToBase64(iss.getInputStream());
} else {
return convertToBase64(new ObjectMapper().writeValueAsString(value));
}
}
/**
* 解决异常
*/
private JsonBean<Object> resolveThrowable(RequestEntity requestEntity, Throwable root) {
MagicScriptException se = null;
Throwable parent = root;
do {
if (parent instanceof MagicScriptAssertException) {
MagicScriptAssertException sae = (MagicScriptAssertException) parent;
return new JsonBean<>(resultProvider.buildResult(requestEntity, sae.getCode(), sae.getMessage()));
}
if (parent instanceof MagicScriptException) {
se = (MagicScriptException) parent;
}
} while ((parent = parent.getCause()) != null);
logger.error("测试脚本出错", root);
if (se != null) {
Span.Line line = se.getLine();
return new JsonBodyBean<>(-1000, se.getSimpleMessage(), resultProvider.buildResult(requestEntity, -1000, se.getSimpleMessage()), line == null ? null : Arrays.asList(line.getLineNumber(), line.getEndLineNumber(), line.getStartCol(), line.getEndCol()));
}
return new JsonBean<>(-1, root.getMessage(), resultProvider.buildResult(requestEntity, RESPONSE_CODE_EXCEPTION, root.getMessage()));
}
/**
* 初始化DEBUG
*/
private MagicScriptDebugContext initializeDebug(RequestEntity requestEntity) {
MagicScriptDebugContext context = (MagicScriptDebugContext) requestEntity.getMagicScriptContext();
HttpServletRequest request = requestEntity.getRequest();
// 由于debug是开启一个新线程为了防止在子线程中无法获取request对象所以将request放在InheritableThreadLocal中。
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
String sessionId = getRequestedSessionId(request);
// 设置断点
context.setBreakpoints(getRequestedBreakpoints(request));
context.setTimeout(configuration.getDebugTimeout());
context.setId(sessionId);
// 设置相关回调,打印日志,回收资源
context.onComplete(() -> {
if (context.isException()) {
MagicLoggerContext.println(new LogInfo(Level.ERROR.name().toLowerCase(), "执行脚本出错", (Throwable) context.getReturnValue()));
}
logger.info("Close Console Session : {}", sessionId);
RequestContext.remove();
MagicLoggerContext.remove(sessionId);
});
context.onStart(() -> {
RequestContext.setRequestEntity(requestEntity);
MagicLoggerContext.SESSION.set(sessionId);
logger.info("Create Console Session : {}", sessionId);
});
return context;
}
/**
* 判断是否是测试请求
*/
private boolean isRequestedFromTest(HttpServletRequest request) {
return configuration.isEnableWeb() && request.getHeader(HEADER_REQUEST_SESSION) != null;
}
/**
* 判断是否是恢复断点
*/
private boolean isRequestedFromContinue(HttpServletRequest request) {
return request.getHeader(HEADER_REQUEST_CONTINUE) != null;
}
/**
* 获取测试sessionId
*/
private String getRequestedSessionId(HttpServletRequest request) {
return request.getHeader(HEADER_REQUEST_SESSION);
}
/**
* 获得断点
*/
private List<Integer> getRequestedBreakpoints(HttpServletRequest request) {
String breakpoints = request.getHeader(HEADER_REQUEST_BREAKPOINTS);
if (breakpoints != null) {
return Arrays.stream(breakpoints.split(","))
.map(val -> ObjectConvertExtension.asInt(val, -1))
.collect(Collectors.toList());
}
return null;
}
/**
* 读取RequestBody
*/
private Object readRequestBody(HttpServletRequest 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, new ServletServerHttpRequest(request));
}
}
} catch (HttpMessageNotReadableException ignored) {
return null;
}
}
return null;
}
/**
* 构建 MagicScriptContext
*/
private MagicScriptContext createMagicScriptContext(RequestEntity requestEntity) throws IOException {
// 构建脚本上下文
MagicScriptContext context = requestEntity.isRequestedFromTest() ? new MagicScriptDebugContext() : new MagicScriptContext();
Object wrap = requestEntity.getApiInfo().getOptionValue(Options.WRAP_REQUEST_PARAMETERS.getValue());
if (wrap != null && StringUtils.isNotBlank(wrap.toString())) {
context.set(wrap.toString(), requestEntity.getParameters());
}
context.putMapIntoContext(requestEntity.getParameters());
context.putMapIntoContext(requestEntity.getPathVariables());
context.set(VAR_NAME_COOKIE, new CookieContext(requestEntity.getRequest()));
context.set(VAR_NAME_HEADER, requestEntity.getHeaders());
context.set(VAR_NAME_SESSION, new SessionContext(requestEntity.getRequest().getSession()));
context.set(VAR_NAME_PATH_VARIABLE, requestEntity.getPathVariables());
Object requestBody = readRequestBody(requestEntity.getRequest());
if (requestBody != null) {
context.set(VAR_NAME_REQUEST_BODY, requestBody);
}
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 target;
}
}
return null;
}
/**
* 执行前置拦截器
*/
private Object doPreHandle(RequestEntity requestEntity) throws Exception {
for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
Object value = requestInterceptor.preHandle(requestEntity);
if (value != null) {
return value;
}
}
return null;
}
}

View File

@@ -0,0 +1,9 @@
package org.ssssssss.magicapi.dialect;
public class ClickhouseDialect extends MySQLDialect {
@Override
public boolean match(String jdbcUrl) {
return jdbcUrl.contains(":clickhouse:");
}
}

View File

@@ -0,0 +1,19 @@
package org.ssssssss.magicapi.dialect;
import org.ssssssss.magicapi.modules.BoundSql;
public class DB2Dialect implements Dialect {
@Override
public boolean match(String jdbcUrl) {
return jdbcUrl.contains(":db2:");
}
@Override
public String getPageSql(String sql, BoundSql boundSql, long offset, long limit) {
boundSql.addParameter(offset + 1);
boundSql.addParameter(offset + limit);
return "SELECT * FROM (SELECT TMP_PAGE.*,ROWNUMBER() OVER() AS ROW_ID FROM ( " + sql +
" ) AS TMP_PAGE) TMP_PAGE WHERE ROW_ID BETWEEN ? AND ?";
}
}

View File

@@ -0,0 +1,24 @@
package org.ssssssss.magicapi.dialect;
import org.ssssssss.magicapi.modules.BoundSql;
public interface Dialect {
/**
* 根据jdbcUrl匹配
*/
boolean match(String jdbcUrl);
/**
* 获取查总数的sql
*/
default String getCountSql(String sql) {
return "select count(1) from (" + sql + ") count_";
}
/**
* 获取分页sql
*/
String getPageSql(String sql, BoundSql boundSql, long offset, long limit);
}

View File

@@ -0,0 +1,26 @@
package org.ssssssss.magicapi.dialect;
import org.ssssssss.magicapi.modules.BoundSql;
/**
* @description: 达梦数据库方言
* @author: qijiantuoluowang
* @create: 2020-12-09 19:33
**/
public class DmDialect implements Dialect {
@Override
public boolean match(String jdbcUrl) {
return jdbcUrl.contains(":dm:");
}
@Override
public String getPageSql(String sql, BoundSql boundSql, long offset, long limit) {
limit = (offset >= 1) ? (offset + limit) : limit;
boundSql.addParameter(limit);
boundSql.addParameter(offset);
return "SELECT * FROM ( SELECT TMP.*, ROWNUM ROW_ID FROM ( " +
sql + " ) TMP WHERE ROWNUM <= ? ) WHERE ROW_ID > ?";
}
}

View File

@@ -0,0 +1,19 @@
package org.ssssssss.magicapi.dialect;
import org.ssssssss.magicapi.modules.BoundSql;
public class MySQLDialect implements Dialect {
@Override
public boolean match(String jdbcUrl) {
return jdbcUrl.contains(":mysql:") || jdbcUrl.contains(":mariadb:") || jdbcUrl.contains(":cobar:");
}
@Override
public String getPageSql(String sql, BoundSql boundSql, long offset, long limit) {
boundSql.addParameter(offset);
boundSql.addParameter(limit);
return sql + " limit ?,?";
}
}

View File

@@ -0,0 +1,21 @@
package org.ssssssss.magicapi.dialect;
import org.ssssssss.magicapi.modules.BoundSql;
public class OracleDialect implements Dialect {
@Override
public boolean match(String jdbcUrl) {
return jdbcUrl.contains(":oracle:");
}
@Override
public String getPageSql(String sql, BoundSql boundSql, long offset, long limit) {
limit = (offset >= 1) ? (offset + limit) : limit;
boundSql.addParameter(limit);
boundSql.addParameter(offset);
return "SELECT * FROM ( SELECT TMP.*, ROWNUM ROW_ID FROM ( " +
sql + " ) TMP WHERE ROWNUM <= ? ) WHERE ROW_ID > ?";
}
}

View File

@@ -0,0 +1,18 @@
package org.ssssssss.magicapi.dialect;
import org.ssssssss.magicapi.modules.BoundSql;
public class PostgreSQLDialect implements Dialect {
@Override
public boolean match(String jdbcUrl) {
return jdbcUrl.contains(":postgresql:");
}
@Override
public String getPageSql(String sql, BoundSql boundSql, long offset, long limit) {
boundSql.addParameter(limit);
boundSql.addParameter(offset);
return sql + " limit ? offset ?";
}
}

View File

@@ -0,0 +1,61 @@
package org.ssssssss.magicapi.dialect;
import org.apache.commons.lang3.StringUtils;
import org.ssssssss.magicapi.modules.BoundSql;
public class SQLServer2005Dialect implements Dialect {
@Override
public boolean match(String jdbcUrl) {
return jdbcUrl.contains(":sqlserver:");
}
@Override
public String getPageSql(String sql, BoundSql boundSql, long offset, long limit) {
StringBuilder pagingBuilder = new StringBuilder();
String orderby = getOrderByPart(sql);
String distinctStr = "";
String loweredString = sql.toLowerCase();
String sqlPartString = sql;
if (loweredString.trim().startsWith("select")) {
int index = 6;
if (loweredString.startsWith("select distinct")) {
distinctStr = "DISTINCT ";
index = 15;
}
sqlPartString = sqlPartString.substring(index);
}
pagingBuilder.append(sqlPartString);
// if no ORDER BY is specified use fake ORDER BY field to avoid errors
if (StringUtils.isEmpty(orderby)) {
orderby = "ORDER BY CURRENT_TIMESTAMP";
}
StringBuilder result = new StringBuilder();
result.append("WITH query AS (SELECT ")
.append(distinctStr)
.append("TOP 100 PERCENT ")
.append(" ROW_NUMBER() OVER (")
.append(orderby)
.append(") as __row_number__, ")
.append(pagingBuilder)
.append(") SELECT * FROM query WHERE __row_number__ BETWEEN ? AND ?")
.append(" ORDER BY __row_number__");
boundSql.addParameter(offset + 1);
boundSql.addParameter(offset + limit);
return result.toString();
}
private String getOrderByPart(String sql) {
String loweredString = sql.toLowerCase();
int orderByIndex = loweredString.indexOf("order by");
if (orderByIndex != -1) {
// if we find a new "order by" then we need to ignore
// the previous one since it was probably used for a subquery
return sql.substring(orderByIndex);
} else {
return "";
}
}
}

View File

@@ -0,0 +1,18 @@
package org.ssssssss.magicapi.dialect;
import org.ssssssss.magicapi.modules.BoundSql;
public class SQLServerDialect implements Dialect {
@Override
public boolean match(String jdbcUrl) {
return jdbcUrl.contains(":sqlserver2012:");
}
@Override
public String getPageSql(String sql, BoundSql boundSql, long offset, long limit) {
boundSql.addParameter(offset);
boundSql.addParameter(limit);
return sql + " OFFSET ? ROWS FETCH NEXT ? ROWS ONLY";
}
}

View File

@@ -0,0 +1,17 @@
package org.ssssssss.magicapi.exception;
import org.ssssssss.magicapi.model.JsonCode;
public class InvalidArgumentException extends RuntimeException {
private final JsonCode jsonCode;
public InvalidArgumentException(JsonCode jsonCode) {
super(jsonCode.getMessage());
this.jsonCode = jsonCode;
}
public int getCode() {
return jsonCode.getCode();
}
}

View File

@@ -0,0 +1,12 @@
package org.ssssssss.magicapi.exception;
public class MagicAPIException extends RuntimeException {
public MagicAPIException(String message) {
super(message);
}
public MagicAPIException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,8 @@
package org.ssssssss.magicapi.exception;
public class MagicLoginException extends Exception {
public MagicLoginException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,8 @@
package org.ssssssss.magicapi.exception;
public class MagicServiceException extends RuntimeException {
public MagicServiceException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,5 @@
package org.ssssssss.magicapi.interceptor;
public enum Authorization {
NONE, SAVE, VIEW, DELETE, DOWNLOAD, UPLOAD, DATASOURCE_SAVE, DATASOURCE_VIEW, DATASOURCE_DELETE
}

View File

@@ -0,0 +1,40 @@
package org.ssssssss.magicapi.interceptor;
import org.ssssssss.magicapi.exception.MagicLoginException;
import javax.servlet.http.HttpServletRequest;
public interface AuthorizationInterceptor {
/**
* 是否需要登录
*/
default boolean requireLogin() {
return true;
}
/**
* 根据Token获取User对象
*/
default MagicUser getUserByToken(String token) throws MagicLoginException {
return null;
}
/**
* 根据用户名,密码登录
*
* @param username 用户名
* @param password 密码
*/
default MagicUser login(String username, String password) throws MagicLoginException {
return null;
}
/**
* 是否拥有页面按钮的权限
*/
default boolean allowVisit(MagicUser magicUser, HttpServletRequest request, Authorization authorization) {
return true;
}
}

View File

@@ -0,0 +1,41 @@
package org.ssssssss.magicapi.interceptor;
import org.ssssssss.magicapi.exception.MagicLoginException;
import org.ssssssss.magicapi.utils.MD5Utils;
import java.util.Objects;
public class DefaultAuthorizationInterceptor implements AuthorizationInterceptor {
private final boolean requireLogin;
private String validToken;
private MagicUser configMagicUser;
public DefaultAuthorizationInterceptor(String username, String password) {
if (this.requireLogin = username != null && password != null) {
this.validToken = MD5Utils.encrypt(String.format("%s||%s", username, password));
this.configMagicUser = new MagicUser(username, username, this.validToken);
}
}
@Override
public boolean requireLogin() {
return this.requireLogin;
}
@Override
public MagicUser getUserByToken(String token) throws MagicLoginException {
if (requireLogin && Objects.equals(validToken, token)) {
return configMagicUser;
}
throw new MagicLoginException("token无效");
}
@Override
public MagicUser login(String username, String password) throws MagicLoginException {
if (requireLogin && Objects.equals(MD5Utils.encrypt(String.format("%s||%s", username, password)), this.validToken)) {
return configMagicUser;
}
throw new MagicLoginException("用户名或密码不正确");
}
}

View File

@@ -0,0 +1,40 @@
package org.ssssssss.magicapi.interceptor;
public class MagicUser {
private String id;
private String username;
private String token;
public MagicUser(String id, String username, String token) {
this.id = id;
this.username = username;
this.token = token;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}

View File

@@ -0,0 +1,54 @@
package org.ssssssss.magicapi.interceptor;
import org.ssssssss.magicapi.model.ApiInfo;
import org.ssssssss.magicapi.model.RequestEntity;
import org.ssssssss.script.MagicScriptContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 请求拦截器
*/
public interface RequestInterceptor {
/**
* 请求之前执行
*
* @return 当返回对象时直接将此对象返回到页面返回null时继续执行后续操作
*/
default Object preHandle(RequestEntity requestEntity) throws Exception {
return preHandle(requestEntity.getApiInfo(), requestEntity.getMagicScriptContext(), requestEntity.getRequest(), requestEntity.getResponse());
}
/**
* 请求之前执行
*
* @return 当返回对象时直接将此对象返回到页面返回null时继续执行后续操作
*/
default Object preHandle(ApiInfo info, MagicScriptContext context, HttpServletRequest request, HttpServletResponse response) throws Exception {
return null;
}
/**
* 执行完毕之后执行
*
* @param value 即将要返回到页面的值
* @return 返回到页面的对象, 当返回null时执行后续拦截器否则直接返回该值不执行后续拦截器
*/
default Object postHandle(ApiInfo info, MagicScriptContext context, Object value, HttpServletRequest request, HttpServletResponse response) throws Exception {
return null;
}
/**
* 执行完毕之后执行
*
* @param value 即将要返回到页面的值
* @return 返回到页面的对象, 当返回null时执行后续拦截器否则直接返回该值不执行后续拦截器
*/
default Object postHandle(RequestEntity requestEntity, Object value) throws Exception {
return postHandle(requestEntity.getApiInfo(), requestEntity.getMagicScriptContext(), value, requestEntity.getRequest(), requestEntity.getResponse());
}
}

View File

@@ -0,0 +1,27 @@
package org.ssssssss.magicapi.interceptor;
import org.ssssssss.magicapi.model.RequestEntity;
import org.ssssssss.magicapi.modules.BoundSql;
/**
* SQL 拦截器
*/
public interface SQLInterceptor {
/**
* 1.1.1 新增
*/
default void preHandle(BoundSql boundSql, RequestEntity requestEntity) {
preHandle(boundSql);
}
/**
* @see SQLInterceptor#preHandle(BoundSql, RequestEntity)
*/
@Deprecated
default void preHandle(BoundSql boundSql) {
}
}

View File

@@ -0,0 +1,58 @@
package org.ssssssss.magicapi.logging;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.apache.logging.log4j.core.layout.PatternLayout;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
/**
* 对接Log4j2
*/
public class Log4j2LoggerContext implements MagicLoggerContext {
@Override
public void generateAppender() {
LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration configuration = context.getConfiguration();
LoggerConfig logger = configuration.getRootLogger();
PatternLayout layout = PatternLayout.newBuilder()
.withCharset(StandardCharsets.UTF_8)
.withConfiguration(configuration)
.withPattern("%d %t %p %X{TracingMsg} %c - %m%n")
.build();
MagicLog4j2Appender appender = new MagicLog4j2Appender("Magic", logger.getFilter(), layout);
appender.start();
configuration.addAppender(appender);
logger.addAppender(appender, logger.getLevel(), logger.getFilter());
context.updateLoggers(configuration);
}
static class MagicLog4j2Appender extends AbstractAppender {
MagicLog4j2Appender(String name, Filter filter, Layout<? extends Serializable> layout) {
super(name, filter, layout, true, Property.EMPTY_ARRAY);
}
@Override
public void append(LogEvent event) {
LogInfo logInfo = new LogInfo();
logInfo.setLevel(event.getLevel().name().toLowerCase());
logInfo.setMessage(event.getMessage().getFormattedMessage());
ThrowableProxy throwableProxy = event.getThrownProxy();
if (throwableProxy != null) {
logInfo.setThrowable(throwableProxy.getThrowable());
}
MagicLoggerContext.println(logInfo);
}
}
}

View File

@@ -0,0 +1,49 @@
package org.ssssssss.magicapi.logging;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.LogManager;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.RootLogger;
import org.apache.log4j.spi.ThrowableInformation;
/**
* 对接Log4j
*/
public class Log4jLoggerContext implements MagicLoggerContext {
@Override
public void generateAppender() {
RootLogger logger = (RootLogger) LogManager.getRootLogger();
PatternLayout patternLayout = new PatternLayout("%d %p [%c] - %m%n");
MagicLog4jAppender magicLog4jAppender = new MagicLog4jAppender();
magicLog4jAppender.setLayout(patternLayout);
logger.addAppender(magicLog4jAppender);
}
static class MagicLog4jAppender extends AppenderSkeleton {
@Override
protected void append(LoggingEvent event) {
LogInfo logInfo = new LogInfo();
logInfo.setLevel(event.getLevel().toString().toLowerCase());
logInfo.setMessage(String.valueOf(event.getMessage()));
ThrowableInformation throwableInformation = event.getThrowableInformation();
if (throwableInformation != null) {
logInfo.setThrowable(throwableInformation.getThrowable());
}
MagicLoggerContext.println(logInfo);
}
@Override
public void close() {
}
@Override
public boolean requiresLayout() {
return false;
}
}
}

View File

@@ -0,0 +1,58 @@
package org.ssssssss.magicapi.logging;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* 打印的日志信息
*/
public class LogInfo {
private String level;
private String message;
private String throwable;
public LogInfo() {
}
public LogInfo(String level, String message, Throwable throwable) {
this.level = level;
this.message = message;
this.setThrowable(throwable);
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getThrowable() {
return throwable;
}
public void setThrowable(Throwable throwable) {
if (throwable != null) {
try (StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer)) {
throwable.printStackTrace(printWriter);
this.throwable = writer.toString();
} catch (IOException ignored) {
}
}
}
}

View File

@@ -0,0 +1,40 @@
package org.ssssssss.magicapi.logging;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 对接Logback
*/
public class LogbackLoggerContext implements MagicLoggerContext {
@Override
public void generateAppender() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
ch.qos.logback.classic.Logger logger = context.getLogger(Logger.ROOT_LOGGER_NAME);
MagicLogbackAppender appender = new MagicLogbackAppender();
appender.setContext(context);
appender.setName(LOGGER_NAME);
appender.start();
logger.addAppender(appender);
}
static class MagicLogbackAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent event) {
LogInfo logInfo = new LogInfo();
logInfo.setLevel(event.getLevel().levelStr.toLowerCase());
logInfo.setMessage(event.getFormattedMessage());
ThrowableProxy throwableProxy = (ThrowableProxy) event.getThrowableProxy();
if (throwableProxy != null) {
logInfo.setThrowable(throwableProxy.getThrowable());
}
MagicLoggerContext.println(logInfo);
}
}
}

View File

@@ -0,0 +1,35 @@
package org.ssssssss.magicapi.logging;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 日志管理
*/
public class LoggerManager {
private static Logger logger = LoggerFactory.getLogger(LoggerManager.class);
/**
* 创建一个新的appender至项目中用于UI界面
*/
public static MagicLoggerContext createMagicAppender() {
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
String loggerFactoryClassName = loggerFactory.getClass().getName();
MagicLoggerContext magicLoggerContext = null;
if ("ch.qos.logback.classic.LoggerContext".equalsIgnoreCase(loggerFactoryClassName)) { //logback
magicLoggerContext = new LogbackLoggerContext();
} else if ("org.apache.logging.slf4j.Log4jLoggerFactory".equalsIgnoreCase(loggerFactoryClassName)) { //log4j2
magicLoggerContext = new Log4j2LoggerContext();
} else if ("org.slf4j.impl.Log4jLoggerFactory".equalsIgnoreCase(loggerFactoryClassName)) { //log4j 1
magicLoggerContext = new Log4jLoggerContext();
}
if (magicLoggerContext == null) {
logger.error("无法识别LoggerContext:{}", loggerFactoryClassName);
} else {
magicLoggerContext.generateAppender();
}
return magicLoggerContext;
}
}

View File

@@ -0,0 +1,77 @@
package org.ssssssss.magicapi.logging;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.ssssssss.script.MagicScriptContext;
import org.ssssssss.script.MagicScriptDebugContext;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public interface MagicLoggerContext {
String LOGGER_NAME = "MagicAPI";
Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
ThreadLocal<String> SESSION = new InheritableThreadLocal<>();
/**
* 创建sseEmitter推送
*
* @param sessionId 会话id
*/
static SseEmitter createEmitter(String sessionId) {
SseEmitter sseEmitter = new SseEmitter(0L);
emitterMap.put(sessionId, sseEmitter);
return sseEmitter;
}
/**
* 删除会话
*
* @param sessionId 会话id
*/
static void remove(String sessionId) {
SseEmitter sseEmitter = emitterMap.remove(sessionId);
SESSION.remove();
if (sseEmitter != null) {
try {
sseEmitter.send(SseEmitter.event().data(sessionId).name("close"));
} catch (IOException ignored) {
}
}
}
/**
* 打印日志
*
* @param logInfo 日志信息
*/
static void println(LogInfo logInfo) {
// 获取SessionId
MagicScriptContext context = MagicScriptContext.get();
String sessionId;
if (context instanceof MagicScriptDebugContext) {
sessionId = ((MagicScriptDebugContext) context).getId();
} else {
sessionId = SESSION.get();
}
if (sessionId != null) {
SseEmitter sseEmitter = emitterMap.get(sessionId);
if (sseEmitter != null) {
try {
// 推送日志事件
sseEmitter.send(SseEmitter.event().data(logInfo).name("log"));
} catch (IOException ignored) {
}
}
}
}
/**
* 生成appender
*/
void generateAppender();
}

View File

@@ -0,0 +1,277 @@
package org.ssssssss.magicapi.model;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ssssssss.magicapi.config.MappingHandlerMapping;
import org.ssssssss.magicapi.utils.JsonUtils;
import java.util.*;
/**
* 接口信息
*/
public class ApiInfo extends MagicEntity {
/**
* 请求方法
*/
private String method = "GET";
/**
* 请求路径
*/
private String path;
/**
* 设置的请求参数
*/
private List<Parameter> parameters = Collections.emptyList();
/**
* 设置的接口选项
*/
private String option;
/**
* 请求体
*/
private String requestBody;
/**
* 请求头
*/
private List<Header> headers = Collections.emptyList();
/**
* 路径变量
*/
private List<Path> paths = Collections.emptyList();
/**
* 输出结果
*/
private String responseBody;
/**
* 接口描述
*/
private String description;
/**
* 接口选项json
*/
private transient JsonNode jsonNode;
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public void setParameter(String parameter) {
if (parameter != null) {
parameter = parameter.trim();
if (parameter.startsWith("[")) { // v0.5.0+
this.parameters = JsonUtils.readValue(Objects.toString(parameter, "[]"), new TypeReference<List<Parameter>>() {
});
} else {
Map map = JsonUtils.readValue(Objects.toString(parameter, "{}"), Map.class);
Object request = map.get("request");
if (request instanceof Map) {
Map requestMap = (Map) request;
Set keys = requestMap.keySet();
this.parameters = new ArrayList<>();
for (Object key : keys) {
this.parameters.add(new Parameter(key.toString(), Objects.toString(requestMap.get(key), "")));
}
}
Object header = map.get("header");
if (header instanceof Map) {
Map headers = (Map) header;
Set keys = headers.keySet();
this.headers = new ArrayList<>();
for (Object key : keys) {
this.headers.add(new Header(key.toString(), Objects.toString(headers.get(key), "")));
}
}
if (map.containsKey("body")) {
this.requestBody = Objects.toString(map.get("body"), null);
}
}
}
}
public String getResponseBody() {
return responseBody;
}
public void setResponseBody(String responseBody) {
this.responseBody = responseBody;
}
public String getRequestBody() {
return requestBody;
}
public void setRequestBody(String requestBody) {
this.requestBody = requestBody;
}
public List<Path> getPaths() {
return paths;
}
public void setPaths(List<Path> paths) {
this.paths = paths;
}
public Map<String, String> getOptionMap() {
Map<String, String> map = new HashMap<>();
if (this.jsonNode == null) {
return null;
} else if (this.jsonNode.isArray()) {
for (JsonNode node : this.jsonNode) {
map.put(node.get("name").asText(), node.get("value").asText());
}
} else {
this.jsonNode.fieldNames().forEachRemaining(it -> map.put(it, this.jsonNode.get(it).asText()));
}
MappingHandlerMapping.findGroups(this.groupId)
.stream()
.flatMap(it -> it.getOptions().stream())
.forEach(option -> {
if (!map.containsKey(option.getName())) {
map.put(option.getName(), option.getValue());
}
});
return map;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getOption() {
return option;
}
public void setOption(String option) {
this.option = option;
try {
this.jsonNode = new ObjectMapper().readTree(option);
} catch (Throwable ignored) {
}
}
public List<Parameter> getParameters() {
return parameters;
}
public void setParameters(List<Parameter> parameters) {
this.parameters = parameters;
}
public void setRequestHeader(String requestHeader) {
this.headers = JsonUtils.readValue(Objects.toString(requestHeader, "[]"), new TypeReference<List<Header>>() {
});
}
public List<Header> getHeaders() {
return headers;
}
public void setHeaders(List<Header> headers) {
this.headers = headers;
}
public void setOptionValue(String optionValue) {
this.setOption(optionValue);
}
public String getOptionValue(Options options) {
return getOptionValue(options.getValue());
}
public String getOptionValue(String key) {
if (this.jsonNode == null) {
return null;
}
if (this.jsonNode.isArray()) {
for (JsonNode node : this.jsonNode) {
if (node.isObject() && Objects.equals(key, node.get("name").asText())) {
return node.get("value").asText();
}
}
} else if (this.jsonNode.isObject()) {
JsonNode node = this.jsonNode.get(key);
if (node != null) {
return node.asText();
}
}
return MappingHandlerMapping.findGroups(this.groupId)
.stream()
.flatMap(it -> it.getOptions().stream())
.filter(it -> key.equals(it.getName()))
.findFirst()
.map(BaseDefinition::getValue)
.orElse(null);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ApiInfo apiInfo = (ApiInfo) o;
return Objects.equals(id, apiInfo.id) &&
Objects.equals(method, apiInfo.method) &&
Objects.equals(path, apiInfo.path) &&
Objects.equals(script, apiInfo.script) &&
Objects.equals(name, apiInfo.name) &&
Objects.equals(paths, apiInfo.paths) &&
Objects.equals(groupId, apiInfo.groupId) &&
Objects.equals(parameters, apiInfo.parameters) &&
Objects.equals(option, apiInfo.option) &&
Objects.equals(requestBody, apiInfo.requestBody) &&
Objects.equals(headers, apiInfo.headers) &&
Objects.equals(description, apiInfo.description);
}
@Override
public int hashCode() {
return Objects.hash(id, method, path, script, name, groupId, parameters, option, requestBody, headers, responseBody, description);
}
public ApiInfo copy() {
ApiInfo info = new ApiInfo();
info.setId(this.id);
info.setMethod(this.method);
info.setName(this.name);
info.setPath(this.path);
info.setScript(this.script);
info.setGroupId(this.groupId);
info.setParameters(this.parameters);
info.jsonNode = this.jsonNode;
info.setRequestBody(this.requestBody);
info.setHeaders(this.headers);
info.setResponseBody(this.responseBody);
info.setDescription(this.description);
info.setPaths(this.paths);
return info;
}
}

View File

@@ -0,0 +1,157 @@
package org.ssssssss.magicapi.model;
import java.util.Objects;
public class BaseDefinition {
/**
* 名
*/
private String name;
/**
* 值
*/
private String value;
/**
* 描述
*/
private String description;
/**
* 是否必填
*/
private boolean required;
/**
* 数据类型
*/
private DataType dataType;
/**
* 类型,函数专用
*/
private String type;
/**
* 默认值
*/
private String defaultValue;
/**
* 验证类型
*/
private String validateType;
/**
* 验证说明
*/
private String error;
/**
* 验证表达式
*/
private String expression;
public BaseDefinition() {
}
public BaseDefinition(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isRequired() {
return required;
}
public void setRequired(boolean required) {
this.required = required;
}
public DataType getDataType() {
return dataType == null ? DataType.String : dataType;
}
public void setDataType(DataType dataType) {
this.dataType = dataType;
}
public String getDefaultValue() {
return defaultValue;
}
public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}
public String getValidateType() {
return validateType;
}
public void setValidateType(String validateType) {
this.validateType = validateType;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getExpression() {
return expression;
}
public void setExpression(String expression) {
this.expression = expression;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BaseDefinition)) return false;
BaseDefinition that = (BaseDefinition) o;
return required == that.required && Objects.equals(name, that.name) && Objects.equals(value, that.value) && Objects.equals(description, that.description) && dataType == that.dataType && Objects.equals(defaultValue, that.defaultValue) && Objects.equals(validateType, that.validateType) && Objects.equals(error, that.error) && Objects.equals(expression, that.expression);
}
@Override
public int hashCode() {
return Objects.hash(name, value, description, required, dataType, defaultValue, validateType, error, expression);
}
}

View File

@@ -0,0 +1,165 @@
package org.ssssssss.magicapi.model;
public class Constants {
/**
* true 常量
*/
public static final String CONST_STRING_TRUE = "true";
/**
* false 常量
*/
public static final String CONST_STRING_FALSE = "false";
/**
* 分组类型: 接口
*/
public static final String GROUP_TYPE_API = "1";
/**
* 分组类型: 函数
*/
public static final String GROUP_TYPE_FUNCTION = "2";
/**
* 接口文件夹名
*/
public static final String PATH_API = "api";
/**
* 函数文件夹名
*/
public static final String PATH_FUNCTION = "function";
/**
* 数据源文件夹名
*/
public static final String PATH_DATASOURCE = "datasource";
/**
* 备份文件夹名
*/
public static final String PATH_BACKUPS = "backups";
/**
* 空值
*/
public static final String EMPTY = "";
/**
* 表达式验证
*/
public static final String VALIDATE_TYPE_EXPRESSION = "expression";
/**
* 正则验证
*/
public static final String VALIDATE_TYPE_PATTERN = "pattern";
/**
* 表达式验证中变量的默认名称
*/
public static final String EXPRESSION_DEFAULT_VAR_NAME = "value";
/**
* 脚本中session的变量名
*/
public static final String VAR_NAME_SESSION = "session";
/**
* 脚本中cookie的变量名
*/
public static final String VAR_NAME_COOKIE = "cookie";
/**
* 脚本中路径变量的变量名
*/
public static final String VAR_NAME_PATH_VARIABLE = "path";
/**
* 脚本中header的变量名
*/
public static final String VAR_NAME_HEADER = "header";
/**
* 脚本中RequestBody的变量名
*/
public static final String VAR_NAME_REQUEST_BODY = "body";
/**
* 脚本中RequestBody的变量名
*/
public static final String HEADER_PREFIX_FOR_TEST = "MA-";
public static final String HEADER_REQUEST_SESSION = "Magic-Request-Session";
public static final String HEADER_REQUEST_BREAKPOINTS = "Magic-Request-Breakpoints";
public static final String HEADER_REQUEST_CONTINUE = "Magic-Request-Continue";
public static final String HEADER_REQUEST_STEP_INTO = "Magic-Request-Step-Into";
public static final String HEADER_RESPONSE_WITH_MAGIC_API = "Response-With-Magic-API";
public static final String ATTRIBUTE_MAGIC_USER = "MAGIC_API_ATTRIBUTE_USER";
public static final String MAGIC_TOKEN_HEADER = "Magic-Token";
/**
* 执行成功的code值
*/
public static int RESPONSE_CODE_SUCCESS = 1;
/**
* 执行出现异常的code值
*/
public static int RESPONSE_CODE_EXCEPTION = -1;
/**
* 参数验证未通过的code值
*/
public static int RESPONSE_CODE_INVALID = 0;
/**
* 通知新增
*/
public static final int NOTIFY_ACTION_ADD = 1;
/**
* 通知修改
*/
public static final int NOTIFY_ACTION_UPDATE = 2;
/**
* 通知删除
*/
public static final int NOTIFY_ACTION_DELETE = 3;
/**
* 通知更新全部
*/
public static final int NOTIFY_ACTION_ALL = 4;
/**
* 通知接口刷新
*/
public static final int NOTIFY_ACTION_API = 1;
/**
* 通知分组刷新
*/
public static final int NOTIFY_ACTION_GROUP = 2;
/**
* 通知函数刷新
*/
public static final int NOTIFY_ACTION_FUNCTION = 3;
/**
* 通知数据源刷新
*/
public static final int NOTIFY_ACTION_DATASOURCE = 4;
}

View File

@@ -0,0 +1,72 @@
package org.ssssssss.magicapi.model;
import org.ssssssss.magicapi.modules.RequestModule;
import org.ssssssss.script.reflection.JavaInvoker;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import static org.ssssssss.script.reflection.JavaReflection.findInvoker;
public enum DataType {
String("string"),
Integer(true, findInvoker(BigDecimal.class, "intValue"), "number"),
Double(true, findInvoker(BigDecimal.class, "doubleValue"), "number"),
Long(true, findInvoker(BigDecimal.class, "longValue"), "number"),
Float(true, findInvoker(BigDecimal.class, "floatValue"), "number"),
Byte(true, findInvoker(BigDecimal.class, "byteValue"), "number"),
Short(true, findInvoker(BigDecimal.class, "shortValue"), "number"),
MultipartFile(findInvoker(RequestModule.class, "getFile", new Class<?>[]{String.class}), true, false, "file"),
MultipartFiles(findInvoker(RequestModule.class, "getFiles", new Class<?>[]{String.class}), true, false, "file");
private boolean isNumber;
private JavaInvoker<Method> invoker;
private boolean needName;
private boolean needValue;
private String javascriptType;
DataType(boolean isNumber, JavaInvoker<Method> invoker, boolean needName, boolean needValue, String javascriptType) {
this.isNumber = isNumber;
this.invoker = invoker;
this.needName = needName;
this.needValue = needValue;
this.javascriptType = javascriptType;
}
DataType(JavaInvoker<Method> invoker, boolean needName, boolean needValue, String javascriptType) {
this(false, invoker, needName, needValue, javascriptType);
}
DataType(boolean isNumber, JavaInvoker<Method> invoker, String javascriptType) {
this(invoker, false, false, javascriptType);
this.isNumber = isNumber;
}
DataType(String javascriptType) {
this.javascriptType = javascriptType;
}
public boolean isNumber() {
return isNumber;
}
public JavaInvoker<Method> getInvoker() {
return invoker;
}
public boolean isNeedName() {
return needName;
}
public boolean isNeedValue() {
return needValue;
}
public java.lang.String getJavascriptType() {
return javascriptType;
}
}

View File

@@ -0,0 +1,90 @@
package org.ssssssss.magicapi.model;
import com.fasterxml.jackson.core.type.TypeReference;
import org.ssssssss.magicapi.utils.JsonUtils;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class FunctionInfo extends MagicEntity {
private String path;
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);
}
@Override
public int hashCode() {
return Objects.hash(id, path, script, name, groupId, parameters, description, returnType);
}
}

View File

@@ -0,0 +1,91 @@
package org.ssssssss.magicapi.model;
import java.util.Collections;
import java.util.List;
public class Group {
private String id;
private String name;
private String type;
private String parentId;
private String path;
/**
* 路径变量
*/
private List<Path> paths = Collections.emptyList();
/**
* 分组选项
*/
private List<BaseDefinition> options = Collections.emptyList();
public Group(String id, String name) {
this.id = id;
this.name = name;
}
public Group() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public List<Path> getPaths() {
return paths;
}
public void setPaths(List<Path> paths) {
this.paths = paths;
}
public List<BaseDefinition> getOptions() {
return options;
}
public void setOptions(List<BaseDefinition> options) {
this.options = options;
}
}

View File

@@ -0,0 +1,11 @@
package org.ssssssss.magicapi.model;
public class Header extends BaseDefinition {
public Header() {
}
public Header(String name, String value) {
super(name, value);
}
}

View File

@@ -0,0 +1,100 @@
package org.ssssssss.magicapi.model;
/**
* 统一返回值对象
*/
public class JsonBean<T> {
/**
* 状态码
*/
private int code = 1;
/**
* 状态说明
*/
private String message = "success";
/**
* 实际数据
*/
private T data;
/**
* 服务器时间
*/
private long timestamp = System.currentTimeMillis();
private Integer executeTime;
public JsonBean(int code, String message) {
this.code = code;
this.message = message;
}
public JsonBean(int code, String message, T data, Integer executeTime) {
this(code, message, data);
this.executeTime = executeTime;
}
public JsonBean(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public JsonBean() {
}
public JsonBean(JsonCode jsonCode) {
this(jsonCode, null);
}
public JsonBean(JsonCode jsonCode, T data) {
this(jsonCode.getCode(), jsonCode.getMessage(), data);
}
public JsonBean(T data) {
this.data = data;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public Integer getExecuteTime() {
return executeTime;
}
public void setExecuteTime(Integer executeTime) {
this.executeTime = executeTime;
}
}

View File

@@ -0,0 +1,24 @@
package org.ssssssss.magicapi.model;
public class JsonBodyBean<T> extends JsonBean<T> {
private Object body;
public JsonBodyBean(int code, String message, T data, Object body) {
super(code, message, data);
this.body = body;
}
public JsonBodyBean(T data, Object body) {
super(data);
this.body = body;
}
public Object getBody() {
return body;
}
public void setBody(Object body) {
this.body = body;
}
}

View File

@@ -0,0 +1,33 @@
package org.ssssssss.magicapi.model;
public class JsonCode {
private int code;
private String message;
public JsonCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public JsonCode format(Object... args) {
return new JsonCode(this.code, String.format(this.message, args));
}
}

View File

@@ -0,0 +1,92 @@
package org.ssssssss.magicapi.model;
import org.apache.commons.lang3.StringUtils;
import org.ssssssss.magicapi.exception.InvalidArgumentException;
public interface JsonCodeConstants {
JsonCode SUCCESS = new JsonCode(1, "success");
JsonCode IS_READ_ONLY = new JsonCode(-2, "当前为只读模式,无法操作");
JsonCode PERMISSION_INVALID = new JsonCode(-10, "无权限操作.");
JsonCode GROUP_NOT_FOUND = new JsonCode(0, "找不到分组信息");
JsonCode NAME_CONFLICT = new JsonCode(0, "移动后名称会重复,请修改名称后在试。");
JsonCode REQUEST_PATH_CONFLICT = new JsonCode(0, "该路径已被映射,请换一个请求方法或路径");
JsonCode FUNCTION_PATH_CONFLICT = new JsonCode(0, "该路径已被映射,请换一个请求方法或路径");
JsonCode REQUEST_METHOD_REQUIRED = new JsonCode(0, "请求方法不能为空");
JsonCode REQUEST_PATH_REQUIRED = new JsonCode(0, "请求路径不能为空");
JsonCode FUNCTION_PATH_REQUIRED = new JsonCode(0, "函数路径不能为空");
JsonCode SCRIPT_REQUIRED = new JsonCode(0, "脚本内容不能为空");
JsonCode API_NAME_REQUIRED = new JsonCode(0, "接口名称不能为空");
JsonCode GROUP_NAME_REQUIRED = new JsonCode(0, "分组名称不能为空");
JsonCode GROUP_TYPE_REQUIRED = new JsonCode(0, "分组类型不能为空");
JsonCode FUNCTION_NAME_REQUIRED = new JsonCode(0, "函数名称不能为空");
JsonCode NAME_INVALID = new JsonCode(0, "名称不能包含特殊字符只允许中文、数字、字母以及_组合");
JsonCode DATASOURCE_KEY_INVALID = new JsonCode(0, "数据源Key不能包含特殊字符只允许中文、数字、字母以及_组合");
JsonCode API_ALREADY_EXISTS = new JsonCode(0, "接口%s:%s已存在或接口名称重复");
JsonCode FUNCTION_ALREADY_EXISTS = new JsonCode(0, "函数%s已存在或名称重复");
JsonCode API_SAVE_FAILURE = new JsonCode(0, "保存失败,请检查接口名称是否重复且不能包含特殊字符。");
JsonCode FUNCTION_SAVE_FAILURE = new JsonCode(0, "保存失败,请检查函数名称是否重复且不能包含特殊字符。");
JsonCode GROUP_SAVE_FAILURE = new JsonCode(0, "保存失败,同一组下分组名称不能重复且不能包含特殊字符。");
JsonCode GROUP_CONFLICT = new JsonCode(-20, "修改分组后,名称或路径会有冲突,请检查!");
JsonCode PARAMETER_INVALID = new JsonCode(0, "参数验证失败");
JsonCode HEADER_INVALID = new JsonCode(0, "header验证失败");
JsonCode PATH_VARIABLE_INVALID = new JsonCode(0, "路径变量验证失败");
JsonCode FILE_IS_REQUIRED = new JsonCode(0, "请上传文件");
JsonCode UPLOAD_PATH_CONFLICT = new JsonCode(0, "上传后%s路径会有冲突请检查");
JsonCode DEBUG_SESSION_NOT_FOUND = new JsonCode(0, "debug session not found!");
JsonCode API_NOT_FOUND = new JsonCode(1001, "api not found");
JsonCode DATASOURCE_KEY_REQUIRED = new JsonCode(0, "数据源Key不能为空");
JsonCode DATASOURCE_KEY_EXISTS = new JsonCode(0, "数据源%s已存在或名称重复");
JsonCode DATASOURCE_TYPE_NOT_FOUND = new JsonCode(0, "%s not found");
JsonCode DATASOURCE_NOT_FOUND = new JsonCode(0, "找不到对应的数据源");
JsonCode DATASOURCE_TYPE_NOT_SET = new JsonCode(0, "请设置数据源类型");
default void notNull(Object value, JsonCode jsonCode) {
if (value == null) {
throw new InvalidArgumentException(jsonCode);
}
}
default void isTrue(boolean value, JsonCode jsonCode) {
if (!value) {
throw new InvalidArgumentException(jsonCode);
}
}
default void notBlank(String value, JsonCode jsonCode) {
isTrue(StringUtils.isNotBlank(value), jsonCode);
}
}

View File

@@ -0,0 +1,72 @@
package org.ssssssss.magicapi.model;
public class MagicEntity implements Cloneable {
protected String id;
protected String script;
protected String groupId;
protected String name;
protected Long createTime;
protected Long updateTime;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getScript() {
return script;
}
public void setScript(String script) {
this.script = script;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
public Long getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Long updateTime) {
this.updateTime = updateTime;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public MagicEntity clone() {
try {
return (MagicEntity) super.clone();
} catch (CloneNotSupportedException e) {
return this;
}
}
}

View File

@@ -0,0 +1,70 @@
package org.ssssssss.magicapi.model;
public class MagicNotify {
/**
* 消息来源
*/
private String from;
/**
* 对应的id如接口id、函数id分组id、数据源id
*/
private String id;
/**
* 动作
*/
private int action = -1;
/**
* 操作对象,如接口、函数、分组、数据源
*/
private int type = -1;
public MagicNotify() {
}
public MagicNotify(String from) {
this(from, null, Constants.NOTIFY_ACTION_ALL, -1);
}
public MagicNotify(String from, String id, int action, int type) {
this.from = from;
this.id = id;
this.action = action;
this.type = type;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getAction() {
return action;
}
public void setAction(int action) {
this.action = action;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}

View File

@@ -0,0 +1,36 @@
package org.ssssssss.magicapi.model;
public enum Options {
WRAP_REQUEST_PARAMETERS("包装请求参数到一个变量中", "wrap_request_parameter"),
PERMISSION("允许拥有该权限的访问", "permission"),
ROLE("允许拥有该角色的访问", "role"),
REQUIRE_LOGIN("该接口需要登录才允许访问", "require_login", "true"),
ANONYMOUS("该接口需要不登录也可访问", "anonymous", "true");
private final String name;
private final String value;
private final String defaultValue;
Options(String name, String value) {
this(name, value, null);
}
Options(String name, String value, String defaultValue) {
this.name = name;
this.value = value;
this.defaultValue = defaultValue;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public String getDefaultValue() {
return defaultValue;
}
}

View File

@@ -0,0 +1,35 @@
package org.ssssssss.magicapi.model;
/**
* 分页对象
*/
public class Page {
private long limit;
private long offset;
public Page() {
}
public Page(long limit, long offset) {
this.limit = limit;
this.offset = offset;
}
public long getLimit() {
return limit;
}
public void setLimit(long limit) {
this.limit = limit;
}
public long getOffset() {
return offset;
}
public void setOffset(long offset) {
this.offset = offset;
}
}

View File

@@ -0,0 +1,43 @@
package org.ssssssss.magicapi.model;
import java.util.List;
/**
* 分页执行结果
*/
public class PageResult<T> {
/**
* 总条数
*/
private long total;
/**
* 数据项
*/
private List<T> list;
public PageResult(long total, List<T> list) {
this.total = total;
this.list = list;
}
public PageResult() {
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
}

View File

@@ -0,0 +1,11 @@
package org.ssssssss.magicapi.model;
public class Parameter extends BaseDefinition {
public Parameter() {
}
public Parameter(String name, String value) {
super(name, value);
}
}

View File

@@ -0,0 +1,16 @@
package org.ssssssss.magicapi.model;
public class Path extends BaseDefinition {
public Path() {
}
public Path(String name, String value) {
super(name, value);
}
@Override
public boolean isRequired() {
return true;
}
}

View File

@@ -0,0 +1,135 @@
package org.ssssssss.magicapi.model;
import org.ssssssss.magicapi.config.MappingHandlerMapping;
import org.ssssssss.script.MagicScriptContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
public class RequestEntity {
private ApiInfo apiInfo;
private HttpServletRequest request;
private HttpServletResponse response;
private boolean requestedFromTest;
private Map<String, Object> parameters;
private Map<String, Object> pathVariables;
private Long requestTime = System.currentTimeMillis();
private MagicScriptContext magicScriptContext;
private Map<String, Object> headers;
private RequestEntity() {
}
public RequestEntity(HttpServletRequest request, HttpServletResponse response, boolean requestedFromTest, Map<String, Object> parameters, Map<String, Object> pathVariables) {
this.request = request;
this.response = response;
this.requestedFromTest = requestedFromTest;
this.parameters = parameters;
this.pathVariables = pathVariables;
ApiInfo info = MappingHandlerMapping.getMappingApiInfo(request);
this.apiInfo = info != null ? info.copy() : null;
}
public RequestEntity(ApiInfo apiInfo, HttpServletRequest request, HttpServletResponse response, boolean requestedFromTest, Map<String, Object> parameters, Map<String, Object> pathVariables) {
this.apiInfo = apiInfo;
this.request = request;
this.response = response;
this.requestedFromTest = requestedFromTest;
this.parameters = parameters;
this.pathVariables = pathVariables;
}
public RequestEntity(HttpServletRequest request, HttpServletResponse response, boolean requestedFromTest, Map<String, Object> parameters, Map<String, Object> pathVariables, MagicScriptContext magicScriptContext, Map<String, Object> headers) {
ApiInfo info = MappingHandlerMapping.getMappingApiInfo(request);
this.apiInfo = info != null ? info.copy() : null;
this.request = request;
this.response = response;
this.requestedFromTest = requestedFromTest;
this.parameters = parameters;
this.pathVariables = pathVariables;
this.magicScriptContext = magicScriptContext;
this.headers = headers;
}
public static RequestEntity empty() {
return new RequestEntity();
}
public ApiInfo getApiInfo() {
return apiInfo;
}
public void setApiInfo(ApiInfo apiInfo) {
this.apiInfo = apiInfo;
}
public HttpServletRequest getRequest() {
return request;
}
public void setRequest(HttpServletRequest request) {
this.request = request;
}
public HttpServletResponse getResponse() {
return response;
}
public void setResponse(HttpServletResponse response) {
this.response = response;
}
public boolean isRequestedFromTest() {
return requestedFromTest;
}
public void setRequestedFromTest(boolean requestedFromTest) {
this.requestedFromTest = requestedFromTest;
}
public Map<String, Object> getParameters() {
return parameters;
}
public void setParameters(Map<String, Object> parameters) {
this.parameters = parameters;
}
public Map<String, Object> getPathVariables() {
return pathVariables;
}
public void setPathVariables(Map<String, Object> pathVariables) {
this.pathVariables = pathVariables;
}
public Long getRequestTime() {
return requestTime;
}
public MagicScriptContext getMagicScriptContext() {
return magicScriptContext;
}
public void setMagicScriptContext(MagicScriptContext magicScriptContext) {
this.magicScriptContext = magicScriptContext;
}
public Map<String, Object> getHeaders() {
return headers;
}
public void setHeaders(Map<String, Object> headers) {
this.headers = headers;
}
}

View File

@@ -0,0 +1,70 @@
package org.ssssssss.magicapi.model;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class TreeNode<T> {
private T node;
private List<TreeNode<T>> children = new ArrayList<>();
public TreeNode() {
}
public TreeNode(T node) {
this.node = node;
}
public T getNode() {
return node;
}
public void setNode(T node) {
this.node = node;
}
public List<TreeNode<T>> getChildren() {
return children;
}
public void setChildren(List<TreeNode<T>> children) {
this.children = children;
}
public TreeNode<T> findTreeNode(Function<T, Boolean> mapping) {
return findTreeNode(this.children, mapping);
}
private TreeNode<T> findTreeNode(List<TreeNode<T>> childs, Function<T, Boolean> mapping) {
for (TreeNode<T> child : childs) {
if (mapping.apply(child.getNode())) {
return child;
}
TreeNode<T> node = findTreeNode(child.children, mapping);
if (node != null) {
return node;
}
}
return null;
}
public void moveTo(TreeNode<T> node) {
node.children.add(this);
}
public List<T> flat() {
return flat(this);
}
private List<T> flat(TreeNode<T> node) {
List<T> result = new ArrayList<>();
result.add(node.getNode());
for (TreeNode<T> item : node.getChildren()) {
result.addAll(flat(item));
}
return result;
}
}

View File

@@ -0,0 +1,90 @@
package org.ssssssss.magicapi.modules;
import org.apache.commons.lang3.StringUtils;
import org.ssssssss.magicapi.config.MagicModule;
import org.ssssssss.script.annotation.Comment;
import org.ssssssss.script.exception.MagicScriptAssertException;
import java.util.regex.Pattern;
/**
* 断言模块
*/
public class AssertModule implements MagicModule {
/**
* 判断值不能为null
*
* @param value 值
* @param code 状态码
* @param message 状态说明
*/
@Comment("判断值不能为空")
public void notNull(@Comment("") Object value, @Comment("判断失败时的code") int code, @Comment("判断失败时的说明") String message) {
if (value == null) {
throw new MagicScriptAssertException(code, message);
}
}
/**
* 判断值不能为empty
*
* @param value 值
* @param code 状态码
* @param message 状态说明
*/
@Comment("判断值不能为Empty")
public void notEmpty(@Comment("") String value, @Comment("判断失败时的code") int code, @Comment("判断失败时的说明") String message) {
if (StringUtils.isEmpty(value)) {
throw new MagicScriptAssertException(code, message);
}
}
/**
* 判断值不能为blank
*
* @param value 值
* @param code 状态码
* @param message 状态说明
*/
@Comment("判断值不能为Blank")
public void notBlank(@Comment("") String value, @Comment("判断失败时的code") int code, @Comment("判断失败时的说明") String message) {
if (StringUtils.isBlank(value)) {
throw new MagicScriptAssertException(code, message);
}
}
/**
* 正则验证值
*
* @param value 值
* @param code 状态码
* @param message 状态说明
*/
@Comment("正则判断")
public void regx(@Comment("") String value, String pattern, @Comment("判断失败时的code") int code, @Comment("判断失败时的说明") String message) {
if (value == null || !Pattern.compile(pattern).matcher(value).matches()) {
throw new MagicScriptAssertException(code, message);
}
}
/**
* 判断值值是否为true
*
* @param value 值
* @param code 状态码
* @param message 状态说明
*/
@Comment("判断值是否为true")
public void isTrue(@Comment("") boolean value, @Comment("判断失败时的code") int code, @Comment("判断失败时的说明") String message) {
if (!value) {
throw new MagicScriptAssertException(code, message);
}
}
@Override
public String getModuleName() {
return "assert";
}
}

View File

@@ -0,0 +1,163 @@
package org.ssssssss.magicapi.modules;
import org.ssssssss.magicapi.cache.SqlCache;
import org.ssssssss.magicapi.context.RequestContext;
import org.ssssssss.magicapi.interceptor.SQLInterceptor;
import org.ssssssss.script.MagicScriptContext;
import org.ssssssss.script.functions.StreamExtension;
import org.ssssssss.script.parsing.GenericTokenParser;
import org.ssssssss.script.parsing.ast.literal.BooleanLiteral;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class BoundSql {
private static final GenericTokenParser concatTokenParser = new GenericTokenParser("${", "}", false);
private static final GenericTokenParser replaceTokenParser = new GenericTokenParser("#{", "}", true);
private static final GenericTokenParser ifTokenParser = new GenericTokenParser("?{", "}", true);
private static final GenericTokenParser ifParamTokenParser = new GenericTokenParser("?{", ",", true);
private static final Pattern REPLACE_MULTI_WHITE_LINE = Pattern.compile("(\r?\n(\\s*\r?\n)+)");
private String sql;
private List<Object> parameters = new ArrayList<>();
private SqlCache sqlCache;
private String cacheName;
private long ttl;
public BoundSql(String sql, List<Object> parameters, SQLModule sqlModule) {
this.sql = sql;
this.parameters = parameters;
this.sqlCache = sqlModule.getSqlCache();
this.cacheName = sqlModule.getCacheName();
this.ttl = sqlModule.getTtl();
}
BoundSql(String sql) {
MagicScriptContext context = MagicScriptContext.get();
// 处理?{}参数
this.sql = ifTokenParser.parse(sql.trim(), text -> {
AtomicBoolean ifTrue = new AtomicBoolean(false);
String val = ifParamTokenParser.parse("?{" + text, param -> {
ifTrue.set(BooleanLiteral.isTrue(context.eval(param)));
return null;
});
return ifTrue.get() ? val : "";
});
// 处理${}参数
this.sql = concatTokenParser.parse(this.sql, text -> String.valueOf(context.eval(text)));
// 处理#{}参数
this.sql = replaceTokenParser.parse(this.sql, text -> {
Object value = context.eval(text);
if (value == null) {
parameters.add(null);
return "?";
}
try {
//对集合自动展开
List<Object> objects = StreamExtension.arrayLikeToList(value);
parameters.addAll(objects);
return IntStream.range(0, objects.size()).mapToObj(t -> "?").collect(Collectors.joining(","));
} catch (Exception e) {
parameters.add(value);
return "?";
}
});
this.sql = this.sql == null ? null : REPLACE_MULTI_WHITE_LINE.matcher(this.sql.trim()).replaceAll("\r\n");
}
BoundSql(String sql, SQLModule sqlModule) {
this(sql);
this.sqlCache = sqlModule.getSqlCache();
this.cacheName = sqlModule.getCacheName();
this.ttl = sqlModule.getTtl();
}
private BoundSql() {
}
BoundSql copy(String newSql) {
BoundSql boundSql = new BoundSql();
boundSql.setParameters(new ArrayList<>(this.parameters));
boundSql.setSql(this.sql);
boundSql.ttl = this.ttl;
boundSql.cacheName = this.cacheName;
boundSql.sqlCache = this.sqlCache;
boundSql.sql = newSql;
return boundSql;
}
/**
* 添加SQL参数
*/
public void addParameter(Object value) {
parameters.add(value);
}
/**
* 获取要执行的SQL
*/
public String getSql() {
return sql;
}
/**
* 设置要执行的SQL
*/
public void setSql(String sql) {
this.sql = sql;
}
/**
* 获取要执行的参数
*/
public Object[] getParameters() {
return parameters.toArray();
}
/**
* 设置要执行的参数
*/
public void setParameters(List<Object> parameters) {
this.parameters = parameters;
}
/**
* 获取缓存值
*/
private <T> T getCacheValue(String sql, Object[] params, Supplier<T> supplier) {
if (cacheName == null) {
return supplier.get();
}
String cacheKey = sqlCache.buildSqlCacheKey(sql, params);
Object cacheValue = sqlCache.get(cacheName, cacheKey);
if (cacheValue != null) {
return (T) cacheValue;
}
T value = supplier.get();
sqlCache.put(cacheName, cacheKey, value, ttl);
return value;
}
/**
* 获取缓存值
*/
<T> T getCacheValue(List<SQLInterceptor> interceptors, Supplier<T> supplier) {
interceptors.forEach(interceptor -> interceptor.preHandle(this, RequestContext.getRequestEntity()));
return getCacheValue(this.getSql(), this.getParameters(), supplier);
}
}

View File

@@ -0,0 +1,29 @@
package org.ssssssss.magicapi.modules;
import org.springframework.core.env.Environment;
import org.ssssssss.magicapi.config.MagicModule;
import org.ssssssss.script.annotation.Comment;
public class EnvModule implements MagicModule {
private final Environment environment;
public EnvModule(Environment environment) {
this.environment = environment;
}
@Override
public String getModuleName() {
return "env";
}
@Comment("获取配置")
public String get(@Comment("配置项") String key) {
return environment.getProperty(key);
}
@Comment("获取配置")
public String get(@Comment("配置项") String key, @Comment("未配置时的默认值") String defaultValue) {
return environment.getProperty(key, defaultValue);
}
}

View File

@@ -0,0 +1,175 @@
package org.ssssssss.magicapi.modules;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.ssssssss.magicapi.config.MagicModule;
import org.ssssssss.script.annotation.Comment;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* http 模块
*
* @since 1.1.0
*/
public class HttpModule implements MagicModule {
private final RestTemplate template;
private final HttpHeaders httpHeaders = new HttpHeaders();
private final Class<?> responseType = Object.class;
private final MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
private final MultiValueMap<String, Object> data = new LinkedMultiValueMap<>();
private final Map<String, ?> variables = new HashMap<>();
private String url;
private HttpMethod method = HttpMethod.GET;
private HttpEntity<Object> entity = null;
private Object requestBody;
public HttpModule(RestTemplate template) {
this.template = template;
}
public HttpModule(RestTemplate template, String url) {
this.template = template;
this.url = url;
}
@Override
public String getModuleName() {
return "http";
}
@Comment("创建连接")
public HttpModule connect(@Comment("目标URL") String url) {
return new HttpModule(template, url);
}
@Comment("设置URL参数")
public HttpModule param(@Comment("参数名") String key, @Comment("参数值") Object... values) {
if (values != null) {
for (Object value : values) {
this.params.add(key, value);
}
}
return this;
}
@Comment("批量设置URL参数")
public HttpModule param(@Comment("参数值") Map<String, Object> values) {
values.forEach((key, value) -> param(key, Objects.toString(value, "")));
return this;
}
@Comment("设置form参数")
public HttpModule data(@Comment("参数名") String key, @Comment("参数值") Object... values) {
if (values != null) {
for (Object value : values) {
this.data.add(key, value);
}
}
return this;
}
@Comment("批量设置form参数")
public HttpModule data(@Comment("参数值") Map<String, Object> values) {
values.forEach((key, value) -> data(key, Objects.toString(value, "")));
return this;
}
@Comment("设置header")
public HttpModule header(@Comment("header名") String key, @Comment("header值") String value) {
httpHeaders.add(key, value);
return this;
}
@Comment("批量设置header")
public HttpModule header(@Comment("header值") Map<String, Object> values) {
values.entrySet()
.stream()
.filter(it -> it.getValue() != null)
.forEach(entry -> header(entry.getKey(), entry.getValue().toString()));
return this;
}
@Comment("设置请求方法默认GET")
public HttpModule method(@Comment("请求方法") HttpMethod method) {
this.method = method;
return this;
}
@Comment("设置`RequestBody`")
public HttpModule body(@Comment("`RequestBody`") Object requestBody) {
this.requestBody = requestBody;
this.contentType(MediaType.APPLICATION_JSON);
return this;
}
@Comment("自定义`HttpEntity`")
public HttpModule entity(@Comment("`HttpEntity`") HttpEntity<Object> entity) {
this.entity = entity;
return this;
}
@Comment("设置`ContentType`")
public HttpModule contentType(@Comment("Content-Type值") String contentType) {
return contentType(MediaType.parseMediaType(contentType));
}
@Comment("设置`ContentType`")
public HttpModule contentType(@Comment("Content-Type值") MediaType mediaType) {
this.httpHeaders.setContentType(mediaType);
return this;
}
@Comment("发送`POST`请求")
public ResponseEntity<Object> post() {
this.method(HttpMethod.POST);
return this.execute();
}
@Comment("发送`GET`请求")
public ResponseEntity<Object> get() {
this.method(HttpMethod.GET);
return this.execute();
}
@Comment("发送`PUT`请求")
public ResponseEntity<Object> put() {
this.method(HttpMethod.PUT);
return this.execute();
}
@Comment("发送`DELETE`请求")
public ResponseEntity<Object> delete() {
this.method(HttpMethod.DELETE);
return this.execute();
}
@Comment("执行请求")
public ResponseEntity<Object> execute() {
if (!this.params.isEmpty()) {
String params = this.params.entrySet().stream()
.map(it -> it.getValue().stream()
.map(value -> it.getKey() + "=" + value)
.collect(Collectors.joining("&"))
).collect(Collectors.joining("&"));
if (StringUtils.isNotBlank(params)) {
this.url += (this.url.contains("?") ? "&" : "?") + params;
}
}
if (!this.data.isEmpty()) {
this.entity = new HttpEntity<>(this.data, this.httpHeaders);
} else if (this.entity == null && this.requestBody != null) {
this.entity = new HttpEntity<>(this.requestBody, this.httpHeaders);
} else {
this.entity = new HttpEntity<>(null, this.httpHeaders);
}
return template.exchange(url, this.method, entity, Object.class, responseType, variables);
}
}

View File

@@ -0,0 +1,78 @@
package org.ssssssss.magicapi.modules;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.UpdateOptions;
import org.bson.Document;
import org.ssssssss.script.annotation.Comment;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* MongoCollection方法扩展
*/
public class MongoCollectionExtension {
@Comment("执行批量插入操作")
public void insert(MongoCollection<Document> collection, @Comment("要插入的集合") List<Map<String, Object>> maps) {
collection.insertMany(maps.stream().map(Document::new).collect(Collectors.toList()));
}
@Comment("执行单条插入操作")
public void insert(MongoCollection<Document> collection, @Comment("执行插入数据") Map<String, Object> map) {
insert(collection, Collections.singletonList(map));
}
@Comment("执行查询操作")
public FindIterable<Document> find(MongoCollection<Document> collection, @Comment("查询条件") Map<String, Object> query) {
return collection.find(new Document(query));
}
@Comment("修改操作,返回修改数量")
public long update(MongoCollection<Document> collection, @Comment("查询条件") Map<String, Object> query, @Comment("修改值") Map<String, Object> update) {
return collection.updateOne(new Document(query), new Document(update)).getModifiedCount();
}
@Comment("批量修改,返回修改数量")
public long updateMany(MongoCollection<Document> collection, @Comment("修改条件") Map<String, Object> query, @Comment("修改值") Map<String, Object> update) {
return collection.updateMany(new Document(query), new Document(update)).getModifiedCount();
}
@Comment("批量修改,返回修改数量")
public long updateMany(MongoCollection<Document> collection, @Comment("查询条件") Map<String, Object> query, @Comment("修改值") Map<String, Object> update, Map<String, Object> filters) {
UpdateOptions updateOptions = new UpdateOptions();
if (filters != null && !filters.isEmpty()) {
Object upsert = filters.get("upsert");
if (upsert != null) {
filters.remove("upsert");
updateOptions.upsert(Boolean.parseBoolean(upsert.toString()));
}
Object bypassDocumentValidation = filters.get("bypassDocumentValidation");
if (bypassDocumentValidation != null) {
filters.remove("bypassDocumentValidation");
updateOptions.bypassDocumentValidation(Boolean.parseBoolean(bypassDocumentValidation.toString()));
}
List<Document> arrayFilters = filters.entrySet().stream().map(entry -> new Document(entry.getKey(), entry.getValue())).collect(Collectors.toList());
updateOptions.arrayFilters(arrayFilters);
}
return collection.updateMany(new Document(query), new Document(update), updateOptions).getModifiedCount();
}
@Comment("查询数量")
public long count(MongoCollection<Document> collection, @Comment("查询") Map<String, Object> query) {
return collection.countDocuments(new Document(query));
}
@Comment("批量删除,返回删除条数")
public long remove(MongoCollection<Document> collection, @Comment("删除条件") Map<String, Object> query) {
return collection.deleteMany(new Document(query)).getDeletedCount();
}
@Comment("删除一条,返回删除条数")
public long removeOne(MongoCollection<Document> collection, @Comment("删除条件") Map<String, Object> query) {
return collection.deleteOne(new Document(query)).getDeletedCount();
}
}

View File

@@ -0,0 +1,27 @@
package org.ssssssss.magicapi.modules;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCursor;
import org.bson.Document;
import org.ssssssss.script.annotation.Comment;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Mongo FindIterable 方法扩展
*/
public class MongoFindIterableExtension {
@Comment("结果转为List")
public List<Map<String, Object>> list(FindIterable<Document> iterable) {
MongoCursor<Document> cursor = iterable.iterator();
List<Map<String, Object>> result = new ArrayList<>();
while (cursor.hasNext()) {
result.add(cursor.next());
}
return result;
}
}

View File

@@ -0,0 +1,70 @@
package org.ssssssss.magicapi.modules;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import org.apache.commons.lang3.StringUtils;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.ssssssss.magicapi.config.MagicModule;
import org.ssssssss.script.reflection.AbstractReflection;
import org.ssssssss.script.reflection.JavaInvoker;
import java.lang.reflect.Method;
import java.util.HashMap;
/**
* mongo模块
*/
public class MongoModule extends HashMap<String, Object> implements MagicModule {
private static final Logger logger = LoggerFactory.getLogger(MongoModule.class);
private final MongoTemplate mongoTemplate;
private final JavaInvoker<Method> mongoDbFactoryInvoker;
private JavaInvoker<Method> invoker;
public MongoModule(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
AbstractReflection reflection = AbstractReflection.getInstance();
mongoDbFactoryInvoker = reflection.getMethod(this.mongoTemplate, "getMongoDbFactory");
if (mongoDbFactoryInvoker != null) {
try {
Object factory = mongoDbFactoryInvoker.invoke0(this.mongoTemplate, null);
invoker = reflection.getMethod(factory, "getDb", StringUtils.EMPTY);
if (invoker == null) {
invoker = reflection.getMethod(factory, "getMongoDatabase", StringUtils.EMPTY);
}
} catch (Throwable e) {
logger.error("mongo模块初始化失败", e);
}
} else {
logger.error("mongo模块初始化失败");
}
}
@Override
public Object get(Object databaseName) {
return databaseName == null ? null : new HashMap<String, MongoCollection<Document>>() {
@Override
public MongoCollection<Document> get(Object collection) {
if (collection == null) {
return null;
}
try {
Object factory = mongoDbFactoryInvoker.invoke0(mongoTemplate, null);
MongoDatabase database = (MongoDatabase) invoker.invoke0(factory, null, databaseName.toString());
return database.getCollection(collection.toString());
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
}
};
}
@Override
public String getModuleName() {
return "mongo";
}
}

View File

@@ -0,0 +1,75 @@
package org.ssssssss.magicapi.modules;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.ssssssss.magicapi.config.MagicModule;
import org.ssssssss.script.functions.DynamicMethod;
import java.util.ArrayList;
import java.util.List;
/**
* redis模块
*/
public class RedisModule implements MagicModule, DynamicMethod {
private final StringRedisTemplate redisTemplate;
public RedisModule(RedisConnectionFactory connectionFactory) {
this.redisTemplate = new StringRedisTemplate(connectionFactory);
}
@Override
public String getModuleName() {
return "redis";
}
/**
* 序列化
*/
private byte[] serializer(Object value) {
if (value == null || value instanceof String) {
return redisTemplate.getStringSerializer().serialize((String) value);
}
return serializer(value.toString());
}
/**
* 反序列化
*/
private Object deserialize(Object value) {
if (value != null) {
if (value instanceof byte[]) {
return this.redisTemplate.getStringSerializer().deserialize((byte[]) value);
}
if (value instanceof List) {
@SuppressWarnings("unchecked")
List<Object> valueList = (List<Object>) value;
List<Object> resultList = new ArrayList<>(valueList.size());
for (Object val : valueList) {
resultList.add(deserialize(val));
}
return resultList;
}
}
return value;
}
/**
* 执行命令
*
* @param methodName 命令名称
* @param parameters 命令参数
*/
@Override
public Object execute(String methodName, List<Object> parameters) {
return this.redisTemplate.execute((RedisCallback<Object>) connection -> {
byte[][] params = new byte[parameters.size()][];
for (int i = 0; i < params.length; i++) {
params[i] = serializer(parameters.get(i));
}
return deserialize(connection.execute(methodName, params));
});
}
}

View File

@@ -0,0 +1,87 @@
package org.ssssssss.magicapi.modules;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
import org.springframework.web.util.WebUtils;
import org.ssssssss.magicapi.context.RequestContext;
import org.ssssssss.script.annotation.Comment;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
/**
* request 模块
*/
public class RequestModule {
/**
* 获取文件信息
*
* @param name 参数名
*/
@Comment("获取文件")
public static MultipartFile getFile(@Comment("参数名") String name) {
MultipartRequest request = getMultipartHttpServletRequest();
return request == null ? null : request.getFile(name);
}
/**
* 获取文件信息
*
* @param name 参数名
*/
@Comment("获取多个文件")
public static List<MultipartFile> getFiles(@Comment("参数名") String name) {
MultipartRequest request = getMultipartHttpServletRequest();
return request == null ? null : request.getFiles(name);
}
/**
* 获取原生HttpServletRequest对象
*/
public static HttpServletRequest get() {
return RequestContext.getHttpServletRequest();
}
private static MultipartRequest getMultipartHttpServletRequest() {
HttpServletRequest request = get();
if (request != null && request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart/")) {
return WebUtils.getNativeRequest(request, MultipartRequest.class);
}
return null;
}
/**
* 根据参数名获取参数值集合
*
* @param name 参数名
*/
@Comment("根据请求参数名获取值")
public List<String> getValues(@Comment("参数名") String name) {
HttpServletRequest request = get();
if (request != null) {
String[] values = request.getParameterValues(name);
return values == null ? null : Arrays.asList(values);
}
return null;
}
/**
* 根据header名获取header集合
*
* @param name 参数名
*/
@Comment("根据header名获取值")
public List<String> getHeaders(@Comment("header名") String name) {
HttpServletRequest request = get();
if (request != null) {
Enumeration<String> headers = request.getHeaders(name);
return headers == null ? null : Collections.list(headers);
}
return null;
}
}

View File

@@ -0,0 +1,200 @@
package org.ssssssss.magicapi.modules;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.ssssssss.magicapi.provider.ResultProvider;
import org.ssssssss.script.annotation.Comment;
import org.ssssssss.script.functions.ObjectConvertExtension;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
/**
* response模块
*/
public class ResponseModule {
private final ResultProvider resultProvider;
public ResponseModule(ResultProvider resultProvider) {
this.resultProvider = resultProvider;
}
/**
* 文件下载
*
* @param value 文件内容
* @param filename 文件名
*/
@Comment("文件下载")
public static ResponseEntity<?> download(@Comment("文件内容,如`byte[]`") Object value, @Comment("文件名") String filename) throws UnsupportedEncodingException {
return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(filename, "UTF-8"))
.body(value);
}
/**
* 自行构建分页结果
*
* @param total 条数
* @param values 数据内容
*/
@Comment("返回自定义分页结果")
public Object page(@Comment("总条数") long total, @Comment("当前结果集") List<Map<String, Object>> values) {
return resultProvider.buildPageResult(total, values);
}
/**
* 自定义json结果
*
* @param value json内容
*/
@Comment("自定义返回json内容")
public ResponseEntity<Object> json(@Comment("返回对象") Object value) {
return ResponseEntity.ok(value);
}
/**
* 添加Header
*/
@Comment("添加response header")
public ResponseModule addHeader(@Comment("header名") String key, @Comment("header值") String value) {
if (StringUtils.isNotBlank(key)) {
HttpServletResponse response = getResponse();
if (response != null) {
response.addHeader(key, value);
}
}
return this;
}
/**
* 设置header
*/
@Comment("设置response header")
public ResponseModule setHeader(@Comment("header名") String key, @Comment("header值") String value) {
if (StringUtils.isNotBlank(key)) {
HttpServletResponse response = getResponse();
if (response != null) {
response.setHeader(key, value);
}
}
return this;
}
/**
* 添加cookie
*/
@Comment("添加Cookie")
public ResponseModule addCookie(@Comment("cookie名") String name, @Comment("cookie值") String value) {
if (StringUtils.isNotBlank(name)) {
addCookie(new Cookie(name, value));
}
return this;
}
/**
* 批量添加cookie
*/
@Comment("批量添加Cookie")
public ResponseModule addCookies(@Comment("Cookies") Map<String, String> cookies, @Comment("Cookie选项如`path`、`httpOnly`、`domain`、`maxAge`") Map<String, Object> options) {
if (cookies != null) {
for (Map.Entry<String, String> entry : cookies.entrySet()) {
addCookie(entry.getKey(), entry.getValue(), options);
}
}
return this;
}
/**
* 批量添加cookie
*/
@Comment("批量添加Cookie")
public ResponseModule addCookies(@Comment("Cookies") Map<String, String> cookies) {
return addCookies(cookies, null);
}
/**
* 添加cookie
*/
@Comment("添加Cookie")
public ResponseModule addCookie(@Comment("Cookie名") String name, @Comment("Cookie值") String value,
@Comment("Cookie选项如`path`、`httpOnly`、`domain`、`maxAge`") Map<String, Object> options) {
if (StringUtils.isNotBlank(name)) {
Cookie cookie = new Cookie(name, value);
if (options != null) {
Object path = options.get("path");
if (path != null) {
cookie.setPath(path.toString());
}
Object httpOnly = options.get("httpOnly");
if (httpOnly != null) {
cookie.setHttpOnly("true".equalsIgnoreCase(httpOnly.toString()));
}
Object domain = options.get("domain");
if (domain != null) {
cookie.setDomain(domain.toString());
}
Object maxAge = options.get("maxAge");
int age;
if (maxAge != null && (age = ObjectConvertExtension.asInt(maxAge, Integer.MIN_VALUE)) != Integer.MIN_VALUE) {
cookie.setMaxAge(age);
}
}
addCookie(cookie);
}
return this;
}
@Comment("终止输出,执行此方法后不会对结果进行任何输出及处理")
public NullValue end() {
return NullValue.INSTANCE;
}
/**
* 添加cookie
*/
@Comment("添加Cookie")
public ResponseModule addCookie(@Comment("Cookie对象") Cookie cookie) {
if (cookie != null) {
HttpServletResponse response = getResponse();
if (response != null) {
response.addCookie(cookie);
}
}
return this;
}
private HttpServletResponse getResponse() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
return ((ServletRequestAttributes) requestAttributes).getResponse();
}
return null;
}
/**
* 展示图片
*
* @param value 图片内容
* @param mime 图片类型image/png,image/jpeg,image/gif
*/
@Comment("输出图片")
public ResponseEntity image(@Comment("图片内容,如`byte[]`") Object value, @Comment("图片类型,如`image/png`、`image/jpeg`、`image/gif`") String mime) {
return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, mime).body(value);
}
public static class NullValue {
static final NullValue INSTANCE = new NullValue();
}
}

View File

@@ -0,0 +1,463 @@
package org.ssssssss.magicapi.modules;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.ssssssss.magicapi.adapter.ColumnMapperAdapter;
import org.ssssssss.magicapi.adapter.DialectAdapter;
import org.ssssssss.magicapi.cache.SqlCache;
import org.ssssssss.magicapi.config.MagicDynamicDataSource;
import org.ssssssss.magicapi.config.MagicDynamicDataSource.DataSourceNode;
import org.ssssssss.magicapi.config.MagicModule;
import org.ssssssss.magicapi.context.RequestContext;
import org.ssssssss.magicapi.dialect.Dialect;
import org.ssssssss.magicapi.interceptor.SQLInterceptor;
import org.ssssssss.magicapi.model.Page;
import org.ssssssss.magicapi.model.RequestEntity;
import org.ssssssss.magicapi.modules.table.NamedTable;
import org.ssssssss.magicapi.provider.PageProvider;
import org.ssssssss.magicapi.provider.ResultProvider;
import org.ssssssss.script.MagicScriptContext;
import org.ssssssss.script.annotation.Comment;
import org.ssssssss.script.annotation.UnableCall;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* 数据库查询模块
*/
public class SQLModule extends HashMap<String, SQLModule> implements MagicModule {
private MagicDynamicDataSource dynamicDataSource;
private DataSourceNode dataSourceNode;
private PageProvider pageProvider;
private ResultProvider resultProvider;
private ColumnMapperAdapter columnMapperAdapter;
private DialectAdapter dialectAdapter;
private RowMapper<Map<String, Object>> columnMapRowMapper;
private Function<String, String> rowMapColumnMapper;
private SqlCache sqlCache;
private String cacheName;
private List<SQLInterceptor> sqlInterceptors;
private long ttl;
public SQLModule() {
}
public SQLModule(MagicDynamicDataSource dynamicDataSource) {
this.dynamicDataSource = dynamicDataSource;
this.dataSourceNode = dynamicDataSource.getDataSource();
}
@UnableCall
public void setPageProvider(PageProvider pageProvider) {
this.pageProvider = pageProvider;
}
@UnableCall
public void setResultProvider(ResultProvider resultProvider) {
this.resultProvider = resultProvider;
}
@UnableCall
public void setColumnMapperProvider(ColumnMapperAdapter columnMapperAdapter) {
this.columnMapperAdapter = columnMapperAdapter;
}
@UnableCall
public void setDialectAdapter(DialectAdapter dialectAdapter) {
this.dialectAdapter = dialectAdapter;
}
@UnableCall
public void setColumnMapRowMapper(RowMapper<Map<String, Object>> columnMapRowMapper) {
this.columnMapRowMapper = columnMapRowMapper;
}
@UnableCall
public void setRowMapColumnMapper(Function<String, String> rowMapColumnMapper) {
this.rowMapColumnMapper = rowMapColumnMapper;
}
private void setDynamicDataSource(MagicDynamicDataSource dynamicDataSource) {
this.dynamicDataSource = dynamicDataSource;
}
@UnableCall
public void setSqlInterceptors(List<SQLInterceptor> sqlInterceptors) {
this.sqlInterceptors = sqlInterceptors;
}
private void setDataSourceNode(DataSourceNode dataSourceNode) {
this.dataSourceNode = dataSourceNode;
}
protected String getCacheName() {
return cacheName;
}
private void setCacheName(String cacheName) {
this.cacheName = cacheName;
}
protected long getTtl() {
return ttl;
}
private void setTtl(long ttl) {
this.ttl = ttl;
}
protected SqlCache getSqlCache() {
return sqlCache;
}
@UnableCall
public void setSqlCache(SqlCache sqlCache) {
this.sqlCache = sqlCache;
}
@UnableCall
private SQLModule cloneSQLModule() {
SQLModule sqlModule = new SQLModule();
sqlModule.setDynamicDataSource(this.dynamicDataSource);
sqlModule.setDataSourceNode(this.dataSourceNode);
sqlModule.setPageProvider(this.pageProvider);
sqlModule.setColumnMapperProvider(this.columnMapperAdapter);
sqlModule.setColumnMapRowMapper(this.columnMapRowMapper);
sqlModule.setRowMapColumnMapper(this.rowMapColumnMapper);
sqlModule.setSqlCache(this.sqlCache);
sqlModule.setTtl(this.ttl);
sqlModule.setResultProvider(this.resultProvider);
sqlModule.setDialectAdapter(this.dialectAdapter);
sqlModule.setSqlInterceptors(this.sqlInterceptors);
return sqlModule;
}
/**
* 开启事务,在一个回调中进行操作
*
* @param function 回调函数
*/
@Comment("开启事务,并在回调中处理")
public Object transaction(@Comment("回调函数,如:()=>{....}") Function<?, ?> function) {
Transaction transaction = transaction(); //创建事务
try {
Object val = function.apply(null);
transaction.commit(); //提交事务
return val;
} catch (Throwable throwable) {
transaction.rollback(); //回滚事务
throw throwable;
}
}
/**
* 开启事务,手动提交和回滚
*/
@Comment("开启事务,返回事务对象")
public Transaction transaction() {
return new Transaction(this.dataSourceNode.getDataSourceTransactionManager());
}
/**
* 使用缓存
*
* @param cacheName 缓存名
* @param ttl 过期时间
*/
@Comment("使用缓存")
public SQLModule cache(@Comment("缓存名") String cacheName, @Comment("过期时间") long ttl) {
if (cacheName == null) {
return this;
}
SQLModule sqlModule = cloneSQLModule();
sqlModule.setCacheName(cacheName);
sqlModule.setTtl(ttl);
return sqlModule;
}
/**
* 使用缓存(采用默认缓存时间)
*
* @param cacheName 缓冲名
*/
@Comment("使用缓存,过期时间采用默认配置")
public SQLModule cache(@Comment("缓存名") String cacheName) {
return cache(cacheName, 0);
}
@Comment("采用驼峰列名")
public SQLModule camel() {
return columnCase("camel");
}
@Comment("采用帕斯卡列名")
public SQLModule pascal() {
return columnCase("pascal");
}
@Comment("采用全小写列名")
public SQLModule lower() {
return columnCase("lower");
}
@Comment("采用全大写列名")
public SQLModule upper() {
return columnCase("upper");
}
@Comment("列名保持原样")
public SQLModule normal() {
return columnCase("default");
}
@Comment("指定列名转换")
public SQLModule columnCase(String name) {
SQLModule sqlModule = cloneSQLModule();
sqlModule.setColumnMapRowMapper(this.columnMapperAdapter.getColumnMapRowMapper(name));
sqlModule.setRowMapColumnMapper(this.columnMapperAdapter.getRowMapColumnMapper(name));
return sqlModule;
}
/**
* 数据源切换
*/
@Override
public SQLModule get(Object key) {
SQLModule sqlModule = cloneSQLModule();
if (key == null) {
sqlModule.setDataSourceNode(dynamicDataSource.getDataSource());
} else {
sqlModule.setDataSourceNode(dynamicDataSource.getDataSource(key.toString()));
}
return sqlModule;
}
/**
* 查询List
*/
@Comment("查询SQL返回List类型结果")
public List<Map<String, Object>> select(@Comment("`SQL`语句") String sql) {
return select(new BoundSql(sql, this));
}
@UnableCall
public List<Map<String, Object>> select(BoundSql boundSql) {
return boundSql.getCacheValue(this.sqlInterceptors, () -> dataSourceNode.getJdbcTemplate().query(boundSql.getSql(), this.columnMapRowMapper, boundSql.getParameters()));
}
/**
* 执行update
*/
@Comment("执行update操作返回受影响行数")
public int update(@Comment("`SQL`语句") String sql) {
return update(new BoundSql(sql));
}
@UnableCall
public int update(BoundSql boundSql) {
sqlInterceptors.forEach(sqlInterceptor -> sqlInterceptor.preHandle(boundSql));
int value = dataSourceNode.getJdbcTemplate().update(boundSql.getSql(), boundSql.getParameters());
if (this.cacheName != null) {
this.sqlCache.delete(this.cacheName);
}
return value;
}
/**
* 插入并返回主键
*/
@Comment("执行insert操作返回插入主键")
public long insert(@Comment("`SQL`语句") String sql) {
MagicKeyHolder magicKeyHolder = new MagicKeyHolder();
insert(new BoundSql(sql), magicKeyHolder);
return magicKeyHolder.getLongKey();
}
/**
* 插入并返回主键
*/
@Comment("执行insert操作返回插入主键")
public Object insert(@Comment("`SQL`语句") String sql, @Comment("主键列") String primary) {
return insert(new BoundSql(sql), primary);
}
void insert(BoundSql boundSql, MagicKeyHolder keyHolder) {
sqlInterceptors.forEach(sqlInterceptor -> sqlInterceptor.preHandle(boundSql));
dataSourceNode.getJdbcTemplate().update(con -> {
PreparedStatement ps = keyHolder.createPrepareStatement(con, boundSql.getSql());
new ArgumentPreparedStatementSetter(boundSql.getParameters()).setValues(ps);
return ps;
}, keyHolder);
if (this.cacheName != null) {
this.sqlCache.delete(this.cacheName);
}
}
@UnableCall
public Object insert(BoundSql boundSql, String primary) {
MagicKeyHolder keyHolder = new MagicKeyHolder(primary);
insert(boundSql, keyHolder);
return keyHolder.getObjectKey();
}
/**
* 分页查询
*/
@Comment("执行分页查询,分页条件自动获取")
public Object page(@Comment("`SQL`语句") String sql) {
return page(new BoundSql(sql, this));
}
/**
* 分页查询手动传入limit和offset参数
*/
@Comment("执行分页查询,分页条件手动传入")
public Object page(@Comment("`SQL`语句") String sql, @Comment("限制条数") long limit, @Comment("跳过条数") long offset) {
BoundSql boundSql = new BoundSql(sql, this);
return page(boundSql, new Page(limit, offset));
}
@UnableCall
public Object page(BoundSql boundSql) {
Page page = pageProvider.getPage(MagicScriptContext.get());
return page(boundSql, page);
}
private Object page(BoundSql boundSql, Page page) {
Dialect dialect = dataSourceNode.getDialect(dialectAdapter);
BoundSql countBoundSql = boundSql.copy(dialect.getCountSql(boundSql.getSql()));
int count = countBoundSql.getCacheValue(this.sqlInterceptors, () -> dataSourceNode.getJdbcTemplate().query(countBoundSql.getSql(), new SingleRowResultSetExtractor<>(Integer.class), countBoundSql.getParameters()));
List<Map<String, Object>> list = null;
if (count > 0) {
BoundSql pageBoundSql = buildPageBoundSql(dialect, boundSql, page.getOffset(), page.getLimit());
list = pageBoundSql.getCacheValue(this.sqlInterceptors, () -> dataSourceNode.getJdbcTemplate().query(pageBoundSql.getSql(), this.columnMapRowMapper, pageBoundSql.getParameters()));
}
RequestEntity requestEntity = RequestContext.getRequestEntity();
return resultProvider.buildPageResult(requestEntity, page, count, list);
}
/**
* 查询int值
*/
@Comment("查询int值适合单行单列int的结果")
public Integer selectInt(@Comment("`SQL`语句") String sql) {
BoundSql boundSql = new BoundSql(sql, this);
return boundSql.getCacheValue(this.sqlInterceptors, () -> dataSourceNode.getJdbcTemplate().query(boundSql.getSql(),new SingleRowResultSetExtractor<>(Integer.class), boundSql.getParameters()));
}
/**
* 查询Map
*/
@Comment("查询单条结果查不到返回null")
public Map<String, Object> selectOne(@Comment("`SQL`语句") String sql) {
return selectOne(new BoundSql(sql, this));
}
@UnableCall
public Map<String, Object> selectOne(BoundSql boundSql) {
return boundSql.getCacheValue(this.sqlInterceptors, () -> {
Dialect dialect = dataSourceNode.getDialect(dialectAdapter);
BoundSql pageBoundSql = buildPageBoundSql(dialect, boundSql, 0, 1);
return dataSourceNode.getJdbcTemplate().query(pageBoundSql.getSql(), new SingleRowResultSetExtractor<>(this.columnMapRowMapper), pageBoundSql.getParameters());
});
}
/**
* 查询单行单列的值
*/
@Comment("查询单行单列的值")
public Object selectValue(@Comment("`SQL`语句") String sql) {
BoundSql boundSql = new BoundSql(sql, this);
return boundSql.getCacheValue(this.sqlInterceptors, () -> {
Dialect dialect = dataSourceNode.getDialect(dialectAdapter);
BoundSql pageBoundSql = buildPageBoundSql(dialect, boundSql, 0, 1);
return dataSourceNode.getJdbcTemplate().query(pageBoundSql.getSql(), new SingleRowResultSetExtractor<>(Object.class), pageBoundSql.getParameters());
});
}
@Comment("指定table进行单表操作")
public NamedTable table(String tableName) {
return new NamedTable(tableName, this, rowMapColumnMapper);
}
private BoundSql buildPageBoundSql(Dialect dialect, BoundSql boundSql, long offset, long limit) {
String pageSql = dialect.getPageSql(boundSql.getSql(), boundSql, offset, limit);
return boundSql.copy(pageSql);
}
@UnableCall
@Override
public String getModuleName() {
return "db";
}
class MagicKeyHolder extends GeneratedKeyHolder {
private final boolean useGeneratedKeys;
private final String primary;
public MagicKeyHolder() {
this(null);
}
public MagicKeyHolder(String primary) {
this.primary = primary;
this.useGeneratedKeys = StringUtils.isBlank(primary);
}
PreparedStatement createPrepareStatement(Connection connection, String sql) throws SQLException {
if (useGeneratedKeys) {
return connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
}
return connection.prepareStatement(sql, new String[]{primary});
}
public long getLongKey() throws InvalidDataAccessApiUsageException, DataRetrievalFailureException {
Number key = super.getKey();
if (key == null) {
return -1;
}
return key.longValue();
}
public Object getObjectKey() {
if (useGeneratedKeys) {
return getLongKey();
}
List<Map<String, Object>> keyList = getKeyList();
if (keyList.isEmpty()) {
return null;
}
Iterator<Object> keyIterator = keyList.get(0).values().iterator();
return keyIterator.hasNext() ? keyIterator.next() : null;
}
}
}

View File

@@ -0,0 +1,45 @@
package org.ssssssss.magicapi.modules;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SingleRowResultSetExtractor<T> implements ResultSetExtractor<T>{
private final boolean singleColumn;
private final RowMapper<T> mapper;
private final Class<T> requiredType;
public SingleRowResultSetExtractor(RowMapper<T> mapper) {
this(mapper, null, false);
}
public SingleRowResultSetExtractor(Class<T> requiredType) {
this(null, requiredType, true);
}
private SingleRowResultSetExtractor(RowMapper<T> mapper, Class<T> requiredType, boolean singleColumn) {
this.mapper = mapper;
this.requiredType = requiredType;
this.singleColumn = singleColumn;
}
@Override
@SuppressWarnings("unchecked")
public T extractData(ResultSet rs) throws SQLException, DataAccessException {
if(rs.next()){
if(singleColumn){
return (T) JdbcUtils.getResultSetValue(rs, 1, requiredType);
}
return mapper.mapRow(rs, 0);
}
return null;
}
}

View File

@@ -0,0 +1,38 @@
package org.ssssssss.magicapi.modules;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.ssssssss.script.annotation.Comment;
/**
* 事务模块
*/
public class Transaction {
private static final TransactionDefinition TRANSACTION_DEFINITION = new DefaultTransactionDefinition();
private final DataSourceTransactionManager dataSourceTransactionManager;
private final TransactionStatus transactionStatus;
public Transaction(DataSourceTransactionManager dataSourceTransactionManager) {
this.dataSourceTransactionManager = dataSourceTransactionManager;
this.transactionStatus = dataSourceTransactionManager.getTransaction(TRANSACTION_DEFINITION);
}
/**
* 回滚事务
*/
@Comment("回滚事务")
public void rollback() {
this.dataSourceTransactionManager.rollback(this.transactionStatus);
}
/**
* 提交事务
*/
@Comment("提交事务")
public void commit() {
this.dataSourceTransactionManager.commit(this.transactionStatus);
}
}

View File

@@ -0,0 +1,257 @@
package org.ssssssss.magicapi.modules.table;
import org.apache.commons.lang3.StringUtils;
import org.ssssssss.magicapi.exception.MagicAPIException;
import org.ssssssss.magicapi.modules.BoundSql;
import org.ssssssss.magicapi.modules.SQLModule;
import org.ssssssss.script.annotation.Comment;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class NamedTable {
String tableName;
SQLModule sqlModule;
String primary;
Map<String, Object> columns = new HashMap<>();
List<String> fields = new ArrayList<>();
List<String> groups = new ArrayList<>();
List<String> orders = new ArrayList<>();
Function<String, String> rowMapColumnMapper;
Object defaultPrimaryValue;
Where where = new Where(this);
public NamedTable(String tableName, SQLModule sqlModule, Function<String, String> rowMapColumnMapper) {
this.tableName = tableName;
this.sqlModule = sqlModule;
this.rowMapColumnMapper = rowMapColumnMapper;
}
@Comment("设置主键名update时使用")
public NamedTable primary(String primary) {
return primary(primary, null);
}
@Comment("设置主键名,并设置默认主键值(主要用于insert)")
public NamedTable primary(String primary, Object defaultPrimaryValue) {
this.primary = rowMapColumnMapper.apply(primary);
this.defaultPrimaryValue = defaultPrimaryValue;
return this;
}
@Comment("拼接where")
public Where where() {
return where;
}
@Comment("设置单列的值")
public NamedTable column(@Comment("列名") String key, @Comment("") Object value) {
this.columns.put(rowMapColumnMapper.apply(key), value);
return this;
}
@Comment("设置查询的列,如`columns('a','b','c')`")
public NamedTable columns(@Comment("各项列") String... columns) {
if (columns != null) {
for (String column : columns) {
column(column);
}
}
return this;
}
@Comment("设置查询的列,如`columns(['a','b','c'])`")
public NamedTable columns(Collection<String> columns) {
if (columns != null) {
columns.stream().filter(StringUtils::isNotBlank).map(rowMapColumnMapper).forEach(this.fields::add);
}
return this;
}
@Comment("设置查询的列,如`column('a')`")
public NamedTable column(String column) {
if (StringUtils.isNotBlank(column)) {
this.fields.add(this.rowMapColumnMapper.apply(column));
}
return this;
}
@Comment("拼接`order by xxx asc/desc`")
public NamedTable orderBy(@Comment("要排序的列") String column, @Comment("`asc`或`desc`") String sort) {
this.orders.add(column + " " + sort);
return this;
}
@Comment("拼接`order by xxx asc`")
public NamedTable orderBy(@Comment("要排序的列") String column) {
return orderBy(column, "asc");
}
@Comment("拼接`order by xxx desc`")
public NamedTable orderByDesc(@Comment("要排序的列") String column) {
return orderBy(column, "desc");
}
@Comment("拼接`group by`")
public NamedTable groupBy(@Comment("要分组的列") String... columns) {
this.groups.addAll(Arrays.asList(columns));
return this;
}
private List<Map.Entry<String, Object>> filterNotBlanks() {
return this.columns.entrySet().stream()
.filter(it -> StringUtils.isNotBlank(Objects.toString(it.getValue(), "")))
.collect(Collectors.toList());
}
@Comment("执行插入,返回主键")
public Object insert() {
return insert(null);
}
@Comment("执行插入,返回主键")
public Object insert(@Comment("各项列和值") Map<String, Object> data) {
if (data != null) {
data.forEach((key, value) -> this.columns.put(rowMapColumnMapper.apply(key), value));
}
if (this.defaultPrimaryValue != null && StringUtils.isBlank(Objects.toString(this.columns.getOrDefault(this.primary, "")))) {
this.columns.put(this.primary, this.defaultPrimaryValue);
}
List<Map.Entry<String, Object>> entries = filterNotBlanks();
if (entries.isEmpty()) {
throw new MagicAPIException("参数不能为空");
}
StringBuilder builder = new StringBuilder();
builder.append("insert into ");
builder.append(tableName);
builder.append("(");
builder.append(StringUtils.join(entries.stream().map(Map.Entry::getKey).toArray(), ","));
builder.append(") values (");
builder.append(StringUtils.join(Collections.nCopies(entries.size(), "?"), ","));
builder.append(")");
return sqlModule.insert(new BoundSql(builder.toString(), entries.stream().map(Map.Entry::getValue).collect(Collectors.toList()), sqlModule), this.primary);
}
@Comment("执行delete语句(物理删除)")
public int delete() {
if (where.isEmpty()) {
throw new MagicAPIException("delete语句不能没有条件");
}
StringBuilder builder = new StringBuilder();
builder.append("delete from ");
builder.append(tableName);
builder.append(where.getSql());
return sqlModule.update(new BoundSql(builder.toString(), where.getParams(), sqlModule));
}
@Comment("保存到表中,当主键有值时则修改,否则插入")
public Object save() {
return this.save(null);
}
@Comment("保存到表中,当主键有值时则修改,否则插入")
public Object save(@Comment("各项列和值") Map<String, Object> data) {
if (StringUtils.isBlank(this.primary)) {
throw new MagicAPIException("请设置主键");
}
if (StringUtils.isNotBlank(Objects.toString(this.columns.get(this.primary), "")) || (data != null && StringUtils.isNotBlank(Objects.toString(data.get(this.primary), "")))) {
return update(data);
}
return insert(data);
}
@Comment("执行`select`查询")
public List<Map<String, Object>> select() {
return sqlModule.select(buildSelect());
}
@Comment("执行`selectOne`查询")
public Map<String, Object> selectOne() {
return sqlModule.selectOne(buildSelect());
}
private BoundSql buildSelect() {
StringBuilder builder = new StringBuilder();
builder.append("select ");
if (this.fields.isEmpty()) {
builder.append("*");
} else {
builder.append(StringUtils.join(this.fields, ","));
}
builder.append(" from ").append(tableName);
List<Object> params = new ArrayList<>();
if (!where.isEmpty()) {
builder.append(where.getSql());
params.addAll(where.getParams());
}
if (!orders.isEmpty()) {
builder.append(" order by ");
builder.append(String.join(",", orders));
}
if (!groups.isEmpty()) {
builder.append(" group by ");
builder.append(String.join(",", groups));
}
return new BoundSql(builder.toString(), params, sqlModule);
}
@Comment("执行分页查询")
public Object page() {
return sqlModule.page(buildSelect());
}
@Comment("执行update语句")
public int update() {
return update(null);
}
@Comment("执行update语句")
public int update(@Comment("各项列和值") Map<String, Object> data) {
if (null != data) {
data.forEach((key, value) -> this.columns.put(rowMapColumnMapper.apply(key), value));
}
Object primaryValue = null;
if (StringUtils.isNotBlank(this.primary)) {
primaryValue = this.columns.remove(this.primary);
}
List<Map.Entry<String, Object>> entries = filterNotBlanks();
if (entries.isEmpty()) {
throw new MagicAPIException("要修改的列不能为空");
}
StringBuilder builder = new StringBuilder();
builder.append("update ");
builder.append(tableName);
builder.append(" set ");
List<Object> params = new ArrayList<>();
for (int i = 0, size = entries.size(); i < size; i++) {
Map.Entry<String, Object> entry = entries.get(i);
builder.append(entry.getKey()).append(" = ?");
params.add(entry.getValue());
if (i + 1 < size) {
builder.append(",");
}
}
if (!where.isEmpty()) {
builder.append(where.getSql());
params.addAll(where.getParams());
} else if (primaryValue != null) {
builder.append(" where ").append(this.primary).append(" = ?");
params.add(primaryValue);
} else {
throw new MagicAPIException("主键值不能为空");
}
return sqlModule.update(new BoundSql(builder.toString(), params, sqlModule));
}
}

View File

@@ -0,0 +1,393 @@
package org.ssssssss.magicapi.modules.table;
import org.apache.commons.lang3.StringUtils;
import org.ssssssss.script.annotation.Comment;
import org.ssssssss.script.functions.StreamExtension;
import java.util.*;
import java.util.function.Function;
public class Where {
private final List<String> tokens = new ArrayList<>();
private final List<Object> params = new ArrayList<>();
private final NamedTable namedTable;
private final boolean needWhere;
private boolean notNull = false;
private boolean notBlank = false;
public Where(NamedTable namedTable) {
this(namedTable, true);
}
public Where(NamedTable namedTable, boolean needWhere) {
this.namedTable = namedTable;
this.needWhere = needWhere;
}
void appendAnd() {
remove();
tokens.add("and");
}
void appendOr() {
remove();
tokens.add("or");
}
List<Object> getParams() {
return params;
}
void remove() {
int size = tokens.size();
while (size > 0) {
String token = tokens.get(size - 1);
if ("and".equalsIgnoreCase(token) || "or".equalsIgnoreCase(token)) {
tokens.remove(size - 1);
size--;
} else {
break;
}
}
while (size > 0) {
String token = tokens.get(0);
if ("and".equalsIgnoreCase(token) || "or".equalsIgnoreCase(token)) {
tokens.remove(0);
size--;
} else {
break;
}
}
}
boolean isEmpty() {
return tokens.isEmpty();
}
void append(String value) {
tokens.add(value);
}
String getSql() {
remove();
if (isEmpty()) {
return "";
}
return (needWhere ? " where " : "") + StringUtils.join(tokens, " ");
}
@Comment("过滤`null`的参数")
public Where notNull() {
return notNull(true);
}
@Comment("过滤`blank`的参数")
public Where notBlank() {
return notBlank(true);
}
@Comment("是否过滤`null`的参数")
public Where notNull(boolean flag) {
this.notNull = flag;
return this;
}
@Comment("是否过滤`blank`的参数")
public Where notBlank(boolean flag) {
this.notNull = flag;
return this;
}
boolean filterNullAndBlank(Object value) {
if (notNull && value == null) {
return false;
}
return !notBlank || !StringUtils.isEmpty(Objects.toString(value, ""));
}
@Comment("等于`=`,如:`eq('name', '老王') ---> name = '老王'`")
public Where eq(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return eq(true, column, value);
}
@Comment("等于`=`,如:`eq('name', '老王') ---> name = '老王'`")
public Where eq(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
if (condition && filterNullAndBlank(value)) {
tokens.add(column);
if (value == null) {
append(" is null");
} else {
params.add(value);
append(" = ?");
}
appendAnd();
}
return this;
}
@Comment("不等于`<>`,如:`ne('name', '老王') ---> name <> '老王'`")
public Where ne(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return ne(true, column, value);
}
@Comment("不等于`<>`,如:`ne('name', '老王') ---> name <> '老王'`")
public Where ne(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
if (condition && filterNullAndBlank(value)) {
append(column);
if (value == null) {
append("is not null");
} else {
params.add(value);
append("<> ?");
}
appendAnd();
}
return this;
}
private Where append(boolean append, String column, String condition, Object value) {
if (append && filterNullAndBlank(value)) {
append(column);
append(condition);
appendAnd();
params.add(value);
}
return this;
}
@Comment("小于`<`,如:`lt('age', 18) ---> age < 18")
public Where lt(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return lt(true, column, value);
}
@Comment("小于`<`,如:`lt('age', 18) ---> age < 18")
public Where lt(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
return append(condition, column, " < ?", value);
}
@Comment("小于等于`<=`,如:`lte('age', 18) ---> age <= 18")
public Where lte(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return lte(true, column, value);
}
@Comment("小于等于`<=`,如:`lte('age', 18) ---> age <= 18")
public Where lte(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
return append(condition, column, " <= ?", value);
}
@Comment("大于`>`,如:`get('age', 18) ---> age > 18")
public Where gt(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return gt(true, column, value);
}
@Comment("大于`>`,如:`get('age', 18) ---> age > 18")
public Where gt(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
return append(condition, column, " > ?", value);
}
@Comment("大于等于`>=`,如:`get('age', 18) ---> age >= 18")
public Where gte(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return gte(true, column, value);
}
@Comment("大于等于`>=`,如:`get('age', 18) ---> age >= 18")
public Where gte(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
return append(condition, column, " >= ?", value);
}
@Comment("`in`,如:`in('age', [1,2,3]) ---> age in (1,2,3)")
public Where in(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return in(true, column, value);
}
@Comment("`in`,如:`in('age', [1,2,3]) ---> age in (1,2,3)")
public Where in(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
if (condition && value != null) {
List<Object> objects = StreamExtension.arrayLikeToList(value);
if (objects.size() > 0) {
append(column);
append(" in (");
append(StringUtils.join(",", Collections.nCopies(objects.size(), "?")));
append(")");
appendAnd();
params.addAll(objects);
}
}
return this;
}
@Comment("`not in`,如:`notIn('age', [1,2,3]) ---> age not in (1,2,3)")
public Where notIn(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return notIn(true, column, value);
}
@Comment("`not in`,如:`notIn('age', [1,2,3]) ---> age not in (1,2,3)")
public Where notIn(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
if (condition && value != null) {
List<Object> objects = StreamExtension.arrayLikeToList(value);
if (objects.size() > 0) {
append(column);
append("not in (");
append(StringUtils.join(",", Collections.nCopies(objects.size(), "?")));
append(")");
appendAnd();
params.addAll(objects);
}
}
return this;
}
@Comment("`like`,如:`like('name', '%王%') ---> name like '%王%'")
public Where like(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return like(true, column, value);
}
@Comment("`like`,如:`like('name', '%王%') ---> name like '%王%'")
public Where like(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
return append(condition, column, "like ?", value);
}
@Comment("`not like`,如:`notLike('name', '%王%') ---> name not like '%王%'")
public Where notLike(@Comment("数据库中的列名") String column, @Comment("") Object value) {
return notLike(true, column, value);
}
@Comment("`not like` ,如:`notLike('name', '%王%') ---> name not like '%王%'")
public Where notLike(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column, @Comment("") Object value) {
return append(condition, column, "not like ?", value);
}
@Comment("`is null`,如:`isNull('name') ---> name is null")
public Where isNull(@Comment("数据库中的列名") String column) {
return isNull(true, column);
}
@Comment("`is null`,如:`isNull('name') ---> name is null")
public Where isNull(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column) {
if (condition) {
append(column);
append("is null");
appendAnd();
}
return this;
}
@Comment("`is not null`,如:`isNotNull('name') ---> name is not null")
public Where isNotNull(@Comment("数据库中的列名") String column) {
return isNotNull(true, column);
}
@Comment("`is not null`,如:`isNotNull('name') ---> name is not null")
public Where isNotNull(@Comment("判断表达式当为true时拼接条件") boolean condition, @Comment("数据库中的列名") String column) {
if (condition) {
append(column);
append("is not null");
appendAnd();
}
return this;
}
@Comment("拼接`or`")
public Where or() {
appendOr();
return this;
}
@Comment("拼接`and`")
public Where and() {
appendAnd();
return this;
}
@Comment("`and`嵌套如and(it => it.eq('name','李白').ne('status','正常') --> and (name = '李白' and status <> '正常')")
public Where and(Function<Object[], Where> function) {
return and(true, function);
}
@Comment("`and`嵌套如and(it => it.eq('name','李白').ne('status','正常') --> and (name = '李白' and status <> '正常')")
public Where and(@Comment("判断表达式当为true时拼接条件") boolean condition, Function<Object[], Where> function) {
if (condition) {
Where expr = function.apply(new Object[]{new Where(this.namedTable, false)});
this.params.addAll(expr.params);
append("(");
append(expr.getSql());
append(")");
appendAnd();
}
return this;
}
@Comment("拼接`order by xxx asc/desc`")
public Where orderBy(@Comment("要排序的列") String column, @Comment("`asc`或`desc`") String sort) {
this.namedTable.orderBy(column, sort);
return this;
}
@Comment("拼接`order by xxx asc`")
public Where orderBy(@Comment("要排序的列") String column) {
return orderBy(column, "asc");
}
@Comment("拼接`order by xxx desc`")
public Where orderByDesc(@Comment("要排序的列") String column) {
return orderBy(column, "desc");
}
@Comment("拼接`group by`")
public Where groupBy(@Comment("要分组的列") String... columns) {
this.namedTable.groupBy(columns);
return this;
}
@Comment("保存到表中,当主键有值时则修改,否则插入")
public Object save() {
return namedTable.save();
}
@Comment("保存到表中,当主键有值时则修改,否则插入")
public Object save(@Comment("各项列和值") Map<String, Object> data) {
return namedTable.save(data);
}
@Comment("执行插入语句,返回主键")
public Object insert() {
return namedTable.insert();
}
@Comment("执行插入语句,返回主键")
public Object insert(@Comment("各项列和值") Map<String, Object> data) {
return namedTable.insert(data);
}
@Comment("执行update语句")
public int update() {
return namedTable.update();
}
@Comment("执行update语句")
public int update(@Comment("各项列和值") Map<String, Object> data) {
return namedTable.update(data);
}
@Comment("执行分页查询")
public Object page() {
return namedTable.page();
}
@Comment("执行select查询")
public List<Map<String, Object>> select() {
return namedTable.select();
}
@Comment("执行selectOne查询")
public Map<String, Object> selectOne() {
return namedTable.selectOne();
}
}

View File

@@ -0,0 +1,37 @@
package org.ssssssss.magicapi.provider;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.model.ApiInfo;
/**
* API存储接口
*/
public abstract class ApiServiceProvider extends StoreServiceProvider<ApiInfo> {
public ApiServiceProvider(Resource workspace, GroupServiceProvider groupServiceProvider) {
super(ApiInfo.class, workspace, groupServiceProvider);
}
/**
* 判断接口是否存在
*/
public boolean exists(ApiInfo info) {
return infos.values().stream()
.anyMatch(it -> info.getGroupId().equals(it.getGroupId()) && (info.getName().equals(it.getName()) || (info.getMethod().equals(it.getMethod()) && info.getPath().equals(it.getPath()))));
}
/**
* 判断接口是否存在
*/
public boolean existsWithoutId(ApiInfo info) {
return infos.values().stream()
.anyMatch(it -> !info.getId().equals(it.getId()) && info.getGroupId().equals(it.getGroupId()) && (info.getName().equals(it.getName()) || (info.getMethod().equals(it.getMethod()) && info.getPath().equals(it.getPath()))));
}
@Override
public byte[] serialize(ApiInfo info) {
return super.serialize(info);
}
}

View File

@@ -0,0 +1,37 @@
package org.ssssssss.magicapi.provider;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
public interface ColumnMapperProvider {
String name();
String mapping(String columnName);
default String unmapping(String name) {
return name;
}
default Function<String, String> getRowMapColumnMapper() {
return this::unmapping;
}
default ColumnMapRowMapper getColumnMapRowMapper() {
return new ColumnMapRowMapper() {
@Override
protected Map<String, Object> createColumnMap(int columnCount) {
return new LinkedHashMap<>(columnCount);
}
@Override
protected String getColumnKey(String columnName) {
return mapping(columnName);
}
};
}
}

View File

@@ -0,0 +1,26 @@
package org.ssssssss.magicapi.provider;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.model.FunctionInfo;
public abstract class FunctionServiceProvider extends StoreServiceProvider<FunctionInfo> {
public FunctionServiceProvider(Resource workspace, GroupServiceProvider groupServiceProvider) {
super(FunctionInfo.class, workspace, groupServiceProvider);
}
public boolean exists(FunctionInfo info) {
return infos.values().stream()
.anyMatch(it -> info.getGroupId().equals(it.getGroupId()) && (info.getName().equals(it.getName()) || info.getPath().equals(it.getPath())));
}
public boolean existsWithoutId(FunctionInfo info) {
return infos.values().stream()
.anyMatch(it -> !info.getId().equals(it.getId()) && info.getGroupId().equals(it.getGroupId()) && (info.getName().equals(it.getName()) || info.getPath().equals(it.getPath())));
}
@Override
public byte[] serialize(FunctionInfo info) {
return super.serialize(info);
}
}

View File

@@ -0,0 +1,65 @@
package org.ssssssss.magicapi.provider;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.model.Group;
import org.ssssssss.magicapi.model.TreeNode;
import java.util.List;
public interface GroupServiceProvider {
/**
* 添加分组
*/
boolean insert(Group group);
/**
* 修改分组
*/
boolean update(Group group);
/**
* 删除分组
*/
boolean delete(String groupId);
/**
* 分组是否存在
*/
boolean exists(Group group);
/**
* 是否有该分组
*/
boolean containsApiGroup(String groupId);
Group readGroup(Resource resource);
/**
* 接口分组列表
*/
TreeNode<Group> apiGroupTree();
/**
* 函数分组列表
*/
TreeNode<Group> functionGroupTree();
/**
* 分组列表
*/
List<Group> groupList(String type);
/**
* 根据分组Id获取分组路径
*/
String getFullPath(String groupId);
/**
* 根据分组Id获取分组名称
*/
String getFullName(String groupId);
Resource getGroupResource(String groupId);
}

View File

@@ -0,0 +1,20 @@
package org.ssssssss.magicapi.provider;
import java.util.Map;
public interface LanguageProvider {
/**
* 是否支持该语言
*/
boolean support(String languageName);
/**
* 执行具体脚本
*
* @param languageName 语言类型
* @param script 脚本内容
* @param context 当前环境中的变量信息
*/
Object execute(String languageName, String script, Map<String, Object> context) throws Exception;
}

View File

@@ -0,0 +1,136 @@
package org.ssssssss.magicapi.provider;
import org.ssssssss.magicapi.config.MagicModule;
import org.ssssssss.magicapi.model.ApiInfo;
import org.ssssssss.magicapi.model.FunctionInfo;
import org.ssssssss.magicapi.model.Group;
import org.ssssssss.magicapi.model.MagicNotify;
import java.util.List;
import java.util.Map;
/**
* API调用接口
*/
public interface MagicAPIService extends MagicModule {
/**
* 执行MagicAPI中的接口,原始内容不包含code以及message信息
*
* @param method 请求方法
* @param path 请求路径
* @param context 请求上下文,主要给脚本中使用
*/
Object execute(String method, String path, Map<String, Object> context);
/**
* 执行MagicAPI中的接口,带code和message信息
*
* @param method 请求方法
* @param path 请求路径
* @param context 请求上下文,主要给脚本中使用
*/
Object call(String method, String path, Map<String, Object> context);
/**
* 保存接口
*
* @return 保存成功后返回Id
*/
String saveApi(ApiInfo apiInfo);
/**
* 获取接口详情
* @param id 接口id
* @return
*/
ApiInfo getApiInfo(String id);
/**
* 获取接口列表
*/
List<ApiInfo> apiList();
/**
* 删除接口
*
* @param id 接口id
*/
boolean deleteApi(String id);
/**
* 移动接口
*
* @param id 接口id
* @param groupId 分组id
*/
boolean moveApi(String id, String groupId);
/**
* 保存函数
*
* @return 保存成功后返回Id
*/
String saveFunction(FunctionInfo functionInfo);
/**
* 获取函数详情
* @param id 函数id
*/
FunctionInfo getFunctionInfo(String id);
/**
* 获取函数列表
*/
List<FunctionInfo> functionList();
/**
* 删除函数
*
* @param id 函数id
*/
boolean deleteFunction(String id);
/**
* 移动函数
*
* @param id 函数id
* @param groupId 分组id
*/
boolean moveFunction(String id, String groupId);
/**
* 创建分组
*
* @param group 分组信息
* @return 创建成功后返回分组id
*/
String createGroup(Group group);
/**
* 修改分组信息
*
* @param group 分组信息
*/
boolean updateGroup(Group group);
/**
* 删除分组
*
* @param id 分组id
*/
boolean deleteGroup(String id);
/**
* 查询分组列表
*
* @param type 分组类型1 接口分组2 函数分组
*/
List<Group> groupList(String type);
/**
* 处理刷新通知
*/
boolean processNotify(MagicNotify magicNotify);
}

View File

@@ -0,0 +1,9 @@
package org.ssssssss.magicapi.provider;
import org.ssssssss.magicapi.model.MagicNotify;
public interface MagicNotifyService {
void sendNotify(MagicNotify magicNotify);
}

Some files were not shown because too many files have changed in this diff Show More