1
mirror of https://github.com/CarmJos/EasyConfiguration.git synced 2024-09-19 20:25:51 +00:00

feat(sql): Support sql based configuration provider.

This commit is contained in:
Carm Jos 2023-12-24 04:52:28 +08:00
parent aa50345329
commit 374f646f9e
14 changed files with 547 additions and 103 deletions

View File

@ -137,39 +137,43 @@ interface ConfigurationReader {
return getWrapper().get(path, def, String.class);
}
default <V> @NotNull List<V> getList(@NotNull String path, @NotNull ConfigValueParser<Object, V> parser) {
return parseList(getWrapper().getList(path), parser);
}
@Unmodifiable
default @NotNull List<String> getStringList(@NotNull String path) {
return parseList(getWrapper().getList(path), ConfigValueParser.castToString());
return getList(path, ConfigValueParser.castToString());
}
@Unmodifiable
default @NotNull List<Integer> getIntegerList(@NotNull String path) {
return parseList(getWrapper().getList(path), ConfigValueParser.intValue());
return getList(path, ConfigValueParser.intValue());
}
@Unmodifiable
default @NotNull List<Long> getLongList(@NotNull String path) {
return parseList(getWrapper().getList(path), ConfigValueParser.longValue());
return getList(path, ConfigValueParser.longValue());
}
@Unmodifiable
default @NotNull List<Double> getDoubleList(@NotNull String path) {
return parseList(getWrapper().getList(path), ConfigValueParser.doubleValue());
return getList(path, ConfigValueParser.doubleValue());
}
@Unmodifiable
default @NotNull List<Float> getFloatList(@NotNull String path) {
return parseList(getWrapper().getList(path), ConfigValueParser.floatValue());
return getList(path, ConfigValueParser.floatValue());
}
@Unmodifiable
default @NotNull List<Byte> getByteList(@NotNull String path) {
return parseList(getWrapper().getList(path), ConfigValueParser.byteValue());
return getList(path, ConfigValueParser.byteValue());
}
@Unmodifiable
default @NotNull List<Character> getCharList(@NotNull String path) {
return parseList(getWrapper().getList(path), ConfigValueParser.castObject(Character.class));
return getList(path, ConfigValueParser.castObject(Character.class));
}
@Unmodifiable

View File

