1
0
Fork 0
mirror of https://github.com/xHyroM/lighteco.git synced 2024-11-10 01:18:07 +01:00

feat: auto dependency download

This commit is contained in:
Jozef Steinhübl 2023-08-28 20:40:47 +02:00
parent 341bc3eb44
commit 66bb95559c
24 changed files with 461 additions and 46 deletions

View file

@ -50,8 +50,8 @@ public class BukkitLightEcoBootstrap implements LightEcoBootstrap, LoaderBootstr
}
@Override
public File getDataFolder() {
return this.loader.getDataFolder();
public Path getDataDirectory() {
return this.loader.getDataFolder().toPath();
}
@Override

View file

@ -10,8 +10,7 @@ dependencies {
exclude(module = "annotations")
}
api("net.kyori:adventure-text-minimessage:4.14.0")
api("org.xerial:sqlite-jdbc:3.40.0.0")
api("com.google.guava:guava:32.1.2-jre")
implementation("eu.okaeri:okaeri-configs-yaml-snakeyaml:5.0.0-beta.5")
implementation("eu.okaeri:okaeri-configs-validator-okaeri:5.0.0-beta.5")

View file

@ -6,10 +6,11 @@ import eu.okaeri.configs.annotation.Variable;
public class StorageConfig extends OkaeriConfig {
@Comment("Storage provider.")
@Comment("Available providers: memory")
public String provider = "memory";
@Comment("Available providers: h2, sqlite")
public String provider = "h2";
@Comment("Data storage settings.")
@Comment("You don't need to worry about this if you're using local database.")
public StorageDataConfig data = new StorageDataConfig();
@Variable("table-prefix")

View file

@ -0,0 +1,44 @@
package dev.xhyrom.lighteco.common.dependencies;
import lombok.Getter;
public enum Dependency {
H2_DRIVER(
"com.h2database",
"h2",
"2.1.214"
),
SQLITE_DRIVER(
"org.xerial",
"sqlite-jdbc",
"3.28.0"
);
@Getter
private final String fullPath;
private final String groupId;
private final String artifactId;
private final String version;
private static final String MAVEN_FORMAT = "%s/%s/%s/%s-%s.jar";
Dependency(String groupId, String artifactId, String version) {
this.fullPath = String.format(MAVEN_FORMAT,
groupId.replace('.', '/'),
artifactId,
version,
artifactId,
version
);
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
}
public String getFileName() {
String name = name().toLowerCase().replace('_', '-');
return String.format("%s-%s.jar", name, this.version);
}
}

View file

@ -0,0 +1,16 @@
package dev.xhyrom.lighteco.common.dependencies;
import dev.xhyrom.lighteco.common.storage.StorageType;
import java.util.Set;
public interface DependencyManager extends AutoCloseable {
void loadDependencies(Set<Dependency> dependencies);
void loadStorageDependencies(Set<StorageType> types);
ClassLoader obtainClassLoaderWith(Set<Dependency> dependencies);
@Override
void close() throws Exception;
}

View file

@ -0,0 +1,157 @@
package dev.xhyrom.lighteco.common.dependencies;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.MoreFiles;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.storage.StorageType;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
public class DependencyManagerImpl implements DependencyManager {
private final EnumMap<Dependency, Path> loaded = new EnumMap<>(Dependency.class);
private final Map<ImmutableSet<Dependency>, IsolatedClassLoader> loaders = new HashMap<>();
private final DependencyRegistry registry;
private final Path cacheDirectory;
public DependencyManagerImpl(LightEcoPlugin plugin) {
this.registry = new DependencyRegistry();
this.cacheDirectory = setupCacheDirectory(plugin);
}
@Override
public void loadDependencies(Set<Dependency> dependencies) {
CountDownLatch latch = new CountDownLatch(dependencies.size());
for (Dependency dependency : dependencies) {
if (this.loaded.containsKey(dependency)) {
latch.countDown();
continue;
}
CompletableFuture.runAsync(() -> {
try {
loadDependency(dependency);
} catch (Exception e) {
new RuntimeException("Failed to load dependency " + dependency, e);
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void loadDependency(Dependency dependency) {
if (this.loaded.containsKey(dependency)) {
return;
}
try {
Path file = downloadDependency(dependency);
this.loaded.put(dependency, file);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private Path downloadDependency(Dependency dependency) throws Exception {
Path file = this.cacheDirectory.resolve(dependency.getFileName());
if (Files.exists(file)) {
return file;
}
Files.createDirectories(file.getParent());
DependencyRepository.MAVEN_CENTRAL.download(dependency, file);
return file;
}
@Override
public void loadStorageDependencies(Set<StorageType> types) {
loadDependencies(this.registry.resolveStorageDependencies(types));
}
@Override
public ClassLoader obtainClassLoaderWith(Set<Dependency> dependencies) {
ImmutableSet<Dependency> set = ImmutableSet.copyOf(dependencies);
for (Dependency dependency : dependencies) {
if (!this.loaded.containsKey(dependency)) {
throw new IllegalStateException("Dependency " + dependency + " is not loaded");
}
}
synchronized (this.loaders) {
IsolatedClassLoader classLoader = this.loaders.get(set);
if (classLoader != null) {
return classLoader;
}
URL[] urls = set.stream()
.map(this.loaded::get)
.map(file -> {
try {
return file.toUri().toURL();
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
classLoader = new IsolatedClassLoader(urls);
this.loaders.put(set, classLoader);
return classLoader;
}
}
private static Path setupCacheDirectory(LightEcoPlugin plugin) {
Path cacheDirectory = plugin.getBootstrap().getDataDirectory().resolve("libraries");
try {
MoreFiles.createParentDirectories(cacheDirectory);
} catch (IOException e) {
throw new RuntimeException("Unable to create libraries (cache) directory", e);
}
return cacheDirectory;
}
@Override
public void close() {
IOException exception = null;
for (IsolatedClassLoader loader : this.loaders.values()) {
try {
loader.close();
} catch (Exception e) {
if (exception == null) {
exception = new IOException("Failed to close class loader", e);
} else {
exception.addSuppressed(e);
}
}
}
if (exception != null) {
throw new RuntimeException(exception);
}
}
}

View file

@ -0,0 +1,25 @@
package dev.xhyrom.lighteco.common.dependencies;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.SetMultimap;
import dev.xhyrom.lighteco.common.storage.StorageType;
import java.util.LinkedHashSet;
import java.util.Set;
public class DependencyRegistry {
private static final SetMultimap<StorageType, Dependency> STORAGE_DEPENDENCIES = ImmutableSetMultimap.<StorageType, Dependency>builder()
.putAll(StorageType.SQLITE, Dependency.SQLITE_DRIVER)
.putAll(StorageType.H2, Dependency.H2_DRIVER)
.build();
public Set<Dependency> resolveStorageDependencies(Set<StorageType> types) {
Set<Dependency> dependencies = new LinkedHashSet<>();
for (StorageType type : types) {
dependencies.addAll(STORAGE_DEPENDENCIES.get(type));
}
return dependencies;
}
}

View file

@ -0,0 +1,33 @@
package dev.xhyrom.lighteco.common.dependencies;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
public enum DependencyRepository {
MAVEN_CENTRAL("https://repo1.maven.org/maven2/");
private final String url;
DependencyRepository(String url) {
this.url = url;
}
private URLConnection openConnection(Dependency dependency) throws IOException {
URL dependencyUrl = new URL(this.url + dependency.getFullPath());
return dependencyUrl.openConnection();
}
private byte[] download(Dependency dependency) throws IOException {
URLConnection connection = this.openConnection(dependency);
connection.connect();
return connection.getInputStream().readAllBytes();
}
public void download(Dependency dependency, Path file) throws IOException {
Files.write(file, this.download(dependency));
}
}

View file

@ -0,0 +1,15 @@
package dev.xhyrom.lighteco.common.dependencies;
import java.net.URL;
import java.net.URLClassLoader;
public class IsolatedClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
public IsolatedClassLoader(URL[] urls) {
super(urls, ClassLoader.getSystemClassLoader().getParent());
}
}

View file

@ -0,0 +1,7 @@
// Lot of implementation is from LuckPerms
// https://github.com/LuckPerms/LuckPerms/tree/71416baff214802a52f7d26521e6575f71c97fe4/common/src/main/java/me/lucko/luckperms/common/dependencies
// Copyright (c) lucko (Luck) <lucko@lucko.me>
// Copyright (c) contributors
// Under MIT License
package dev.xhyrom.lighteco.common.dependencies;

View file

@ -4,6 +4,8 @@ import dev.xhyrom.lighteco.api.LightEco;
import dev.xhyrom.lighteco.api.LightEcoProvider;
import dev.xhyrom.lighteco.common.api.LightEcoApi;
import dev.xhyrom.lighteco.common.config.Config;
import dev.xhyrom.lighteco.common.dependencies.DependencyManager;
import dev.xhyrom.lighteco.common.dependencies.DependencyManagerImpl;
import dev.xhyrom.lighteco.common.storage.Storage;
import dev.xhyrom.lighteco.common.storage.StorageFactory;
import eu.okaeri.configs.ConfigManager;
@ -14,18 +16,20 @@ import java.io.File;
public abstract class AbstractLightEcoPlugin implements LightEcoPlugin {
@Getter
private Storage storage;
private LightEcoApi api;
private DependencyManager dependencyManager;
@Getter
private Config config;
public final void load() {
this.config = ConfigManager.create(Config.class, (it) -> {
File path = new File(this.getBootstrap().getDataFolder(), "config.yml");
path.mkdir();
@Getter
private Storage storage;
private LightEcoApi api;
public final void load() {
this.dependencyManager = new DependencyManagerImpl(this);
this.config = ConfigManager.create(Config.class, (it) -> {
it.withConfigurer(new YamlSnakeYamlConfigurer());
it.withBindFile(path);
it.withBindFile(this.getBootstrap().getDataDirectory().resolve("config.yml"));
it.withRemoveOrphans(true);
it.saveDefaults();
it.load(true);
@ -35,6 +39,8 @@ public abstract class AbstractLightEcoPlugin implements LightEcoPlugin {
public final void enable() {
// setup storage
StorageFactory factory = new StorageFactory(this);
this.dependencyManager.loadStorageDependencies(factory.getRequiredTypes());
this.storage = factory.get();
// register listeners

View file

@ -3,6 +3,7 @@ package dev.xhyrom.lighteco.common.plugin;
import dev.xhyrom.lighteco.api.manager.ContextManager;
import dev.xhyrom.lighteco.api.platform.Platform;
import dev.xhyrom.lighteco.common.config.Config;
import dev.xhyrom.lighteco.common.dependencies.DependencyManager;
import dev.xhyrom.lighteco.common.manager.command.CommandManager;
import dev.xhyrom.lighteco.common.manager.currency.CurrencyManager;
import dev.xhyrom.lighteco.common.manager.user.UserManager;
@ -21,5 +22,7 @@ public interface LightEcoPlugin {
@NonNull CommandManager getCommandManager();
@NonNull ContextManager<?> getContextManager();
@NonNull DependencyManager getDependencyManager();
@NonNull Storage getStorage();
}

View file

@ -2,7 +2,6 @@ package dev.xhyrom.lighteco.common.plugin.bootstrap;
import dev.xhyrom.lighteco.common.plugin.logger.PluginLogger;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.List;
@ -11,7 +10,7 @@ import java.util.UUID;
public interface LightEcoBootstrap {
Object getLoader();
PluginLogger getLogger();
File getDataFolder();
Path getDataDirectory();
List<UUID> getOnlinePlayers();
InputStream getResourceStream(String filename);
}

View file

@ -1,11 +1,15 @@
package dev.xhyrom.lighteco.common.storage;
import com.google.common.collect.ImmutableSet;
import dev.xhyrom.lighteco.api.storage.StorageProvider;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.storage.provider.memory.MemoryStorageProvider;
import dev.xhyrom.lighteco.common.storage.provider.sql.SqlStorageProvider;
import dev.xhyrom.lighteco.common.storage.provider.sql.connection.file.H2ConnectionFactory;
import dev.xhyrom.lighteco.common.storage.provider.sql.connection.file.SqliteConnectionFactory;
import java.util.Set;
public class StorageFactory {
private final LightEcoPlugin plugin;
@ -13,6 +17,10 @@ public class StorageFactory {
this.plugin = plugin;
}
public Set<StorageType> getRequiredTypes() {
return ImmutableSet.of(StorageType.parse(this.plugin.getConfig().storage.provider));
}
public Storage get() {
// todo: use config
String provider = this.plugin.getConfig().storage.provider;
@ -27,10 +35,15 @@ public class StorageFactory {
switch (provider.toLowerCase()) {
case "memory":
return new MemoryStorageProvider(this.plugin);
case "h2":
return new SqlStorageProvider(
this.plugin,
new H2ConnectionFactory(this.plugin.getBootstrap().getDataDirectory().resolve("lighteco-h2").toAbsolutePath())
);
case "sqlite":
return new SqlStorageProvider(
this.plugin,
new SqliteConnectionFactory(this.plugin.getBootstrap().getDataFolder().toPath().resolve("data.db"))
new SqliteConnectionFactory(this.plugin.getBootstrap().getDataDirectory().resolve("lighteco-sqlite.db"))
);
default:
throw new IllegalArgumentException("Unknown storage provider: " + provider);

View file

@ -0,0 +1,27 @@
package dev.xhyrom.lighteco.common.storage;
import java.util.List;
import java.util.stream.Stream;
public enum StorageType {
MARIADB("MariaDB", "mariadb"),
MYSQL("MySQL", "mysql"),
SQLITE("SQLite", "sqlite"),
H2("H2", "h2");
private final String name;
private final List<String> identifiers;
StorageType(String name, String... identifiers) {
this.name = name;
this.identifiers = List.of(identifiers);
}
public static StorageType parse(String name) {
return Stream.of(values())
.filter(type -> type.identifiers.contains(name.toLowerCase()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown storage type: " + name));
}
}

View file

@ -0,0 +1,7 @@
// Lot of implementation is from LuckPerms
// https://github.com/LuckPerms/LuckPerms/tree/71416baff214802a52f7d26521e6575f71c97fe4/common/src/main/java/me/lucko/luckperms/common/storage
// Copyright (c) lucko (Luck) <lucko@lucko.me>
// Copyright (c) contributors
// Under MIT License
package dev.xhyrom.lighteco.common.storage.provider;

View file

@ -1,7 +0,0 @@
package dev.xhyrom.lighteco.common.storage.provider.sql;
public enum SqlImplementation {
H2,
SQLITE,
MYSQL;
}

View file

@ -1,9 +1,11 @@
package dev.xhyrom.lighteco.common.storage.provider.sql;
import dev.xhyrom.lighteco.common.storage.StorageType;
public enum SqlStatements {
SAVE_USER_LOCAL_CURRENCY(
"INSERT INTO {prefix}_users (uuid, currency_identifier, balance) VALUES (?1, ?2, ?3) ON CONFLICT (uuid, currency_identifier) DO UPDATE SET balance=?3;",
"INSERT INTO {prefix}_{context}_users (uuid, currency_identifier, balance) VALUES (?1, ?2, ?3) ON DUPLICATE KEY UPDATE balance=?3;"
"INSERT INTO {prefix}_users (uuid, currency_identifier, balance) VALUES (?1, ?2, ?3) ON DUPLICATE KEY UPDATE balance=?3;"
),
SAVE_USER_GLOBAL_CURRENCY(
"INSERT INTO {prefix}_users (uuid, currency_identifier, balance) VALUES (?1, ?2, ?3) ON CONFLICT (uuid, currency_identifier) DO UPDATE SET balance=?3;",
@ -18,12 +20,12 @@ public enum SqlStatements {
this.mysql = mysql;
}
public String get(SqlImplementation implementationName) {
public String get(StorageType implementationName) {
switch (implementationName) {
case SQLITE -> {
return this.sqlite;
}
case MYSQL -> {
case H2, MYSQL -> {
return this.mysql;
}
}

View file

@ -4,6 +4,7 @@ import dev.xhyrom.lighteco.api.model.user.User;
import dev.xhyrom.lighteco.api.storage.StorageProvider;
import dev.xhyrom.lighteco.common.model.currency.Currency;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.storage.StorageType;
import dev.xhyrom.lighteco.common.storage.provider.sql.connection.ConnectionFactory;
import org.checkerframework.checker.nullness.qual.NonNull;
@ -46,7 +47,7 @@ public class SqlStorageProvider implements StorageProvider {
.replace("{context}", plugin.getConfig().server)
);
final SqlImplementation implementationName = this.connectionFactory.getImplementationName();
final StorageType implementationName = this.connectionFactory.getImplementationName();
this.SAVE_USER_LOCAL_CURRENCY = SqlStatements.SAVE_USER_LOCAL_CURRENCY.get(implementationName);
this.SAVE_USER_GLOBAL_CURRENCY = SqlStatements.SAVE_USER_GLOBAL_CURRENCY.get(implementationName);
}

View file

@ -1,13 +1,13 @@
package dev.xhyrom.lighteco.common.storage.provider.sql.connection;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.storage.provider.sql.SqlImplementation;
import dev.xhyrom.lighteco.common.storage.StorageType;
import java.sql.Connection;
import java.util.function.Function;
public interface ConnectionFactory {
SqlImplementation getImplementationName();
StorageType getImplementationName();
void init(LightEcoPlugin plugin);
void shutdown() throws Exception;

View file

@ -1,13 +1,14 @@
package dev.xhyrom.lighteco.common.storage.provider.sql.connection.file;
import dev.xhyrom.lighteco.common.dependencies.Dependency;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.storage.provider.sql.SqlImplementation;
import dev.xhyrom.lighteco.common.storage.StorageType;
import java.io.File;
import java.lang.reflect.Constructor;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.EnumSet;
import java.util.Properties;
import java.util.function.Function;
@ -19,21 +20,27 @@ public class H2ConnectionFactory extends FileConnectionFactory {
}
@Override
public SqlImplementation getImplementationName() {
return SqlImplementation.H2;
public StorageType getImplementationName() {
return StorageType.H2;
}
@Override
public void init(LightEcoPlugin plugin) {
// TODO: implement
//ClassLoader classLoader = plugin
ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER));
try {
Class<?> connectionClass = classLoader.loadClass("org.h2.jdbc.JdbcConnection");
this.connectionConstructor = connectionClass.getConstructor(String.class, Properties.class, String.class, Object.class, boolean.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
@Override
protected Connection createConnection(Path file) throws SQLException {
try {
return (Connection) this.connectionConstructor.newInstance(
"jdbc:h2:" + file,
"jdbc:h2:" + file.toString() + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE",
new Properties(),
null, null, false
);
@ -45,4 +52,13 @@ public class H2ConnectionFactory extends FileConnectionFactory {
throw new SQLException("Failed to create connection", e);
}
}
@Override
public Function<String, String> getStatementProcessor() {
return s -> s
.replace('\'', '`')
.replace("LIKE", "ILIKE")
.replace("value", "`value`")
.replace("``value``", "`value`");
}
}

View file

@ -1,13 +1,16 @@
package dev.xhyrom.lighteco.common.storage.provider.sql.connection.file;
import dev.xhyrom.lighteco.common.dependencies.Dependency;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.storage.provider.sql.SqlImplementation;
import dev.xhyrom.lighteco.common.storage.StorageType;
import java.lang.reflect.Constructor;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.EnumSet;
import java.util.Properties;
public class SqliteConnectionFactory extends FileConnectionFactory {
private Constructor<?> connectionConstructor;
@ -17,31 +20,30 @@ public class SqliteConnectionFactory extends FileConnectionFactory {
}
@Override
public SqlImplementation getImplementationName() {
return SqlImplementation.SQLITE;
public StorageType getImplementationName() {
return StorageType.SQLITE;
}
@Override
public void init(LightEcoPlugin plugin) {
/*ClassLoader classLoader = ClassLoader.getSystemClassLoader();
ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER));
try {
Class<?> connectionClass = classLoader.loadClass("org.sqlite.jdbc4.JDBC4Connection");
this.connectionConstructor = connectionClass.getConstructor(String.class, String.class, Properties.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}*/
}
}
@Override
protected Connection createConnection(Path file) throws SQLException {
try {
/*return (Connection) this.connectionConstructor.newInstance(
return (Connection) this.connectionConstructor.newInstance(
"jdbc:sqlite:" + file,
new Properties(),
null, null, false
);*/
return DriverManager.getConnection("jdbc:sqlite:" + file);
file.toString(),
new Properties()
);
} catch (Exception e) {
if (e.getCause() instanceof SQLException) {
throw (SQLException) e.getCause();

View file

@ -0,0 +1,36 @@
package dev.xhyrom.lighteco.common.storage.provider.sql.connection.hikari;
import dev.xhyrom.lighteco.common.plugin.LightEcoPlugin;
import dev.xhyrom.lighteco.common.storage.StorageType;
import dev.xhyrom.lighteco.common.storage.provider.sql.connection.ConnectionFactory;
import java.sql.Connection;
import java.util.function.Function;
public class HikariConnectionFactory implements ConnectionFactory {
@Override
public StorageType getImplementationName() {
return null;
}
@Override
public void init(LightEcoPlugin plugin) {
}
@Override
public void shutdown() throws Exception {
}
@Override
public Function<String, String> getStatementProcessor() {
return null;
}
@Override
public Connection getConnection() throws Exception {
return null;
}
}

View file

@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS `{prefix}_users` (
`uuid` VARCHAR(36) NOT NULL,
`currency_identifier` VARCHAR(255) NOT NULL,
`balance` DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (`uuid`, `currency_identifier`)
);
CREATE TABLE IF NOT EXISTS `{prefix}_{context}_users` (
`uuid` VARCHAR(36) NOT NULL,
`currency_identifier` VARCHAR(255) NOT NULL,
`balance` DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (`uuid`, `currency_identifier`)
);