@ -12,6 +12,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.LinkedHashMap;
/**
@ -37,6 +38,16 @@ public class JSONConfigProvider extends FileConfigProvider<JSONConfigWrapper> {
}
public void initializeConfig() {
onReload();
}
@Override
public @NotNull JSONConfigWrapper getConfiguration() {
return this.configuration;
}
@Override
protected void onReload() {
LinkedHashMap<?, ?> map = null;
try (FileInputStream is = new FileInputStream(file)) {
@ -50,17 +61,6 @@ public class JSONConfigProvider extends FileConfigProvider<JSONConfigWrapper> {
this.configuration = new JSONConfigWrapper(map);
}
@Override
public @NotNull JSONConfigWrapper getConfiguration() {
return this.configuration;
}
@Override
protected void onReload() throws Exception {
super.reload();
initializeConfig();
}
@Override
public @Nullable ConfigurationComments getComments() {
return null;
@ -68,7 +68,7 @@ public class JSONConfigProvider extends FileConfigProvider<JSONConfigWrapper> {
@Override
public void save() throws Exception {
try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
try (Writer writer = new OutputStreamWriter(Files.newOutputStream(file.toPath()), StandardCharsets.UTF_8)) {
gson.toJson(configuration.data, writer);
}
}

View File

@ -15,7 +15,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<maven.deploy.skip>true</maven.deploy.skip>
<log4j.version>2.22.0</log4j.version>
</properties>
<artifactId>easyconfiguration-sql</artifactId>
@ -28,6 +28,54 @@
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>cc.carm.lib</groupId>
<artifactId>easysql-beecp</artifactId>
<version>0.4.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.210</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.parent.groupId}</groupId>
<artifactId>easyconfiguration-demo</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,8 +1,26 @@
package cc.carm.lib.configuration;
import cc.carm.lib.configuration.sql.SQLConfigProvider;
import cc.carm.lib.easysql.api.SQLManager;
import org.jetbrains.annotations.NotNull;
public class EasyConfiguration {
private EasyConfiguration() {
}
public static SQLConfigProvider from(@NotNull SQLManager sqlManager, @NotNull String tableName, @NotNull String namespace) {
SQLConfigProvider provider = new SQLConfigProvider(sqlManager, tableName, namespace);
try {
provider.initializeConfig();
} catch (Exception e) {
e.printStackTrace();
}
return provider;
}
public static SQLConfigProvider from(@NotNull SQLManager sqlManager, @NotNull String tableName) {
return from(sqlManager, tableName, "base");
}
}

View File

@ -3,36 +3,184 @@ package cc.carm.lib.configuration.sql;
import cc.carm.lib.configuration.core.ConfigInitializer;
import cc.carm.lib.configuration.core.source.ConfigurationComments;
import cc.carm.lib.configuration.core.source.ConfigurationProvider;
import cc.carm.lib.easysql.api.SQLManager;
import cc.carm.lib.easysql.api.SQLQuery;
import cc.carm.lib.easysql.api.SQLTable;
import cc.carm.lib.easysql.api.enums.IndexType;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class SQLConfigProvider extends ConfigurationProvider<SQLSectionWrapper> {
import java.sql.ResultSet;
import java.util.*;
public class SQLConfigProvider extends ConfigurationProvider<SQLConfigWrapper> {
@Override
public @NotNull SQLSectionWrapper getConfiguration() {
return null;
public static Gson GSON = new GsonBuilder().serializeNulls().disableHtmlEscaping().setPrettyPrinting().create();
protected final @NotNull SQLManager sqlManager;
protected final @NotNull SQLTable table;
protected final @NotNull String namespace;
protected ConfigInitializer<SQLConfigProvider> initializer;
protected ConfigurationComments comments = new ConfigurationComments();
protected SQLConfigWrapper rootConfiguration;
protected final @NotNull Set<String> updated = new HashSet<>();
public SQLConfigProvider(@NotNull SQLManager sqlManager, @NotNull String tableName, @NotNull String namespace) {
this.sqlManager = sqlManager;
this.table = SQLTable.of(tableName, builder -> {
builder.addColumn("namespace", "VARCHAR(32) NOT NULL");
builder.addColumn("path", "VARCHAR(96) NOT NULL");
builder.addColumn("type", "TINYINT NOT NULL DEFAULT 0");
builder.addColumn("value", "TEXT");
builder.addColumn("inline_comment", "TEXT");
builder.addColumn("header_comments", "MEDIUMTEXT");
builder.addColumn("create_time", "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP");
builder.addColumn("update_time", "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP");
builder.setIndex(
IndexType.PRIMARY_KEY, "pk_" + tableName.toLowerCase(),
"namespace", "path"
);
builder.setTableSettings("ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
});
this.namespace = namespace;
}
@Override
public void save() throws Exception {
public @NotNull SQLConfigWrapper getConfiguration() {
return rootConfiguration;
}
public void initializeConfig() throws Exception {
this.table.create(this.sqlManager);
this.initializer = new ConfigInitializer<>(this);
onReload();
}
@Override
protected void onReload() throws Exception {
this.comments = new ConfigurationComments();
LinkedHashMap<String, Object> values = new LinkedHashMap<>();
try (SQLQuery query = this.table.createQuery()
.addCondition("namespace", namespace)
.build().execute()) {
ResultSet rs = query.getResultSet();
while (rs.next()) {
String path = rs.getString("path");
int type = rs.getInt("type");
try {
SQLValueResolver<?> resolver = SQLValueTypes.get(type);
if (resolver == null) throw new IllegalStateException("No resolver for type #" + type);
String value = rs.getString("value");
values.put(path, resolver.resolve(value));
loadInlineComment(path, rs.getString("inline_comment"));
loadHeaderComment(path, rs.getString("header_comments"));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
this.rootConfiguration = new SQLConfigWrapper(this, values);
}
@Override
public @Nullable ConfigurationComments getComments() {
return null;
public void save() throws Exception {
if (this.updated.isEmpty()) return;
Date date = new Date();
List<Object[]> values = new ArrayList<>();
List<String> deletes = new ArrayList<>();
for (String path : this.updated) {
Object value = this.rootConfiguration.get(path);
if (value == null) {
deletes.add(path);
continue;
}
if (value instanceof SQLConfigWrapper) {
value = getSourceMap(((SQLConfigWrapper) value).getSource());
}
SQLValueResolver<?> type = SQLValueTypes.get(value.getClass());
if (type != null) {
values.add(new Object[]{
this.namespace, path, date,
type.getID(), type.serializeObject(value),
getComments().getInlineComment(path),
GSON.toJson(getComments().getHeaderComment(path))
});
}
}
this.updated.clear();
this.table.createReplaceBatch()
.setColumnNames("namespace", "path", "update_time", "type", "value", "inline_comment", "header_comments")
.setAllParams(values)
.execute();
for (String path : deletes) {
this.table.createDelete()
.addCondition("namespace", this.namespace)
.addCondition("path", path)
.build().execute();
}
}
@Override
public @NotNull ConfigurationComments getComments() {
return this.comments;
}
@Override
public @NotNull ConfigInitializer<? extends ConfigurationProvider<SQLSectionWrapper>> getInitializer() {
return null;
public @NotNull ConfigInitializer<? extends ConfigurationProvider<SQLConfigWrapper>> getInitializer() {
return this.initializer;
}
protected void loadInlineComment(String path, String comment) {
if (comment == null) return;
comment = comment.trim();
if (comment.isEmpty()) return;
this.comments.setInlineComment(path, comment);
}
protected void loadHeaderComment(String path, String commentJson) {
if (commentJson == null) return;
commentJson = commentJson.trim();
if (commentJson.isEmpty()) return;
List<String> headerComments = GSON.fromJson(commentJson, new TypeToken<List<String>>() {
}.getType());
if (headerComments == null || headerComments.isEmpty()) return;
this.comments.setHeaderComments(path, headerComments);
}
protected static Map<String, Object> getSourceMap(Map<String, Object> map) {
Map<String, Object> source = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof SQLConfigWrapper) {
source.put(entry.getKey(), getSourceMap(((SQLConfigWrapper) entry.getValue()).getSource()));
} else {
source.put(entry.getKey(), entry.getValue());
}
}
return source;
}
}

View File

@ -0,0 +1,122 @@
package cc.carm.lib.configuration.sql;
import cc.carm.lib.configuration.core.source.ConfigurationWrapper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* For SQL configs, that primary path will be directly mapped to the value.
*
* @author CarmJos
*/
public class SQLConfigWrapper implements ConfigurationWrapper<Map<String, Object>> {
private static final char SEPARATOR = '.';
protected final @NotNull SQLConfigProvider provider;
protected final @NotNull Map<String, Object> data;
SQLConfigWrapper(@NotNull SQLConfigProvider provider, @NotNull Map<?, ?> map) {
this.provider = provider;
this.data = new LinkedHashMap<>();
for (Map.Entry<?, ?> entry : map.entrySet()) {
String key = (entry.getKey() == null) ? "null" : entry.getKey().toString();
if (entry.getValue() instanceof Map) {
this.data.put(key, new SQLConfigWrapper(provider, (Map<?, ?>) entry.getValue()));
} else {
this.data.put(key, entry.getValue());
}
}
}
@Override
public @NotNull Map<String, Object> getSource() {
return this.data;
}
@Override
public @NotNull Set<String> getKeys(boolean deep) {
return getValues(deep).keySet();
}
@Override
public @NotNull Map<String, Object> getValues(boolean deep) {
// Deep is not supported for SQL configs.
return new LinkedHashMap<>(this.data);
}
@Override
public void set(@NotNull String path, @Nullable Object value) {
if (value instanceof Map) {
value = new SQLConfigWrapper(this.provider, (Map<?, ?>) value);
}
SQLConfigWrapper section = getSectionFor(path);
if (section == this) {
if (value == null) {
this.data.remove(path);
} else {
this.data.put(path, value);
}
} else {
section.set(getChild(path), value);
}
this.provider.updated.add(path);
}
@Override
public boolean contains(@NotNull String path) {
return get(path) != null;
}
@Override
public @Nullable Object get(@NotNull String path) {
SQLConfigWrapper section = getSectionFor(path);
return section == this ? data.get(path) : section.get(getChild(path));
}
@Override
public boolean isList(@NotNull String path) {
return get(path) instanceof List<?>;
}
@Override
public @Nullable List<?> getList(@NotNull String path) {
Object val = get(path);
return (val instanceof List<?>) ? (List<?>) val : null;
}
@Override
public boolean isConfigurationSection(@NotNull String path) {
return get(path) instanceof SQLConfigWrapper;
}
@Override
public @Nullable SQLConfigWrapper getConfigurationSection(@NotNull String path) {
Object val = get(path);
return (val instanceof SQLConfigWrapper) ? (SQLConfigWrapper) val : null;
}
private SQLConfigWrapper getSectionFor(String path) {
int index = path.indexOf(SEPARATOR);
if (index == -1) return this;
String root = path.substring(0, index);
Object section = this.data.computeIfAbsent(root, k -> new SQLConfigWrapper(this.provider, new LinkedHashMap<>()));
return (SQLConfigWrapper) section;
}
private String getChild(String path) {
int index = path.indexOf(SEPARATOR);
return (index == -1) ? path : path.substring(index + 1);
}
}

View File

@ -1,63 +0,0 @@
package cc.carm.lib.configuration.sql;
import cc.carm.lib.configuration.core.source.ConfigurationWrapper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class SQLSectionWrapper implements ConfigurationWrapper<Map<String, Object>> {
@Override
public @NotNull Map<String, Object> getSource() {
return null;
}
@Override
public @NotNull Set<String> getKeys(boolean deep) {
return null;
}
@Override
public @NotNull Map<String, Object> getValues(boolean deep) {
return null;
}
@Override
public void set(@NotNull String path, @Nullable Object value) {
}
@Override
public boolean contains(@NotNull String path) {
return false;
}
@Override
public @Nullable Object get(@NotNull String path) {
return null;
}
@Override
public boolean isList(@NotNull String path) {
return false;
}
@Override
public @Nullable List<?> getList(@NotNull String path) {
return null;
}
@Override
public boolean isConfigurationSection(@NotNull String path) {
return false;
}
@Override
public @Nullable SQLSectionWrapper getConfigurationSection(@NotNull String path) {
return null;
}
}

View File

@ -1,7 +0,0 @@
package cc.carm.lib.configuration.sql;
public class SQLValueParser {
}

View File

@ -0,0 +1,60 @@
package cc.carm.lib.configuration.sql;
import cc.carm.lib.configuration.core.function.ConfigDataFunction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
public class SQLValueResolver<T> {
public static <V> SQLValueResolver<V> of(int id, @NotNull Class<V> clazz,
@NotNull ConfigDataFunction<String, V> resolver) {
return of(id, clazz, resolver, String::valueOf);
}
public static <V> SQLValueResolver<V> of(int id, @NotNull Class<V> clazz,
@NotNull ConfigDataFunction<String, V> resolver,
@NotNull Function<V, String> serializer) {
return new SQLValueResolver<>(id, clazz, resolver, serializer);
}
protected final int id;
protected final @NotNull Class<T> clazz;
protected final @NotNull ConfigDataFunction<String, T> resolver;
protected final @NotNull Function<T, String> serializer;
protected SQLValueResolver(int id, @NotNull Class<T> clazz,
@NotNull ConfigDataFunction<String, T> resolver,
@NotNull Function<T, String> serializer) {
this.id = id;
this.clazz = clazz;
this.resolver = resolver;
this.serializer = serializer;
}
public int getID() {
return id;
}
public @NotNull Class<T> getClazz() {
return clazz;
}
public boolean isTypeOf(@NotNull Class<?> clazz) {
return this.clazz.isAssignableFrom(clazz);
}
public @NotNull T resolve(String data) throws Exception {
return resolver.parse(data);
}
public @Nullable String serialize(T value) {
return String.valueOf(value);
}
public @Nullable String serializeObject(Object value) {
return isTypeOf(value.getClass()) ? serialize(clazz.cast(value)) : null;
}
}

View File

@ -0,0 +1,54 @@
package cc.carm.lib.configuration.sql;
import cc.carm.lib.configuration.core.function.ConfigDataFunction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
interface SQLValueTypes {
SQLValueResolver<String> STRING = SQLValueResolver.of(0, String.class, ConfigDataFunction.identity());
SQLValueResolver<Byte> BYTE = SQLValueResolver.of(1, Byte.class, Byte::parseByte);
SQLValueResolver<Short> SHORT = SQLValueResolver.of(2, Short.class, Short::parseShort);
SQLValueResolver<Integer> INTEGER = SQLValueResolver.of(3, Integer.class, Integer::parseInt);
SQLValueResolver<Long> LONG = SQLValueResolver.of(4, Long.class, Long::parseLong);
SQLValueResolver<Float> FLOAT = SQLValueResolver.of(5, Float.class, Float::parseFloat);
SQLValueResolver<Double> DOUBLE = SQLValueResolver.of(6, Double.class, Double::parseDouble);
SQLValueResolver<Boolean> BOOLEAN = SQLValueResolver.of(7, Boolean.class, Boolean::parseBoolean);
SQLValueResolver<Character> CHAR = SQLValueResolver.of(8, Character.class, s -> s.charAt(0));
@SuppressWarnings("rawtypes")
SQLValueResolver<List> LIST = SQLValueResolver.of(10, List.class,
array -> SQLConfigProvider.GSON.fromJson(array, List.class),
list -> SQLConfigProvider.GSON.toJson(list)
);
@SuppressWarnings("rawtypes")
SQLValueResolver<Map> SECTION = SQLValueResolver.of(20, Map.class,
section -> SQLConfigProvider.GSON.fromJson(section, LinkedHashMap.class),
section -> SQLConfigProvider.GSON.toJson(section)
);
static @NotNull SQLValueResolver<?>[] values() {
return new SQLValueResolver<?>[]{
STRING, BYTE, SHORT, INTEGER, LONG, FLOAT, DOUBLE, BOOLEAN, CHAR, LIST, SECTION
};
}
static SQLValueResolver<?> valueOf(int index) {
return values()[index];
}
static @Nullable SQLValueResolver<?> get(int id) {
return Arrays.stream(values()).filter(v -> v.id == id).findFirst().orElse(null);
}
static @Nullable SQLValueResolver<?> get(Class<?> typeClazz) {
return Arrays.stream(values()).filter(v -> v.isTypeOf(typeClazz)).findFirst().orElse(null);
}
}

View File

@ -0,0 +1,40 @@
package config;
import cc.carm.lib.configuration.EasyConfiguration;
import cc.carm.lib.configuration.demo.tests.ConfigurationTest;
import cc.carm.lib.configuration.sql.SQLConfigProvider;
import cc.carm.lib.easysql.EasySQL;
import cc.carm.lib.easysql.api.SQLManager;
import cc.carm.lib.easysql.beecp.BeeDataSourceConfig;
import org.junit.Test;
public class SQLConfigTest {
@Test
public void onTest() {
// test();
}
public void test() {
BeeDataSourceConfig config = new BeeDataSourceConfig();
config.setDriverClassName("org.h2.Driver");
config.setJdbcUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MySQL;");
SQLManager manager = EasySQL.createManager(config);
manager.setDebugMode(true);
SQLConfigProvider provider = EasyConfiguration.from(manager, "conf_test", "TESTING");
ConfigurationTest.testDemo(provider);
ConfigurationTest.testInner(provider);
System.out.println("----------------------------------------------------");
provider.getConfiguration().getValues(true).forEach((k, v) -> System.out.println(k + ": " + v));
System.out.println("----------------------------------------------------");
provider.getConfiguration().getValues(false).forEach((k, v) -> System.out.println(k + ": " + v));
System.out.println("----------------------------------------------------");
ConfigurationTest.save(provider);
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages="config.SQLConfigTest">
<Appenders>
<console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n"/>
</console>
</Appenders>
<Loggers>
<Root level="info">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL"/>
<RegexFilter regex=".*\$\{[^}]*\}.*" onMatch="DENY" onMismatch="NEUTRAL"/>
</filters>
<AppenderRef ref="File"/>
<appender-ref ref="Console"/>
</Root>
</Loggers>
</Configuration>

View File

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS conf
(
`namespace` VARCHAR(255) NOT NULL, # 命名空间
`path` VARCHAR(255) NOT NULL, # 配置路径 (ConfigPath)
`namespace` VARCHAR(32) NOT NULL, # 命名空间 (代表其属于谁,类似于单个配置文件地址的概念)
`path` VARCHAR(96) NOT NULL, # 配置路径 (ConfigPath)
`type` TINYINT UNSIGNED NOT NULL DEFAULT 0, # 数据类型 (Integer/Byte/List/Map/...)
`value` MEDIUMTEXT, # 配置项的值 (可能为JSON格式)
`inline_comments` TEXT, # 行内注释

View File

@ -41,6 +41,7 @@
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bspfsystems</groupId>
<artifactId>yamlconfiguration</artifactId>