Add SQL-Storage and add storage configs

This commit is contained in:
Blue (Lukas Rieger) 2021-11-14 14:18:31 +01:00
parent f52d463458
commit 915f9d9608
No known key found for this signature in database
GPG Key ID: 904C4995F9E1F800
48 changed files with 2343 additions and 489 deletions

View File

@ -28,21 +28,30 @@
import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface; import de.bluecolored.bluemap.common.plugin.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.web.WebSettings; import de.bluecolored.bluemap.common.web.WebSettings;
import de.bluecolored.bluemap.core.MinecraftVersion; import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.config.*; import de.bluecolored.bluemap.core.config.ConfigManager;
import de.bluecolored.bluemap.core.config.ConfigurationException;
import de.bluecolored.bluemap.core.config.old.CoreConfig;
import de.bluecolored.bluemap.core.config.old.MapConfig;
import de.bluecolored.bluemap.core.config.old.RenderConfig;
import de.bluecolored.bluemap.core.config.old.WebServerConfig;
import de.bluecolored.bluemap.core.config.storage.StorageConfig;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap; import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.mca.MCAWorld; import de.bluecolored.bluemap.core.mca.MCAWorld;
import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; import de.bluecolored.bluemap.core.resourcepack.ParseResourceException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.FileStorage;
import de.bluecolored.bluemap.core.storage.Storage; import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.spongepowered.configurate.ConfigurationNode;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*; import java.util.*;
/** /**
@ -56,15 +65,19 @@ public class BlueMapService {
private final ThrowingFunction<UUID, String, IOException> worldNameProvider; private final ThrowingFunction<UUID, String, IOException> worldNameProvider;
private final ConfigManager configManager; private final ConfigManager configManager;
private final de.bluecolored.bluemap.core.config.old.ConfigManager configManagerOld;
private CoreConfig coreConfig; private CoreConfig coreConfig;
private RenderConfig renderConfig; private RenderConfig renderConfig;
private WebServerConfig webServerConfig; private WebServerConfig webServerConfig;
private final Map<String, Storage> storages;
private ResourcePack resourcePack; private ResourcePack resourcePack;
private Map<UUID, World> worlds; private Map<UUID, World> worlds;
private Map<String, BmMap> maps; private Map<String, BmMap> maps;
private Map<String, Storage> mapStorages;
public BlueMapService(MinecraftVersion minecraftVersion, File configFolder) { public BlueMapService(MinecraftVersion minecraftVersion, File configFolder) {
this.minecraftVersion = minecraftVersion; this.minecraftVersion = minecraftVersion;
@ -82,7 +95,10 @@ public BlueMapService(MinecraftVersion minecraftVersion, File configFolder) {
this.worldNameProvider = uuid -> null; this.worldNameProvider = uuid -> null;
configManager = new ConfigManager(); this.storages = new HashMap<>();
configManagerOld = new de.bluecolored.bluemap.core.config.old.ConfigManager();
configManager = new ConfigManager(this.configFolder.toPath());
} }
public BlueMapService(MinecraftVersion minecraftVersion, ServerInterface serverInterface) { public BlueMapService(MinecraftVersion minecraftVersion, ServerInterface serverInterface) {
@ -91,47 +107,76 @@ public BlueMapService(MinecraftVersion minecraftVersion, ServerInterface serverI
this.worldUUIDProvider = serverInterface::getUUIDForWorld; this.worldUUIDProvider = serverInterface::getUUIDForWorld;
this.worldNameProvider = serverInterface::getWorldName; this.worldNameProvider = serverInterface::getWorldName;
this.configManager = new ConfigManager(); this.storages = new HashMap<>();
this.configManagerOld = new de.bluecolored.bluemap.core.config.old.ConfigManager();
configManager = new ConfigManager(this.configFolder.toPath());
} }
public synchronized void createOrUpdateWebApp(boolean force) throws IOException { public synchronized void createOrUpdateWebApp(boolean force) throws ConfigurationException {
WebFilesManager webFilesManager = new WebFilesManager(getRenderConfig().getWebRoot()); WebFilesManager webFilesManager = new WebFilesManager(getRenderConfig().getWebRoot());
if (force || webFilesManager.needsUpdate()) { if (force || webFilesManager.needsUpdate()) {
webFilesManager.updateFiles(); try {
webFilesManager.updateFiles();
} catch (IOException ex) {
throw new ConfigurationException("Failed to update web-app files!", ex);
}
} }
} }
public synchronized WebSettings updateWebAppSettings() throws IOException, InterruptedException { public synchronized WebSettings updateWebAppSettings() throws ConfigurationException, InterruptedException {
WebSettings webSettings = new WebSettings(new File(getRenderConfig().getWebRoot(), "data" + File.separator + "settings.json")); try {
webSettings.set(getRenderConfig().isUseCookies(), "useCookies"); WebSettings webSettings = new WebSettings(new File(getRenderConfig().getWebRoot(),
webSettings.set(getRenderConfig().isEnableFreeFlight(), "freeFlightEnabled"); "data" + File.separator + "settings.json"));
webSettings.setAllMapsEnabled(false);
for (BmMap map : getMaps().values()) {
webSettings.setMapEnabled(true, map.getId());
webSettings.setFrom(map);
}
int ordinal = 0;
for (MapConfig map : getRenderConfig().getMapConfigs()) {
if (!getMaps().containsKey(map.getId())) continue; //don't add not loaded maps
webSettings.setOrdinal(ordinal++, map.getId());
webSettings.setFrom(map);
}
webSettings.save();
return webSettings; webSettings.set(getRenderConfig().isUseCookies(), "useCookies");
webSettings.set(getRenderConfig().isEnableFreeFlight(), "freeFlightEnabled");
webSettings.setAllMapsEnabled(false);
for (BmMap map : getMaps().values()) {
webSettings.setMapEnabled(true, map.getId());
webSettings.setFrom(map);
}
int ordinal = 0;
for (MapConfig map : getRenderConfig().getMapConfigs()) {
if (!getMaps().containsKey(map.getId())) continue; //don't add not loaded maps
webSettings.setOrdinal(ordinal++, map.getId());
webSettings.setFrom(map);
}
webSettings.save();
return webSettings;
} catch (IOException ex) {
throw new ConfigurationException("Failed to update web-app settings!", ex);
}
} }
public synchronized Map<UUID, World> getWorlds() throws IOException, InterruptedException { public synchronized Map<UUID, World> getWorlds() throws ConfigurationException, InterruptedException {
if (worlds == null) loadWorldsAndMaps(); if (worlds == null) loadWorldsAndMaps();
return worlds; return worlds;
} }
public synchronized Map<String, BmMap> getMaps() throws IOException, InterruptedException { public synchronized Map<String, BmMap> getMaps() throws ConfigurationException, InterruptedException {
if (maps == null) loadWorldsAndMaps(); if (maps == null) loadWorldsAndMaps();
return maps; return maps;
} }
private synchronized void loadWorldsAndMaps() throws IOException, InterruptedException { public synchronized Map<String, Storage> getMapStorages() throws ConfigurationException {
if (mapStorages == null) {
mapStorages = new HashMap<>();
if (maps == null) {
for (MapConfig mapConfig : getRenderConfig().getMapConfigs()) {
mapStorages.put(mapConfig.getId(), getStorage(mapConfig.getStorage()));
}
} else {
for (BmMap map : maps.values()) {
mapStorages.put(map.getId(), map.getStorage());
}
}
}
return mapStorages;
}
private synchronized void loadWorldsAndMaps() throws ConfigurationException, InterruptedException {
maps = new HashMap<>(); maps = new HashMap<>();
worlds = new HashMap<>(); worlds = new HashMap<>();
@ -141,7 +186,7 @@ private synchronized void loadWorldsAndMaps() throws IOException, InterruptedExc
File worldFolder = new File(mapConfig.getWorldPath()); File worldFolder = new File(mapConfig.getWorldPath());
if (!worldFolder.exists() || !worldFolder.isDirectory()) { if (!worldFolder.exists() || !worldFolder.isDirectory()) {
Logger.global.logWarning("Failed to load map '" + id + "': '" + worldFolder.getCanonicalPath() + "' does not exist or is no directory!"); Logger.global.logWarning("Failed to load map '" + id + "': '" + worldFolder.getAbsolutePath() + "' does not exist or is no directory!");
continue; continue;
} }
@ -158,36 +203,85 @@ private synchronized void loadWorldsAndMaps() throws IOException, InterruptedExc
try { try {
world = MCAWorld.load(worldFolder.toPath(), worldUUID, worldNameProvider.apply(worldUUID), mapConfig.getWorldSkyLight(), mapConfig.isIgnoreMissingLightData()); world = MCAWorld.load(worldFolder.toPath(), worldUUID, worldNameProvider.apply(worldUUID), mapConfig.getWorldSkyLight(), mapConfig.isIgnoreMissingLightData());
worlds.put(worldUUID, world); worlds.put(worldUUID, world);
} catch (MissingResourcesException e) {
throw e; // rethrow this to stop loading and display resource-missing message
} catch (IOException e) { } catch (IOException e) {
Logger.global.logError("Failed to load map '" + id + "'!", e); Logger.global.logError("Failed to load map '" + id + "'!", e);
continue; continue;
} }
} }
Storage storage = new FileStorage( Storage storage = getStorage(mapConfig.getStorage());
getRenderConfig().getWebRoot().toPath().resolve("data"), try {
mapConfig.getCompression() BmMap map = new BmMap(
); id,
name,
world,
storage,
getResourcePack(),
mapConfig
);
BmMap map = new BmMap( maps.put(id, map);
id, } catch (IOException ex) {
name, Logger.global.logError("Failed to load map '" + id + "'!", ex);
world, }
storage,
getResourcePack(),
mapConfig
);
maps.put(id, map);
} }
worlds = Collections.unmodifiableMap(worlds); worlds = Collections.unmodifiableMap(worlds);
maps = Collections.unmodifiableMap(maps); maps = Collections.unmodifiableMap(maps);
} }
public synchronized ResourcePack getResourcePack() throws IOException, InterruptedException { public synchronized Storage getStorage(String id) throws ConfigurationException {
Storage storage = storages.get(id);
if (storage == null) {
storage = loadStorage(id);
storages.put(id, storage);
}
return storage;
}
private synchronized Storage loadStorage(String id) throws ConfigurationException {
Logger.global.logInfo("Loading storage '" + id + "'...");
Path storageFolder = Paths.get("storages");
Path storageConfigFolder = configManager.getConfigRoot().resolve(storageFolder);
if (!Files.exists(storageConfigFolder)){
try {
Files.createDirectories(storageConfigFolder);
Files.copy(
Objects.requireNonNull(BlueMapService.class
.getResourceAsStream("/de/bluecolored/bluemap/config/storages/file.conf")),
storageConfigFolder.resolve("file.conf")
);
Files.copy(
Objects.requireNonNull(BlueMapService.class
.getResourceAsStream("/de/bluecolored/bluemap/config/storages/sql.conf")),
storageConfigFolder.resolve("sql.conf")
);
} catch (IOException | NullPointerException ex) {
Logger.global.logWarning("Failed to create default storage-configuration-files: " + ex);
}
}
try {
ConfigurationNode node = configManager.loadConfig(storageFolder.resolve(id));
StorageConfig storageConfig = Objects.requireNonNull(node.get(StorageConfig.class));
Storage storage = storageConfig.getStorageType().create(node);
storage.initialize();
return storage;
} catch (Exception ex) {
throw new ConfigurationException(
"BlueMap tried to create the storage '" + id + "' but something went wrong.\n" +
"Check if that storage is configured correctly.",
ex
);
}
}
public synchronized ResourcePack getResourcePack() throws ConfigurationException, InterruptedException {
if (resourcePack == null) { if (resourcePack == null) {
File defaultResourceFile = new File(getCoreConfig().getDataFolder(), "minecraft-client-" + minecraftVersion.getResource().getVersion().getVersionString() + ".jar"); File defaultResourceFile = new File(getCoreConfig().getDataFolder(), "minecraft-client-" + minecraftVersion.getResource().getVersion().getVersionString() + ".jar");
File resourceExtensionsFile = new File(getCoreConfig().getDataFolder(), "resourceExtensions.zip"); File resourceExtensionsFile = new File(getCoreConfig().getDataFolder(), "resourceExtensions.zip");
@ -195,7 +289,15 @@ public synchronized ResourcePack getResourcePack() throws IOException, Interrupt
File textureExportFile = new File(getRenderConfig().getWebRoot(), "data" + File.separator + "textures.json"); File textureExportFile = new File(getRenderConfig().getWebRoot(), "data" + File.separator + "textures.json");
File resourcePackFolder = new File(configFolder, "resourcepacks"); File resourcePackFolder = new File(configFolder, "resourcepacks");
FileUtils.forceMkdir(resourcePackFolder); try {
FileUtils.forceMkdir(resourcePackFolder);
} catch (IOException ex) {
throw new ConfigurationException(
"BlueMap failed to create this folder:\n" +
resourcePackFolder + "\n" +
"Does BlueMap has sufficient permissions?",
ex);
}
if (!defaultResourceFile.exists()) { if (!defaultResourceFile.exists()) {
if (getCoreConfig().isDownloadAccepted()) { if (getCoreConfig().isDownloadAccepted()) {
@ -205,8 +307,8 @@ public synchronized ResourcePack getResourcePack() throws IOException, Interrupt
Logger.global.logInfo("Downloading " + minecraftVersion.getResource().getClientUrl() + " to " + defaultResourceFile + " ..."); Logger.global.logInfo("Downloading " + minecraftVersion.getResource().getClientUrl() + " to " + defaultResourceFile + " ...");
FileUtils.forceMkdirParent(defaultResourceFile); FileUtils.forceMkdirParent(defaultResourceFile);
FileUtils.copyURLToFile(new URL(minecraftVersion.getResource().getClientUrl()), defaultResourceFile, 10000, 10000); FileUtils.copyURLToFile(new URL(minecraftVersion.getResource().getClientUrl()), defaultResourceFile, 10000, 10000);
} catch (IOException e) { } catch (IOException ex) {
throw new IOException("Failed to download resources!", e); throw new ConfigurationException("Failed to download resources!", ex);
} }
} else { } else {
@ -216,12 +318,21 @@ public synchronized ResourcePack getResourcePack() throws IOException, Interrupt
Logger.global.logInfo("Loading resources..."); Logger.global.logInfo("Loading resources...");
if (resourceExtensionsFile.exists()) FileUtils.forceDelete(resourceExtensionsFile); try {
FileUtils.forceMkdirParent(resourceExtensionsFile); if (resourceExtensionsFile.exists()) FileUtils.forceDelete(resourceExtensionsFile);
URL resourceExtensionsUrl = Objects.requireNonNull( FileUtils.forceMkdirParent(resourceExtensionsFile);
Plugin.class.getResource("/de/bluecolored/bluemap/" + minecraftVersion.getResource().getResourcePrefix() + "/resourceExtensions.zip") URL resourceExtensionsUrl = Objects.requireNonNull(
); Plugin.class.getResource(
FileUtils.copyURLToFile(resourceExtensionsUrl, resourceExtensionsFile, 10000, 10000); "/de/bluecolored/bluemap/" + minecraftVersion.getResource().getResourcePrefix() +
"/resourceExtensions.zip")
);
FileUtils.copyURLToFile(resourceExtensionsUrl, resourceExtensionsFile, 10000, 10000);
} catch (IOException ex) {
throw new ConfigurationException(
"Failed to create resourceExtensions.zip!\n" +
"Does BlueMap has sufficient write permissions?",
ex);
}
//find more resource packs //find more resource packs
File[] resourcePacks = resourcePackFolder.listFiles(); File[] resourcePacks = resourcePackFolder.listFiles();
@ -238,8 +349,8 @@ public synchronized ResourcePack getResourcePack() throws IOException, Interrupt
if (textureExportFile.exists()) resourcePack.loadTextureFile(textureExportFile); if (textureExportFile.exists()) resourcePack.loadTextureFile(textureExportFile);
resourcePack.load(resources); resourcePack.load(resources);
resourcePack.saveTextureFile(textureExportFile); resourcePack.saveTextureFile(textureExportFile);
} catch (ParseResourceException e) { } catch (IOException | ParseResourceException e) {
throw new IOException("Failed to parse resources!", e); throw new ConfigurationException("Failed to parse resources!", e);
} }
} }
@ -247,17 +358,18 @@ public synchronized ResourcePack getResourcePack() throws IOException, Interrupt
return resourcePack; return resourcePack;
} }
public synchronized ConfigManager getConfigManager() { @Deprecated
return configManager; public synchronized de.bluecolored.bluemap.core.config.old.ConfigManager getConfigManagerOld() {
return configManagerOld;
} }
public File getCoreConfigFile() { public File getCoreConfigFile() {
return new File(configFolder, "core.conf"); return new File(configFolder, "core.conf");
} }
public synchronized CoreConfig getCoreConfig() throws IOException { public synchronized CoreConfig getCoreConfig() throws ConfigurationException {
if (coreConfig == null) { if (coreConfig == null) {
coreConfig = new CoreConfig(configManager.loadOrCreate( coreConfig = new CoreConfig(configManagerOld.loadOrCreate(
getCoreConfigFile(), getCoreConfigFile(),
Plugin.class.getResource("/de/bluecolored/bluemap/core.conf"), Plugin.class.getResource("/de/bluecolored/bluemap/core.conf"),
Plugin.class.getResource("/de/bluecolored/bluemap/core-defaults.conf"), Plugin.class.getResource("/de/bluecolored/bluemap/core-defaults.conf"),
@ -273,9 +385,9 @@ public File getRenderConfigFile() {
return new File(configFolder, "render.conf"); return new File(configFolder, "render.conf");
} }
public synchronized RenderConfig getRenderConfig() throws IOException { public synchronized RenderConfig getRenderConfig() throws ConfigurationException {
if (renderConfig == null) { if (renderConfig == null) {
renderConfig = new RenderConfig(configManager.loadOrCreate( renderConfig = new RenderConfig(configManagerOld.loadOrCreate(
getRenderConfigFile(), getRenderConfigFile(),
Plugin.class.getResource("/de/bluecolored/bluemap/render.conf"), Plugin.class.getResource("/de/bluecolored/bluemap/render.conf"),
Plugin.class.getResource("/de/bluecolored/bluemap/render-defaults.conf"), Plugin.class.getResource("/de/bluecolored/bluemap/render-defaults.conf"),
@ -291,9 +403,9 @@ public File getWebServerConfigFile() {
return new File(configFolder, "webserver.conf"); return new File(configFolder, "webserver.conf");
} }
public synchronized WebServerConfig getWebServerConfig() throws IOException { public synchronized WebServerConfig getWebServerConfig() throws ConfigurationException {
if (webServerConfig == null) { if (webServerConfig == null) {
webServerConfig = new WebServerConfig(configManager.loadOrCreate( webServerConfig = new WebServerConfig(configManagerOld.loadOrCreate(
getWebServerConfigFile(), getWebServerConfigFile(),
Plugin.class.getResource("/de/bluecolored/bluemap/webserver.conf"), Plugin.class.getResource("/de/bluecolored/bluemap/webserver.conf"),
Plugin.class.getResource("/de/bluecolored/bluemap/webserver-defaults.conf"), Plugin.class.getResource("/de/bluecolored/bluemap/webserver-defaults.conf"),
@ -309,4 +421,8 @@ public File getConfigFolder() {
return configFolder; return configFolder;
} }
private interface ThrowingFunction<T, R, E extends Throwable> {
R apply(T t) throws E;
}
} }

View File

@ -24,8 +24,13 @@
*/ */
package de.bluecolored.bluemap.common; package de.bluecolored.bluemap.common;
import java.io.IOException; import de.bluecolored.bluemap.core.config.ConfigurationException;
public class MissingResourcesException extends IOException { public class MissingResourcesException extends ConfigurationException {
private static final long serialVersionUID = 2084565069965755048L; private static final long serialVersionUID = 2084565069965755048L;
public MissingResourcesException() {
super("BlueMap is missing important resources!\n" +
"You must accept the required file download in order for BlueMap to work!");
}
} }

View File

@ -35,11 +35,13 @@
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask; import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.RenderManager; import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.web.FileRequestHandler; import de.bluecolored.bluemap.common.web.FileRequestHandler;
import de.bluecolored.bluemap.common.web.MapStorageRequestHandler;
import de.bluecolored.bluemap.core.BlueMap; import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.MinecraftVersion; import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.config.CoreConfig; import de.bluecolored.bluemap.core.config.ConfigurationException;
import de.bluecolored.bluemap.core.config.RenderConfig; import de.bluecolored.bluemap.core.config.old.CoreConfig;
import de.bluecolored.bluemap.core.config.WebServerConfig; import de.bluecolored.bluemap.core.config.old.RenderConfig;
import de.bluecolored.bluemap.core.config.old.WebServerConfig;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.debug.StateDumper; import de.bluecolored.bluemap.core.debug.StateDumper;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
@ -47,6 +49,7 @@
import de.bluecolored.bluemap.core.metrics.Metrics; import de.bluecolored.bluemap.core.metrics.Metrics;
import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; import de.bluecolored.bluemap.core.resourcepack.ParseResourceException;
import de.bluecolored.bluemap.core.util.FileUtils; import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.util.MappableFunction;
import de.bluecolored.bluemap.core.webserver.HttpRequestHandler; import de.bluecolored.bluemap.core.webserver.HttpRequestHandler;
import de.bluecolored.bluemap.core.webserver.WebServer; import de.bluecolored.bluemap.core.webserver.WebServer;
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
@ -116,9 +119,10 @@ public void load() throws IOException, ParseResourceException {
coreConfig = blueMap.getCoreConfig(); coreConfig = blueMap.getCoreConfig();
renderConfig = blueMap.getRenderConfig(); renderConfig = blueMap.getRenderConfig();
webServerConfig = blueMap.getWebServerConfig(); webServerConfig = blueMap.getWebServerConfig();
blueMap.getMapStorages();
//load plugin config //load plugin config
pluginConfig = new PluginConfig(blueMap.getConfigManager().loadOrCreate( pluginConfig = new PluginConfig(blueMap.getConfigManagerOld().loadOrCreate(
new File(serverInterface.getConfigFolder(), "plugin.conf"), new File(serverInterface.getConfigFolder(), "plugin.conf"),
Plugin.class.getResource("/de/bluecolored/bluemap/plugin.conf"), Plugin.class.getResource("/de/bluecolored/bluemap/plugin.conf"),
Plugin.class.getResource("/de/bluecolored/bluemap/plugin-defaults.conf"), Plugin.class.getResource("/de/bluecolored/bluemap/plugin-defaults.conf"),
@ -137,26 +141,6 @@ public void load() throws IOException, ParseResourceException {
pluginState = new PluginState(); pluginState = new PluginState();
} }
//create and start webserver
if (webServerConfig.isWebserverEnabled()) {
FileUtils.mkDirs(webServerConfig.getWebRoot());
HttpRequestHandler requestHandler = new FileRequestHandler(webServerConfig.getWebRoot().toPath(), "BlueMap v" + BlueMap.VERSION);
//inject live api if enabled
if (pluginConfig.isLiveUpdatesEnabled()) {
requestHandler = new LiveAPIRequestHandler(serverInterface, pluginConfig, requestHandler);
}
webServer = new WebServer(
webServerConfig.getWebserverBindAddress(),
webServerConfig.getWebserverPort(),
webServerConfig.getWebserverMaxConnections(),
requestHandler,
false
);
webServer.start();
}
//try load resources //try load resources
try { try {
blueMap.getResourcePack(); blueMap.getResourcePack();
@ -182,6 +166,31 @@ public void load() throws IOException, ParseResourceException {
return; return;
} }
//create and start webserver
if (webServerConfig.isWebserverEnabled()) {
FileUtils.mkDirs(webServerConfig.getWebRoot());
HttpRequestHandler requestHandler = new FileRequestHandler(webServerConfig.getWebRoot().toPath(), "BlueMap v" + BlueMap.VERSION);
//use map-storage to provide map-tiles
requestHandler = new MapStorageRequestHandler(
MappableFunction.of(maps::get).mapNullable(BmMap::getStorage),
requestHandler);
//inject live api if enabled
if (pluginConfig.isLiveUpdatesEnabled()) {
requestHandler = new LiveAPIRequestHandler(serverInterface, pluginConfig, requestHandler);
}
webServer = new WebServer(
webServerConfig.getWebserverBindAddress(),
webServerConfig.getWebserverPort(),
webServerConfig.getWebserverMaxConnections(),
requestHandler,
false
);
webServer.start();
}
//initialize render manager //initialize render manager
renderManager = new RenderManager(); renderManager = new RenderManager();
@ -275,6 +284,9 @@ public void run() {
//done //done
loaded = true; loaded = true;
} }
} catch (ConfigurationException ex) {
Logger.global.logWarning(ex.getFormattedExplanation());
throw new IOException(ex);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
Logger.global.logWarning("Loading has been interrupted!"); Logger.global.logWarning("Loading has been interrupted!");
@ -310,12 +322,30 @@ public void unload() {
regionFileWatchServices = null; regionFileWatchServices = null;
//stop services //stop services
if (renderManager != null) renderManager.stop(); if (renderManager != null){
renderManager.stop();
try {
renderManager.awaitShutdown();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
renderManager = null; renderManager = null;
if (webServer != null) webServer.close(); if (webServer != null) webServer.close();
webServer = null; webServer = null;
//close storages
if (maps != null) {
maps.values().forEach(map -> {
try {
map.getStorage().close();
} catch (IOException ex) {
Logger.global.logWarning("Failed to close map-storage for map '" + map.getId() + "': " + ex);
}
});
}
//clear resources and configs //clear resources and configs
blueMap = null; blueMap = null;
worlds = null; worlds = null;

View File

@ -431,7 +431,7 @@ public int reloadCommand(CommandContext<S> context) {
source.sendMessage(Text.of(TextColor.RED, "Could not load BlueMap! See the console for details!")); source.sendMessage(Text.of(TextColor.RED, "Could not load BlueMap! See the console for details!"));
} }
} catch (IOException | ParseResourceException | RuntimeException ex) { } catch (Exception ex) {
Logger.global.logError("Failed to reload BlueMap!", ex); Logger.global.logError("Failed to reload BlueMap!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an error reloading BlueMap! See the console for details!")); source.sendMessage(Text.of(TextColor.RED, "There was an error reloading BlueMap! See the console for details!"));

View File

@ -26,7 +26,7 @@
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.map.BmMap; import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.FileStorage; import de.bluecolored.bluemap.core.storage.file.FileStorage;
import de.bluecolored.bluemap.core.storage.Storage; import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileUtils; import de.bluecolored.bluemap.core.util.FileUtils;

View File

@ -28,26 +28,18 @@
import de.bluecolored.bluemap.core.webserver.HttpRequestHandler; import de.bluecolored.bluemap.core.webserver.HttpRequestHandler;
import de.bluecolored.bluemap.core.webserver.HttpResponse; import de.bluecolored.bluemap.core.webserver.HttpResponse;
import de.bluecolored.bluemap.core.webserver.HttpStatusCode; import de.bluecolored.bluemap.core.webserver.HttpStatusCode;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import java.io.*; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.GregorianCalendar; import java.util.*;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class FileRequestHandler implements HttpRequestHandler { public class FileRequestHandler implements HttpRequestHandler {
private static final long DEFLATE_MIN_SIZE = 10L * 1024L;
private static final long DEFLATE_MAX_SIZE = 10L * 1024L * 1024L;
private static final long INFLATE_MAX_SIZE = 10L * 1024L * 1024L;
private final Path webRoot; private final Path webRoot;
private final String serverName; private final String serverName;
@ -86,17 +78,13 @@ private HttpResponse generateResponse(HttpRequest request) {
if (path.startsWith("/")) path = path.substring(1); if (path.startsWith("/")) path = path.substring(1);
if (path.endsWith("/")) path = path.substring(0, path.length() - 1); if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
Path filePath = webRoot; Path filePath;
try { try {
filePath = webRoot.resolve(path); filePath = webRoot.resolve(path);
} catch (InvalidPathException e){ } catch (InvalidPathException e){
return new HttpResponse(HttpStatusCode.NOT_FOUND); return new HttpResponse(HttpStatusCode.NOT_FOUND);
} }
// can we use deflation?
boolean isDeflationPossible = request.getLowercaseHeader("Accept-Encoding").contains("gzip");
boolean isDeflated = false;
// check if file is in web-root // check if file is in web-root
if (!filePath.normalize().startsWith(webRoot)){ if (!filePath.normalize().startsWith(webRoot)){
return new HttpResponse(HttpStatusCode.FORBIDDEN); return new HttpResponse(HttpStatusCode.FORBIDDEN);
@ -111,38 +99,20 @@ private HttpResponse generateResponse(HttpRequest request) {
return response; return response;
} }
if (!file.exists() || file.isDirectory()){ // default to index.html
file = new File(filePath.toString() + ".gz");
isDeflated = true;
}
if (!file.exists() || file.isDirectory()){ if (!file.exists() || file.isDirectory()){
file = new File(filePath.toString() + "/index.html"); file = new File(filePath.toString() + "/index.html");
isDeflated = false;
}
if (!file.exists() || file.isDirectory()){
file = new File(filePath.toString() + "/index.html.gz");
isDeflated = true;
} }
// send empty tile-file if tile not exists
if (!file.exists() && file.toPath().startsWith(webRoot.resolve("data"))){ if (!file.exists() && file.toPath().startsWith(webRoot.resolve("data"))){
file = emptyTileFile; file = emptyTileFile;
isDeflated = false;
} }
if (!file.exists() || file.isDirectory()) { if (!file.exists() || file.isDirectory()) {
return new HttpResponse(HttpStatusCode.NOT_FOUND); return new HttpResponse(HttpStatusCode.NOT_FOUND);
} }
if (isDeflationPossible && (!file.getName().endsWith(".gz"))){
File deflatedFile = new File(file.getAbsolutePath() + ".gz");
if (deflatedFile.exists()){
file = deflatedFile;
isDeflated = true;
}
}
// check if file is still in web-root and is not a directory // check if file is still in web-root and is not a directory
if (!file.toPath().normalize().startsWith(webRoot) || file.isDirectory()){ if (!file.toPath().normalize().startsWith(webRoot) || file.isDirectory()){
return new HttpResponse(HttpStatusCode.FORBIDDEN); return new HttpResponse(HttpStatusCode.FORBIDDEN);
@ -178,76 +148,17 @@ private HttpResponse generateResponse(HttpRequest request) {
//add content type header //add content type header
String filetype = file.getName(); String filetype = file.getName();
if (filetype.endsWith(".gz")) filetype = filetype.substring(0, filetype.length() - 3);
int pointIndex = filetype.lastIndexOf('.'); int pointIndex = filetype.lastIndexOf('.');
if (pointIndex >= 0) filetype = filetype.substring(pointIndex + 1); if (pointIndex >= 0) filetype = filetype.substring(pointIndex + 1);
String contentType = toContentType(filetype);
String contentType = "text/plain";
switch (filetype) {
case "json" :
contentType = "application/json";
break;
case "png" :
contentType = "image/png";
break;
case "jpg" :
case "jpeg" :
case "jpe" :
contentType = "image/jpeg";
break;
case "svg" :
contentType = "image/svg+xml";
break;
case "css" :
contentType = "text/css";
break;
case "js" :
contentType = "text/javascript";
break;
case "html" :
case "htm" :
case "shtml" :
contentType = "text/html";
break;
case "xml" :
contentType = "text/xml";
break;
}
response.addHeader("Content-Type", contentType); response.addHeader("Content-Type", contentType);
//send response
try { try {
if (isDeflated){ response.setData(new FileInputStream(file));
if (isDeflationPossible || file.length() > INFLATE_MAX_SIZE){ return response;
response.addHeader("Content-Encoding", "gzip");
response.setData(new FileInputStream(file));
return response;
} else {
response.setData(new GZIPInputStream(new FileInputStream(file)));
return response;
}
} else {
if (isDeflationPossible && file.length() > DEFLATE_MIN_SIZE && file.length() < DEFLATE_MAX_SIZE){
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
GZIPOutputStream zos = new GZIPOutputStream(byteOut);
IOUtils.copyLarge(fis, zos);
zos.close();
fis.close();
byte[] compressedData = byteOut.toByteArray();
response.setData(new ByteArrayInputStream(compressedData));
response.addHeader("Content-Encoding", "gzip");
return response;
} else {
response.setData(new FileInputStream(file));
return response;
}
}
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
return new HttpResponse(HttpStatusCode.NOT_FOUND); return new HttpResponse(HttpStatusCode.NOT_FOUND);
} catch (IOException e) {
return new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
} }
} }
@ -258,20 +169,21 @@ private static String timestampToString(long time){
private static long stringToTimestamp(String timeString) throws IllegalArgumentException { private static long stringToTimestamp(String timeString) throws IllegalArgumentException {
try { try {
int day = Integer.parseInt(timeString.substring(5, 7)); int day = Integer.parseInt(timeString.substring(5, 7));
int month = 1;
int month = Calendar.JANUARY;
switch (timeString.substring(8, 11)){ switch (timeString.substring(8, 11)){
case "Jan" : month = 0; break; case "Jan" : month = Calendar.JANUARY; break;
case "Feb" : month = 1; break; case "Feb" : month = Calendar.FEBRUARY; break;
case "Mar" : month = 2; break; case "Mar" : month = Calendar.MARCH; break;
case "Apr" : month = 3; break; case "Apr" : month = Calendar.APRIL; break;
case "May" : month = 4; break; case "May" : month = Calendar.MAY; break;
case "Jun" : month = 5; break; case "Jun" : month = Calendar.JUNE; break;
case "Jul" : month = 6; break; case "Jul" : month = Calendar.JULY; break;
case "Aug" : month = 7; break; case "Aug" : month = Calendar.AUGUST; break;
case "Sep" : month = 8; break; case "Sep" : month = Calendar.SEPTEMBER; break;
case "Oct" : month = 9; break; case "Oct" : month = Calendar.OCTOBER; break;
case "Nov" : month = 10; break; case "Nov" : month = Calendar.NOVEMBER; break;
case "Dec" : month = 11; break; case "Dec" : month = Calendar.DECEMBER; break;
} }
int year = Integer.parseInt(timeString.substring(12, 16)); int year = Integer.parseInt(timeString.substring(12, 16));
int hour = Integer.parseInt(timeString.substring(17, 19)); int hour = Integer.parseInt(timeString.substring(17, 19));
@ -285,4 +197,39 @@ private static long stringToTimestamp(String timeString) throws IllegalArgumentE
} }
} }
private static String toContentType(String fileEnding) {
String contentType = "text/plain";
switch (fileEnding) {
case "json" :
contentType = "application/json";
break;
case "png" :
contentType = "image/png";
break;
case "jpg" :
case "jpeg" :
case "jpe" :
contentType = "image/jpeg";
break;
case "svg" :
contentType = "image/svg+xml";
break;
case "css" :
contentType = "text/css";
break;
case "js" :
contentType = "text/javascript";
break;
case "html" :
case "htm" :
case "shtml" :
contentType = "text/html";
break;
case "xml" :
contentType = "text/xml";
break;
}
return contentType;
}
} }

View File

@ -0,0 +1,219 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.storage.*;
import de.bluecolored.bluemap.core.webserver.HttpRequest;
import de.bluecolored.bluemap.core.webserver.HttpRequestHandler;
import de.bluecolored.bluemap.core.webserver.HttpResponse;
import de.bluecolored.bluemap.core.webserver.HttpStatusCode;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MapStorageRequestHandler implements HttpRequestHandler {
private static final Pattern TILE_PATTERN = Pattern.compile("data/([^/]+)/([^/]+)/x(-?[\\d/]+)z(-?[\\d/]+).*");
private static final Pattern META_PATTERN = Pattern.compile("data/([^/]+)/(.*)");
private final Function<? super String, Storage> mapStorageProvider;
private final HttpRequestHandler notFoundHandler;
public MapStorageRequestHandler(Function<? super String, Storage> mapStorageProvider, HttpRequestHandler notFoundHandler) {
this.mapStorageProvider = mapStorageProvider;
this.notFoundHandler = notFoundHandler;
}
@Override
public HttpResponse handle(HttpRequest request) {
String path = request.getPath();
//normalize path
if (path.startsWith("/")) path = path.substring(1);
if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
try {
// provide map-tiles
Matcher tileMatcher = TILE_PATTERN.matcher(path);
if (tileMatcher.matches()) {
String mapId = tileMatcher.group(1);
String tileTypeId = tileMatcher.group(2);
Storage storage = mapStorageProvider.apply(mapId);
if (storage != null) {
TileType tileType = TileType.forTypeId(tileTypeId);
int x = Integer.parseInt(tileMatcher.group(3).replace("/", ""));
int z = Integer.parseInt(tileMatcher.group(4).replace("/", ""));
Optional<TileData> optTileData = storage.readMapTileData(mapId, tileType, new Vector2i(x, z));
if (optTileData.isPresent()) {
TileData tileData = optTileData.get();
// check e-tag
String eTag = calculateETag(path, tileData);
Set<String> etagStringSet = request.getHeader("If-None-Match");
if (!etagStringSet.isEmpty()){
if(etagStringSet.iterator().next().equals(eTag)) {
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
}
// check modified-since
long lastModified = tileData.getLastModified();
Set<String> modStringSet = request.getHeader("If-Modified-Since");
if (!modStringSet.isEmpty()){
try {
long since = stringToTimestamp(modStringSet.iterator().next());
if (since + 1000 >= lastModified){
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
} catch (IllegalArgumentException ignored){}
}
CompressedInputStream compressedIn = tileData.readMapTile();
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("ETag", eTag);
if (lastModified > 0)
response.addHeader("Last-Modified", timestampToString(lastModified));
response.addHeader("Content-Type", "application/json");
writeToResponse(compressedIn, response, request);
return response;
}
}
}
// provide meta-data
Matcher metaMatcher = META_PATTERN.matcher(path);
if (metaMatcher.matches()) {
String mapId = tileMatcher.group(1);
String metaFilePath = tileMatcher.group(2);
Storage storage = mapStorageProvider.apply(mapId);
if (storage != null) {
MetaType metaType = null;
for (MetaType mt : MetaType.values()) {
if (mt.getFilePath().equals(metaFilePath)) {
metaType = mt;
break;
}
}
if (metaType != null) {
Optional<CompressedInputStream> optIn = storage.readMeta(mapId, metaType);
if (optIn.isPresent()) {
CompressedInputStream compressedIn = optIn.get();
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("Content-Type", metaType.getContentType());
writeToResponse(compressedIn, response, request);
return response;
}
}
}
}
} catch (NumberFormatException | NoSuchElementException ignore){
} catch (IOException ex) {
Logger.global.logError("Failed to read map-tile for web-request.", ex);
return new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
return this.notFoundHandler.handle(request);
}
private String calculateETag(String path, TileData tileData) {
return Long.toHexString(tileData.getSize()) + Integer.toHexString(path.hashCode()) + Long.toHexString(tileData.getLastModified());
}
private void writeToResponse(CompressedInputStream data, HttpResponse response, HttpRequest request) throws IOException {
Compression compression = data.getCompression();
if (
compression != Compression.NONE &&
request.getLowercaseHeader("Accept-Encoding").contains(compression.getTypeId())
) {
response.addHeader("Content-Encoding", compression.getTypeId());
response.setData(data);
} else if (
compression != Compression.GZIP &&
request.getLowercaseHeader("Accept-Encoding").contains(Compression.GZIP.getTypeId())
) {
response.addHeader("Content-Encoding", Compression.GZIP.getTypeId());
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
try (OutputStream os = Compression.GZIP.compress(byteOut)) {
IOUtils.copyLarge(data.decompress(), os);
}
byte[] compressedData = byteOut.toByteArray();
response.setData(new ByteArrayInputStream(compressedData));
} else {
response.setData(data.decompress());
}
}
private static String timestampToString(long time){
return DateFormatUtils.format(time, "EEE, dd MMM yyy HH:mm:ss 'GMT'", TimeZone.getTimeZone("GMT"), Locale.ENGLISH);
}
private static long stringToTimestamp(String timeString) throws IllegalArgumentException {
try {
int day = Integer.parseInt(timeString.substring(5, 7));
int month = Calendar.JANUARY;
switch (timeString.substring(8, 11)){
case "Jan" : month = Calendar.JANUARY; break;
case "Feb" : month = Calendar.FEBRUARY; break;
case "Mar" : month = Calendar.MARCH; break;
case "Apr" : month = Calendar.APRIL; break;
case "May" : month = Calendar.MAY; break;
case "Jun" : month = Calendar.JUNE; break;
case "Jul" : month = Calendar.JULY; break;
case "Aug" : month = Calendar.AUGUST; break;
case "Sep" : month = Calendar.SEPTEMBER; break;
case "Oct" : month = Calendar.OCTOBER; break;
case "Nov" : month = Calendar.NOVEMBER; break;
case "Dec" : month = Calendar.DECEMBER; break;
}
int year = Integer.parseInt(timeString.substring(12, 16));
int hour = Integer.parseInt(timeString.substring(17, 19));
int min = Integer.parseInt(timeString.substring(20, 22));
int sec = Integer.parseInt(timeString.substring(23, 25));
GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
cal.set(year, month, day, hour, min, sec);
return cal.getTimeInMillis();
} catch (NumberFormatException | IndexOutOfBoundsException e){
throw new IllegalArgumentException(e);
}
}
}

View File

@ -26,7 +26,7 @@
import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3f; import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.config.MapConfig; import de.bluecolored.bluemap.core.config.old.MapConfig;
import de.bluecolored.bluemap.core.map.BmMap; import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.util.FileUtils; import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.util.MathUtils; import de.bluecolored.bluemap.core.util.MathUtils;

View File

@ -0,0 +1,20 @@
## ##
## BlueMap ##
## Storage-Config ##
## ##
# The storage-type of this storage.
# Depending on this setting, different config-entries are allowed/expected in this config file.
# Don't change this value! (If you want a different storage-type, check out the other example-configs)
storage-type: FILE
# The path to the folder on your file-system where bluemap will save the rendered map
# The default is: "bluemap/web/data"
root: "bluemap/web/data"
# The compression-type that bluemap will use to compress generated map-data.
# Available compression-types are:
# - GZIP
# - NONE
# The default is: GZIP
compression: GZIP

View File

@ -0,0 +1,39 @@
## ##
## BlueMap ##
## Storage-Config ##
## ##
# The storage-type of this storage.
# Depending on this setting, different config-entries are allowed/expected in this config file.
# Don't change this value! (If you want a different storage-type, check out the other example-configs)
storage-type: SQL
# The JDBC-Connection URL that is used to connect to the database.
# The format for this url is: jdbc:[driver]://[host]:[port]/[database]
dbUrl: "jdbc:mysql://localhost:3306/bluemap"
# The user that is used to connect to the database
user: "root"
# The password that is used to connect to the database
# (If this is empty, no password is used to connect to the database)
password: ""
# This can be used to load a custom jdbc-driver from a .jar file.
# E.g. if your runtime-environment is not already providing the sql-driver you need,
# you could download the MariaDB JDBC-Connector from https://mariadb.com/downloads/connectors/connectors-data-access/java8-connector/
# place it in the './bluemap' folder and use is like this:
#driverJar: "bluemap/mariadb-java-client-2.7.4.jar"
# This is the driver-class that bluemap will try to load and use.
# Check the documentation of the driver you are using if you don't know this.
# Leaving this commented means that bluemap automatically tries to find a suitable driver in your classpath.
# (If you added a custom driverJar above, you HAVE TO set the correct class name here)
#driverClass: "org.mariadb.jdbc.Driver"
# The compression-type that bluemap will use to compress generated map-data.
# Available compression-types are:
# - GZIP
# - NONE
# The default is: GZIP
compression: GZIP

View File

@ -7,6 +7,7 @@ dependencies {
api 'org.spongepowered:configurate-hocon:4.1.1' api 'org.spongepowered:configurate-hocon:4.1.1'
api 'org.spongepowered:configurate-gson:4.1.1' api 'org.spongepowered:configurate-gson:4.1.1'
api 'com.github.Querz:NBT:4.0' api 'com.github.Querz:NBT:4.0'
api 'org.apache.commons:commons-dbcp2:2.9.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2'
} }

View File

@ -24,110 +24,76 @@
*/ */
package de.bluecolored.bluemap.core.config; package de.bluecolored.bluemap.core.config;
import org.spongepowered.configurate.ConfigurateException;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.util.FileUtils;
import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.gson.GsonConfigurationLoader; import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import org.spongepowered.configurate.hocon.HoconConfigurationLoader; import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
import org.spongepowered.configurate.loader.ConfigurationLoader; import org.spongepowered.configurate.loader.ConfigurationLoader;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.LocalDateTime; import java.nio.file.Path;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
public class ConfigManager { public class ConfigManager {
private static final Set<Placeholder> CONFIG_PLACEHOLDERS = new HashSet<>(); private static final String[] CONFIG_FILE_ENDINGS = new String[] {
".conf",
".json"
};
static { private final Path configRoot;
CONFIG_PLACEHOLDERS.add(new Placeholder("version", BlueMap.VERSION));
CONFIG_PLACEHOLDERS.add(new Placeholder("datetime-iso", () -> LocalDateTime.now().withNano(0).toString())); public ConfigManager(Path configRoot) {
this.configRoot = configRoot;
} }
/** public ConfigurationNode loadConfig(Path rawPath) throws ConfigurationException {
* Loads or creates a config file for BlueMap. Path path = findConfigPath(configRoot.resolve(rawPath));
*
* @param configFile The config file to load
* @param defaultConfig The default config that is used as a template if the config file does not exist (can be null)
* @param defaultValues The default values used if a key is not present in the config (can be null)
* @param usePlaceholders Whether to replace placeholders from the defaultConfig if it is newly generated
* @param generateEmptyConfig Whether to generate an empty config file if no default config is provided
* @return The loaded configuration node
* @throws IOException if an IOException occurs while loading
*/
public ConfigurationNode loadOrCreate(File configFile, URL defaultConfig, URL defaultValues, boolean usePlaceholders, boolean generateEmptyConfig) throws IOException {
ConfigurationNode configNode; if (!Files.exists(path)) {
if (!configFile.exists()) { throw new ConfigurationException(
FileUtils.mkDirsParent(configFile); "BlueMap tried to find this file, but it does not exist:\n" +
path);
if (defaultConfig != null) {
//load content of default config
String content;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(defaultConfig.openStream(), StandardCharsets.UTF_8))){
content = reader.lines().collect(Collectors.joining("\n"));
}
//replace placeholders if enabled
if (usePlaceholders) {
for (Placeholder placeholder : CONFIG_PLACEHOLDERS) {
content = placeholder.apply(content);
}
}
//create the config file
Files.write(configFile.toPath(), content.getBytes(StandardCharsets.UTF_8));
//load
configNode = getLoader(configFile).load();
} else {
//create empty config
ConfigurationLoader<? extends ConfigurationNode> loader = getLoader(configFile);
configNode = loader.createNode();
//save to create file
if (generateEmptyConfig) loader.save(configNode);
}
} else {
//load config
configNode = getLoader(configFile).load();
} }
//populate missing values with default values if (!Files.isReadable(path)) {
if (defaultValues != null) { throw new ConfigurationException(
ConfigurationNode defaultValuesNode = getLoader(defaultValues).load(); "BlueMap tried to read this file, but can not access it:\n" +
configNode.mergeFrom(defaultValuesNode); path + "\n" +
"Check if BlueMap has the permission to read this file.");
} }
return configNode; try {
return getLoader(path).load();
} catch (ConfigurateException ex) {
throw new ConfigurationException(
"BlueMap failed to parse this file:\n" +
path + "\n" +
"Check if the file is correctly formatted.\n" +
"(for example there might be a } or ] or , missing somewhere)",
ex);
}
} }
private ConfigurationLoader<? extends ConfigurationNode> getLoader(URL url){ public Path getConfigRoot() {
if (url.getFile().endsWith(".json")) return GsonConfigurationLoader.builder().url(url).build(); return configRoot;
else return HoconConfigurationLoader.builder().url(url).build();
} }
private ConfigurationLoader<? extends ConfigurationNode> getLoader(File file){ private Path findConfigPath(Path rawPath) {
if (file.getName().endsWith(".json")) return GsonConfigurationLoader.builder().file(file).build(); for (String fileEnding : CONFIG_FILE_ENDINGS) {
else return HoconConfigurationLoader.builder().file(file).build(); if (rawPath.getFileName().endsWith(fileEnding)) return rawPath;
}
for (String fileEnding : CONFIG_FILE_ENDINGS) {
Path path = rawPath.getParent().resolve(rawPath.getFileName() + fileEnding);
if (Files.exists(path)) return path;
}
return rawPath.getParent().resolve(rawPath.getFileName() + CONFIG_FILE_ENDINGS[0]);
} }
public static File toFolder(String pathString) throws IOException { private ConfigurationLoader<? extends ConfigurationNode> getLoader(Path path){
Objects.requireNonNull(pathString); if (path.getFileName().endsWith(".json")) return GsonConfigurationLoader.builder().path(path).build();
else return HoconConfigurationLoader.builder().path(path).build();
File file = new File(pathString);
if (file.exists() && !file.isDirectory()) throw new IOException("Invalid configuration: Path '" + file.getAbsolutePath() + "' is a file (should be a directory)");
return file;
} }
} }

View File

@ -0,0 +1,83 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.config;
public class ConfigurationException extends Exception {
private static final String FORMATTING_BAR = "################################";
private final String explanation;
public ConfigurationException(String explanation) {
super();
this.explanation = explanation;
}
public ConfigurationException(String message, String explanation) {
super(message);
this.explanation = explanation;
}
public ConfigurationException(String explanation, Throwable cause) {
super(cause);
this.explanation = explanation;
}
public ConfigurationException(String message, String explanation, Throwable cause) {
super(message, cause);
this.explanation = explanation;
}
public Throwable getRootCause() {
Throwable cause = getCause();
if (cause instanceof ConfigurationException) {
return ((ConfigurationException) cause).getRootCause();
} else {
return cause;
}
}
public String getExplanation() {
return explanation;
}
public String getFullExplanation() {
Throwable cause = getCause();
if (cause instanceof ConfigurationException) {
return getExplanation() + "\n\n" + ((ConfigurationException) cause).getFullExplanation();
} else {
return getExplanation();
}
}
public String getFormattedExplanation() {
String indentedExplanation = " " + getFullExplanation().replace("\n", "\n ");
return "\n" + FORMATTING_BAR +
"\n There is a problem with your BlueMap setup!\n" +
indentedExplanation +
"\n" + FORMATTING_BAR;
}
}

View File

@ -0,0 +1,162 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.config.old;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.config.ConfigurationException;
import de.bluecolored.bluemap.core.util.FileUtils;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import org.spongepowered.configurate.hocon.HoconConfigurationLoader;
import org.spongepowered.configurate.loader.ConfigurationLoader;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Deprecated
public class ConfigManager {
private static final Set<Placeholder> CONFIG_PLACEHOLDERS = new HashSet<>();
static {
CONFIG_PLACEHOLDERS.add(new Placeholder("version", BlueMap.VERSION));
CONFIG_PLACEHOLDERS.add(new Placeholder("datetime-iso", () -> LocalDateTime.now().withNano(0).toString()));
}
/**
* Loads or creates a config file for BlueMap.
*
* @param configFile The config file to load
* @param defaultConfig The default config that is used as a template if the config file does not exist (can be null)
* @param defaultValues The default values used if a key is not present in the config (can be null)
* @param usePlaceholders Whether to replace placeholders from the defaultConfig if it is newly generated
* @param generateEmptyConfig Whether to generate an empty config file if no default config is provided
* @return The loaded configuration node
* @throws ConfigurationException if an IOException occurs while loading
*/
public ConfigurationNode loadOrCreate(File configFile, URL defaultConfig, URL defaultValues, boolean usePlaceholders, boolean generateEmptyConfig) throws ConfigurationException {
ConfigurationNode configNode;
if (!configFile.exists()) {
try {
FileUtils.mkDirsParent(configFile);
if (defaultConfig != null) {
//load content of default config
String content;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(defaultConfig.openStream(), StandardCharsets.UTF_8))) {
content = reader.lines().collect(Collectors.joining("\n"));
}
//replace placeholders if enabled
if (usePlaceholders) {
for (Placeholder placeholder : CONFIG_PLACEHOLDERS) {
content = placeholder.apply(content);
}
}
//create the config file
Files.write(configFile.toPath(), content.getBytes(StandardCharsets.UTF_8));
//load
configNode = getLoader(configFile).load();
} else {
//create empty config
ConfigurationLoader<? extends ConfigurationNode> loader = getLoader(configFile);
configNode = loader.createNode();
//save to create file
if (generateEmptyConfig) loader.save(configNode);
}
} catch (IOException ex) {
throw new ConfigurationException(
"BlueMap tried to create this file:\n" +
configFile +
"but something went wrong!\n" +
"Does BlueMap has sufficient write permissions?",
ex);
}
} else {
try {
//load config
configNode = getLoader(configFile).load();
} catch (IOException ex) {
throw new ConfigurationException(
"BlueMap tried to load this file:\n" +
configFile +
"but something went wrong!\n" +
"Is the config-file formatted correctly?\n" +
"Maybe there is a } or ] or , missing?" +
"Does BlueMap has sufficient read permissions to this file?",
ex);
}
}
//populate missing values with default values
if (defaultValues != null) {
try {
ConfigurationNode defaultValuesNode = getLoader(defaultValues).load();
configNode.mergeFrom(defaultValuesNode);
} catch (IOException ex) {
throw new ConfigurationException(
"Something went wrong trying to load this config:\n" +
configFile,
ex);
}
}
return configNode;
}
private ConfigurationLoader<? extends ConfigurationNode> getLoader(URL url){
if (url.getFile().endsWith(".json")) return GsonConfigurationLoader.builder().url(url).build();
else return HoconConfigurationLoader.builder().url(url).build();
}
private ConfigurationLoader<? extends ConfigurationNode> getLoader(File file){
if (file.getName().endsWith(".json")) return GsonConfigurationLoader.builder().file(file).build();
else return HoconConfigurationLoader.builder().file(file).build();
}
public static File toFolder(String pathString) throws ConfigurationException {
Objects.requireNonNull(pathString);
File file = new File(pathString);
if (file.exists() && !file.isDirectory()) throw new ConfigurationException("Invalid configuration: Path '" + file.getAbsolutePath() + "' is a file (should be a directory)");
return file;
}
}

View File

@ -22,8 +22,9 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.bluecolored.bluemap.core.config; package de.bluecolored.bluemap.core.config.old;
import de.bluecolored.bluemap.core.config.ConfigurationException;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationNode;
@ -38,7 +39,7 @@ public class CoreConfig {
private boolean metricsEnabled = false; private boolean metricsEnabled = false;
private File dataFolder = new File("data"); private File dataFolder = new File("data");
public CoreConfig(ConfigurationNode node) throws IOException { public CoreConfig(ConfigurationNode node) throws ConfigurationException {
//accept-download //accept-download
downloadAccepted = node.node("accept-download").getBoolean(false); downloadAccepted = node.node("accept-download").getBoolean(false);

View File

@ -22,17 +22,16 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.bluecolored.bluemap.core.config; package de.bluecolored.bluemap.core.config.old;
import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i; import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.config.ConfigurationException;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.map.MapSettings; import de.bluecolored.bluemap.core.map.MapSettings;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.util.ConfigUtils; import de.bluecolored.bluemap.core.util.ConfigUtils;
import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationNode;
import java.io.IOException;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@DebugDump @DebugDump
@ -54,7 +53,7 @@ public class MapConfig implements MapSettings {
private Vector3i min, max; private Vector3i min, max;
private boolean renderEdges; private boolean renderEdges;
private Compression compression; private String storage;
private boolean ignoreMissingLightData; private boolean ignoreMissingLightData;
private int hiresTileSize; private int hiresTileSize;
@ -62,19 +61,19 @@ public class MapConfig implements MapSettings {
private int lowresPointsPerHiresTile; private int lowresPointsPerHiresTile;
private int lowresPointsPerLowresTile; private int lowresPointsPerLowresTile;
public MapConfig(ConfigurationNode node) throws IOException { public MapConfig(ConfigurationNode node) throws ConfigurationException {
//id //id
this.id = node.node("id").getString(""); this.id = node.node("id").getString("");
if (id.isEmpty()) throw new IOException("Invalid configuration: Node maps[?].id is not defined"); if (id.isEmpty()) throw new ConfigurationException("Invalid configuration: Node maps[?].id is not defined");
if (!VALID_ID_PATTERN.matcher(id).matches()) throw new IOException("Invalid configuration: Node maps[?].id '" + id + "' has invalid characters in it"); if (!VALID_ID_PATTERN.matcher(id).matches()) throw new ConfigurationException("Invalid configuration: Node maps[?].id '" + id + "' has invalid characters in it");
//name //name
this.name = node.node("name").getString(id); this.name = node.node("name").getString(id);
//world //world
this.world = node.node("world").getString(""); this.world = node.node("world").getString("");
if (world.isEmpty()) throw new IOException("Invalid configuration: Node maps[?].world is not defined"); if (world.isEmpty()) throw new ConfigurationException("Invalid configuration: Node maps[?].world is not defined");
//startPos //startPos
if (!node.node("startPos").virtual()) this.startPos = ConfigUtils.readVector2i(node.node("startPos")); if (!node.node("startPos").virtual()) this.startPos = ConfigUtils.readVector2i(node.node("startPos"));
@ -106,8 +105,8 @@ public MapConfig(ConfigurationNode node) throws IOException {
//renderEdges //renderEdges
this.renderEdges = node.node("renderEdges").getBoolean(true); this.renderEdges = node.node("renderEdges").getBoolean(true);
//useCompression //storage
this.compression = node.node("useCompression").getBoolean(true) ? Compression.GZIP : Compression.NONE; this.storage = node.node("storage").getString("file");
//ignoreMissingLightData //ignoreMissingLightData
this.ignoreMissingLightData = node.node("ignoreMissingLightData").getBoolean(false); this.ignoreMissingLightData = node.node("ignoreMissingLightData").getBoolean(false);
@ -119,7 +118,7 @@ public MapConfig(ConfigurationNode node) throws IOException {
//check valid tile configuration values //check valid tile configuration values
double blocksPerPoint = (double) this.hiresTileSize / (double) this.lowresPointsPerHiresTile; double blocksPerPoint = (double) this.hiresTileSize / (double) this.lowresPointsPerHiresTile;
if (blocksPerPoint != Math.floor(blocksPerPoint)) throw new IOException("Invalid configuration: Invalid map resolution settings of map " + id + ": hires.tileSize / lowres.pointsPerTile has to be an integer result"); if (blocksPerPoint != Math.floor(blocksPerPoint)) throw new ConfigurationException("Invalid configuration: Invalid map resolution settings of map " + id + ": hires.tileSize / lowres.pointsPerTile has to be an integer result");
} }
@ -143,8 +142,8 @@ public int getSkyColor() {
return skyColor; return skyColor;
} }
public Compression getCompression() { public String getStorage() {
return compression; return storage;
} }
@Override @Override

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.bluecolored.bluemap.core.config; package de.bluecolored.bluemap.core.config.old;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.regex.Pattern; import java.util.regex.Pattern;

View File

@ -22,8 +22,9 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.bluecolored.bluemap.core.config; package de.bluecolored.bluemap.core.config.old;
import de.bluecolored.bluemap.core.config.ConfigurationException;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationNode;
@ -40,11 +41,11 @@ public class RenderConfig {
private boolean enableFreeFlight; private boolean enableFreeFlight;
private List<MapConfig> mapConfigs; private List<MapConfig> mapConfigs;
public RenderConfig(ConfigurationNode node) throws IOException { public RenderConfig(ConfigurationNode node) throws ConfigurationException {
//webroot //webroot
String webRootString = node.node("webroot").getString(); String webRootString = node.node("webroot").getString();
if (webRootString == null) throw new IOException("Invalid configuration: Node webroot is not defined"); if (webRootString == null) throw new ConfigurationException("Invalid configuration: Node webroot is not defined");
webRoot = ConfigManager.toFolder(webRootString); webRoot = ConfigManager.toFolder(webRootString);
//cookies //cookies

View File

@ -22,8 +22,9 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.bluecolored.bluemap.core.config; package de.bluecolored.bluemap.core.config.old;
import de.bluecolored.bluemap.core.config.ConfigurationException;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationNode;
@ -42,7 +43,7 @@ public class WebServerConfig {
private int port = 8100; private int port = 8100;
private int maxConnections = 100; private int maxConnections = 100;
public WebServerConfig(ConfigurationNode node) throws IOException { public WebServerConfig(ConfigurationNode node) throws ConfigurationException {
//enabled //enabled
enabled = node.node("enabled").getBoolean(false); enabled = node.node("enabled").getBoolean(false);
@ -50,17 +51,22 @@ public WebServerConfig(ConfigurationNode node) throws IOException {
if (enabled) { if (enabled) {
//webroot //webroot
String webRootString = node.node("webroot").getString(); String webRootString = node.node("webroot").getString();
if (webRootString == null) throw new IOException("Invalid configuration: Node webroot is not defined"); if (webRootString == null) throw new ConfigurationException("Invalid configuration: Node webroot is not defined");
webRoot = ConfigManager.toFolder(webRootString); webRoot = ConfigManager.toFolder(webRootString);
//ip //ip
String bindAddressString = node.node("ip").getString(""); String bindAddressString = node.node("ip").getString("");
if (bindAddressString.isEmpty() || bindAddressString.equals("0.0.0.0") || bindAddressString.equals("::0")) { try {
bindAddress = new InetSocketAddress(0).getAddress(); // 0.0.0.0 if (bindAddressString.isEmpty() || bindAddressString.equals("0.0.0.0") ||
} else if (bindAddressString.equals("#getLocalHost")) { bindAddressString.equals("::0")) {
bindAddress = InetAddress.getLocalHost(); bindAddress = new InetSocketAddress(0).getAddress(); // 0.0.0.0
} else { } else if (bindAddressString.equals("#getLocalHost")) {
bindAddress = InetAddress.getByName(bindAddressString); bindAddress = InetAddress.getLocalHost();
} else {
bindAddress = InetAddress.getByName(bindAddressString);
}
} catch (IOException ex) {
throw new ConfigurationException("Failed to parse ip: '" + bindAddressString + "'", ex);
} }
//port //port

View File

@ -0,0 +1,51 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.config.storage;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.Compression;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.nio.file.Path;
import java.nio.file.Paths;
@SuppressWarnings("FieldMayBeFinal")
@DebugDump
@ConfigSerializable
public class FileConfig extends StorageConfig {
private Path root = Paths.get("bluemap", "web", "data");
private Compression compression = Compression.GZIP;
public Path getRoot() {
return root;
}
public Compression getCompression() {
return compression;
}
}

View File

@ -0,0 +1,81 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.config.storage;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.Compression;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Paths;
import java.util.Optional;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@DebugDump
@ConfigSerializable
public class SQLConfig extends StorageConfig {
private String driverJar = null;
private String driverClass = null;
private String dbUrl = "jdbc:mysql://localhost:3306/bluemap";
private String user = "root";
private String password = "";
private Compression compression = Compression.GZIP;
private transient URL driverJarURL = null;
public Optional<URL> getDriverJar() throws MalformedURLException {
if (driverJar == null) return Optional.empty();
if (driverJarURL == null) {
driverJarURL = Paths.get(driverJar).toUri().toURL();
}
return Optional.of(driverJarURL);
}
public Optional<String> getDriverClass() {
return Optional.ofNullable(driverClass);
}
public String getDbUrl() {
return dbUrl;
}
public String getUser() {
return user;
}
public String getPassword() {
return password;
}
public Compression getCompression() {
return compression;
}
}

View File

@ -22,11 +22,21 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.bluecolored.bluemap.core.util; package de.bluecolored.bluemap.core.config.storage;
@FunctionalInterface import de.bluecolored.bluemap.core.debug.DebugDump;
public interface ThrowingRunnable<E extends Throwable> { import de.bluecolored.bluemap.core.storage.StorageType;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
void run() throws E; @SuppressWarnings("FieldMayBeFinal")
@DebugDump
@ConfigSerializable
public class StorageConfig {
private StorageType storageType = StorageType.FILE;
public StorageType getStorageType() {
return storageType;
}
} }

View File

@ -31,14 +31,16 @@
import de.bluecolored.bluemap.core.map.hires.HiresTileMeta; import de.bluecolored.bluemap.core.map.hires.HiresTileMeta;
import de.bluecolored.bluemap.core.map.lowres.LowresModelManager; import de.bluecolored.bluemap.core.map.lowres.LowresModelManager;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.*; import de.bluecolored.bluemap.core.storage.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.MetaType;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.TileType;
import de.bluecolored.bluemap.core.world.Grid; import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -72,9 +74,9 @@ public BmMap(String id, String name, World world, Storage storage, ResourcePack
this.renderState = new MapRenderState(); this.renderState = new MapRenderState();
Optional<InputStream> rstateData = storage.readMeta(id, MetaType.RENDER_STATE); Optional<CompressedInputStream> rstateData = storage.readMeta(id, MetaType.RENDER_STATE);
if (rstateData.isPresent()) { if (rstateData.isPresent()) {
try (InputStream in = rstateData.get()){ try (InputStream in = rstateData.get().decompress()){
this.renderState.load(in); this.renderState.load(in);
} catch (IOException ex) { } catch (IOException ex) {
Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex); Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex);

View File

@ -28,6 +28,7 @@
import com.flowpowered.math.vector.Vector3f; import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.hires.HiresTileMeta; import de.bluecolored.bluemap.core.map.hires.HiresTileMeta;
import de.bluecolored.bluemap.core.storage.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.Storage; import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.threejs.BufferGeometry; import de.bluecolored.bluemap.core.threejs.BufferGeometry;
import de.bluecolored.bluemap.core.util.math.Color; import de.bluecolored.bluemap.core.util.math.Color;
@ -163,9 +164,9 @@ private LowresModel getModel(Vector2i tile) {
if (model == null){ if (model == null){
try { try {
Optional<InputStream> optIs = storage.read(tile); Optional<CompressedInputStream> optIs = storage.read(tile);
if (optIs.isPresent()){ if (optIs.isPresent()){
try (InputStream is = optIs.get()) { try (InputStream is = optIs.get().decompress()) {
String json = IOUtils.toString(is, StandardCharsets.UTF_8); String json = IOUtils.toString(is, StandardCharsets.UTF_8);
model = new CachedModel(BufferGeometry.fromJson(json)); model = new CachedModel(BufferGeometry.fromJson(json));

View File

@ -0,0 +1,73 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
import java.io.IOException;
import java.io.InputStream;
public class CompressedInputStream extends InputStream {
private final InputStream in;
private final Compression compression;
public CompressedInputStream(InputStream in, Compression compression) {
this.in = in;
this.compression = compression;
}
public InputStream decompress() throws IOException {
return compression.decompress(in);
}
public Compression getCompression() {
return compression;
}
@Override
public int read() throws IOException {
return in.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return in.read(b, off, len);
}
@Override
public void close() throws IOException {
in.close();
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void reset() throws IOException {
in.reset();
}
}

View File

@ -27,49 +27,56 @@
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.NoSuchElementException;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
public enum Compression { public enum Compression {
NONE("") { NONE("none", "", out -> out, in -> in),
@Override GZIP("gzip", ".gz", GZIPOutputStream::new, GZIPInputStream::new);
public OutputStream compress(OutputStream out) {
return out;
}
@Override
public InputStream decompress(InputStream in) {
return in;
}
},
GZIP(".gz"){
@Override
public OutputStream compress(OutputStream out) throws IOException {
return new GZIPOutputStream(out);
}
@Override
public InputStream decompress(InputStream in) throws IOException {
return new GZIPInputStream(in);
}
};
private final String typeId;
private final String fileSuffix; private final String fileSuffix;
private final StreamTransformer<OutputStream> compressor;
private final StreamTransformer<InputStream> decompressor;
Compression(String fileSuffix) { Compression(String typeId, String fileSuffix,
StreamTransformer<OutputStream> compressor,
StreamTransformer<InputStream> decompressor) {
this.fileSuffix = fileSuffix; this.fileSuffix = fileSuffix;
this.typeId = typeId;
this.compressor = compressor;
this.decompressor = decompressor;
}
public String getTypeId() {
return typeId;
} }
public String getFileSuffix() { public String getFileSuffix() {
return fileSuffix; return fileSuffix;
} }
public abstract OutputStream compress(OutputStream out) throws IOException; public OutputStream compress(OutputStream out) throws IOException {
return compressor.apply(out);
}
public abstract InputStream decompress(InputStream in) throws IOException; public InputStream decompress(InputStream in) throws IOException {
return decompressor.apply(in);
}
public static Compression forTypeId(String id) {
for (Compression compression : values()) {
if (compression.typeId.equals(id)) return compression;
}
throw new NoSuchElementException("There is no Compression with type-id: " + id);
}
@FunctionalInterface
private interface StreamTransformer<T> {
T apply(T original) throws IOException;
}
} }

View File

@ -26,9 +26,30 @@
public enum MetaType { public enum MetaType {
TEXTURES, //TEXTURES ("textures", "textures.json", "application/json"),
SETTINGS, //SETTINGS ("settings", "settings.json", "application/json"),
MARKERS, //MARKERS ("markers", "markers.json", "application/json"),
RENDER_STATE RENDER_STATE ("render_state", ".rstate", "application/octet-stream");
private final String typeId;
private final String filePath;
private final String contentType;
MetaType(String typeId, String filePath, String contentType) {
this.typeId = typeId;
this.filePath = filePath;
this.contentType = contentType;
}
public String getTypeId() {
return typeId;
}
public String getFilePath() {
return filePath;
}
public String getContentType() {
return contentType;
}
} }

View File

@ -26,22 +26,28 @@
import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector2i;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Optional; import java.util.Optional;
public abstract class Storage { public abstract class Storage implements Closeable {
public abstract void initialize() throws IOException;
public abstract OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException; public abstract OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract Optional<InputStream> readMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException; public abstract Optional<CompressedInputStream> readMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract Optional<TileData> readMapTileData(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract void deleteMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException; public abstract void deleteMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract OutputStream writeMeta(String mapId, MetaType metaType) throws IOException; public abstract OutputStream writeMeta(String mapId, MetaType metaType) throws IOException;
public abstract Optional<InputStream> readMeta(String mapId, MetaType metaType) throws IOException; public abstract Optional<CompressedInputStream> readMeta(String mapId, MetaType metaType) throws IOException;
public abstract void deleteMeta(String mapId, MetaType metaType) throws IOException;
public abstract void purgeMap(String mapId) throws IOException; public abstract void purgeMap(String mapId) throws IOException;
@ -60,11 +66,11 @@ private TileStorage(String mapId, TileType tileType) {
} }
public OutputStream write(Vector2i tile) throws IOException { public OutputStream write(Vector2i tile) throws IOException {
return Storage.this.writeMapTile(mapId, tileType, tile); return writeMapTile(mapId, tileType, tile);
} }
public Optional<InputStream> read(Vector2i tile) throws IOException { public Optional<CompressedInputStream> read(Vector2i tile) throws IOException {
return Storage.this.readMapTile(mapId, tileType, tile); return readMapTile(mapId, tileType, tile);
} }
public void delete(Vector2i tile) throws IOException { public void delete(Vector2i tile) throws IOException {

View File

@ -0,0 +1,55 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
import de.bluecolored.bluemap.core.config.storage.FileConfig;
import de.bluecolored.bluemap.core.config.storage.SQLConfig;
import de.bluecolored.bluemap.core.storage.file.FileStorage;
import de.bluecolored.bluemap.core.storage.sql.SQLStorage;
import org.spongepowered.configurate.ConfigurationNode;
import java.util.Objects;
public enum StorageType {
FILE (node -> new FileStorage(Objects.requireNonNull(node.get(FileConfig.class)))),
SQL (node -> new SQLStorage(Objects.requireNonNull(node.get(SQLConfig.class))));
private final StorageProvider storageProvider;
StorageType(StorageProvider storageProvider) {
this.storageProvider = storageProvider;
}
public Storage create(ConfigurationNode node) throws Exception {
return storageProvider.provide(node);
}
@FunctionalInterface
private interface StorageProvider {
Storage provide(ConfigurationNode node) throws Exception;
}
}

View File

@ -22,10 +22,18 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.bluecolored.bluemap.common; package de.bluecolored.bluemap.core.storage;
public interface ThrowingFunction<T, R, E extends Throwable> { import java.io.IOException;
R apply(T t) throws E; public interface TileData {
CompressedInputStream readMapTile() throws IOException;
Compression getCompression();
long getSize();
long getLastModified();
} }

View File

@ -24,6 +24,8 @@
*/ */
package de.bluecolored.bluemap.core.storage; package de.bluecolored.bluemap.core.storage;
import java.util.NoSuchElementException;
public enum TileType { public enum TileType {
HIRES ("hires"), HIRES ("hires"),
@ -38,4 +40,13 @@ public enum TileType {
public String getTypeId() { public String getTypeId() {
return typeId; return typeId;
} }
public static TileType forTypeId(String id) {
for (TileType type : values()) {
if (type.typeId.equals(id)) return type;
}
throw new NoSuchElementException("There is no TileType with id: " + id);
}
} }

View File

@ -22,10 +22,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.bluecolored.bluemap.core.storage; package de.bluecolored.bluemap.core.storage.file;
import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.config.storage.FileConfig;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.*;
import de.bluecolored.bluemap.core.util.AtomicFileHelper; import de.bluecolored.bluemap.core.util.AtomicFileHelper;
import de.bluecolored.bluemap.core.util.FileUtils; import de.bluecolored.bluemap.core.util.FileUtils;
@ -38,22 +40,25 @@
@DebugDump @DebugDump
public class FileStorage extends Storage { public class FileStorage extends Storage {
private static final EnumMap<MetaType, String> metaTypeFileNames = new EnumMap<>(MetaType.class);
static {
metaTypeFileNames.put(MetaType.TEXTURES, "../textures.json");
metaTypeFileNames.put(MetaType.SETTINGS, "../settings.json");
metaTypeFileNames.put(MetaType.MARKERS, "../markers.json");
metaTypeFileNames.put(MetaType.RENDER_STATE, ".rstate");
}
private final Path root; private final Path root;
private final Compression compression; private final Compression compression;
public FileStorage(FileConfig config) {
this.root = config.getRoot();
this.compression = config.getCompression();
}
public FileStorage(Path root, Compression compression) { public FileStorage(Path root, Compression compression) {
this.root = root; this.root = root;
this.compression = compression; this.compression = compression;
} }
@Override
public void initialize() {}
@Override
public void close() throws IOException {}
@Override @Override
public OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException { public OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, tileType, tile); Path file = getFilePath(mapId, tileType, tile);
@ -66,16 +71,48 @@ public OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile)
} }
@Override @Override
public Optional<InputStream> readMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException { public Optional<CompressedInputStream> readMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, tileType, tile); Path file = getFilePath(mapId, tileType, tile);
if (!Files.exists(file)) return Optional.empty(); if (!Files.exists(file)) return Optional.empty();
InputStream is = Files.newInputStream(file, StandardOpenOption.READ); InputStream is = Files.newInputStream(file, StandardOpenOption.READ);
is = new BufferedInputStream(is); is = new BufferedInputStream(is);
is = compression.decompress(is);
return Optional.of(is); return Optional.of(new CompressedInputStream(is, compression));
}
@Override
public Optional<TileData> readMapTileData(String mapId, TileType tileType, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, tileType, tile);
if (!Files.exists(file)) return Optional.empty();
final long size = Files.size(file);
final long lastModified = Files.getLastModifiedTime(file).toMillis();
return Optional.of(new TileData() {
@Override
public CompressedInputStream readMapTile() throws IOException {
return FileStorage.this.readMapTile(mapId, tileType, tile)
.orElseThrow(() -> new IOException("Tile no longer present!"));
}
@Override
public Compression getCompression() {
return compression;
}
@Override
public long getSize() {
return size;
}
@Override
public long getLastModified() {
return lastModified;
}
});
} }
@Override @Override
@ -86,7 +123,7 @@ public void deleteMapTile(String mapId, TileType tileType, Vector2i tile) throws
@Override @Override
public OutputStream writeMeta(String mapId, MetaType metaType) throws IOException { public OutputStream writeMeta(String mapId, MetaType metaType) throws IOException {
Path file = getFilePath(mapId).resolve(getFilename(metaType)); Path file = getFilePath(mapId).resolve(metaType.getFilePath());
OutputStream os = AtomicFileHelper.createFilepartOutputStream(file); OutputStream os = AtomicFileHelper.createFilepartOutputStream(file);
os = new BufferedOutputStream(os); os = new BufferedOutputStream(os);
@ -95,15 +132,21 @@ public OutputStream writeMeta(String mapId, MetaType metaType) throws IOExceptio
} }
@Override @Override
public Optional<InputStream> readMeta(String mapId, MetaType metaType) throws IOException { public Optional<CompressedInputStream> readMeta(String mapId, MetaType metaType) throws IOException {
Path file = getFilePath(mapId).resolve(getFilename(metaType)); Path file = getFilePath(mapId).resolve(metaType.getFilePath());
if (!Files.exists(file)) return Optional.empty(); if (!Files.exists(file)) return Optional.empty();
InputStream is = Files.newInputStream(file, StandardOpenOption.READ); InputStream is = Files.newInputStream(file, StandardOpenOption.READ);
is = new BufferedInputStream(is); is = new BufferedInputStream(is);
return Optional.of(is); return Optional.of(new CompressedInputStream(is, Compression.NONE));
}
@Override
public void deleteMeta(String mapId, MetaType metaType) throws IOException {
Path file = getFilePath(mapId).resolve(metaType.getFilePath());
FileUtils.delete(file.toFile());
} }
@Override @Override
@ -137,8 +180,4 @@ public Path getFilePath(String mapId) {
return root.resolve(mapId); return root.resolve(mapId);
} }
private static String getFilename(MetaType metaType) {
return metaTypeFileNames.getOrDefault(metaType, metaType.name().toLowerCase(Locale.ROOT));
}
} }

View File

@ -0,0 +1,718 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage.sql;
import com.flowpowered.math.vector.Vector2i;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.config.ConfigurationException;
import de.bluecolored.bluemap.core.config.storage.SQLConfig;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.storage.*;
import de.bluecolored.bluemap.core.util.WrappedOutputStream;
import org.apache.commons.dbcp2.*;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import javax.sql.DataSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.*;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.CompletionException;
public class SQLStorage extends Storage {
private final DataSource dataSource;
private final Compression compression;
private final LoadingCache<String, Integer> mapFKs = Caffeine.newBuilder()
.build(this::loadMapFK);
private final LoadingCache<TileType, Integer> mapTileTypeFKs = Caffeine.newBuilder()
.build(this::loadMapTileTypeFK);
private final LoadingCache<Compression, Integer> mapTileCompressionFKs = Caffeine.newBuilder()
.build(this::loadMapTileCompressionFK);
public SQLStorage(SQLConfig config) throws ConfigurationException {
try {
if (config.getDriverClass().isPresent()) {
if (config.getDriverJar().isPresent()) {
ClassLoader classLoader = new URLClassLoader(new URL[]{config.getDriverJar().get()});
Class<?> driverClass = Class.forName(config.getDriverClass().get(), true, classLoader);
Driver driver;
try {
driver = (Driver) driverClass.getDeclaredConstructor().newInstance();
} catch (Exception ex) {
throw new ConfigurationException(
"BlueMap is not able to create an instance of the configured Driver-Class.\n" +
"This means that BlueMap can not load this Driver at runtime.\n" +
"Instead you'll need to add your driver-jar to the classpath when starting your server," +
"e.g. using the '-classpath' command-line argument", ex);
}
this.dataSource = createDataSource(config.getDbUrl(), config.getUser(), config.getPassword(), driver);
} else {
Class.forName(config.getDriverClass().get());
this.dataSource = createDataSource(config.getDbUrl(), config.getUser(), config.getPassword());
}
} else {
this.dataSource = createDataSource(config.getDbUrl(), config.getUser(), config.getPassword());
}
} catch (MalformedURLException ex) {
throw new ConfigurationException(
"The path to your driver-jar is invalid. Check your sql-storage-config!", ex);
} catch (ClassNotFoundException ex) {
throw new ConfigurationException(
"The driver-class does not exist. Check your sql-storage-config!", ex);
}
this.compression = config.getCompression();
}
public SQLStorage(String dbUrl, String user, String password, Compression compression) {
this.dataSource = createDataSource(dbUrl, user, password);
this.compression = compression;
}
public SQLStorage(DataSource dataSource, Compression compression) {
this.dataSource = dataSource;
this.compression = compression;
}
@Override
public OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new WrappedOutputStream(compression.compress(byteOut), () -> {
int mapFK = getMapFK(mapId);
int tileTypeFK = getMapTileTypeFK(tileType);
int tileCompressionFK = getMapTileCompressionFK(compression);
recoveringConnection(connection -> {
Blob dataBlob = connection.createBlob();
try {
try (OutputStream blobOut = dataBlob.setBinaryStream(1)) {
byteOut.writeTo(blobOut);
}
executeUpdate(connection,
//language=SQL
"REPLACE INTO `bluemap_map_tile` (`map`, `type`, `x`, `z`, `compression`, `data`) " +
"VALUES (?, ?, ?, ?, ?, ?)",
mapFK,
tileTypeFK,
tile.getX(),
tile.getY(),
tileCompressionFK,
dataBlob
);
} finally {
dataBlob.free();
}
}, 2);
});
}
@Override
public Optional<CompressedInputStream> readMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
try {
byte[] data = recoveringConnection(connection -> {
ResultSet result = executeQuery(connection,
//language=SQL
"SELECT t.`data` " +
"FROM `bluemap_map_tile` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
" INNER JOIN `bluemap_map_tile_type` u " +
" ON t.`type` = u.`id` " +
" INNER JOIN `bluemap_map_tile_compression` c " +
" ON t.`compression` = c.`id` " +
"WHERE m.`map_id` = ? " +
"AND u.`type` = ? " +
"AND t.`x` = ? " +
"AND t.`z` = ? " +
"AND c.`compression` = ?",
mapId,
tileType.getTypeId(),
tile.getX(),
tile.getY(),
compression.getTypeId()
);
if (result.next()) {
Blob dataBlob = result.getBlob("data");
return dataBlob.getBytes(1, (int) dataBlob.length());
} else {
return null;
}
}, 2);
if (data == null) return Optional.empty();
return Optional.of(new CompressedInputStream(new ByteArrayInputStream(data), compression));
} catch (SQLException ex) {
throw new IOException(ex);
}
}
@Override
public Optional<TileData> readMapTileData(final String mapId, final TileType tileType, final Vector2i tile) throws IOException {
try {
TileData tileData = recoveringConnection(connection -> {
ResultSet result = executeQuery(connection,
//language=SQL
"SELECT c.`compression`, t.`changed`, LENGTH(t.`data`) as 'size' " +
"FROM `bluemap_map_tile` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
" INNER JOIN `bluemap_map_tile_type` u " +
" ON t.`type` = u.`id` " +
" INNER JOIN `bluemap_map_tile_compression` c " +
" ON t.`compression` = c.`id` " +
"WHERE m.`map_id` = ? " +
"AND u.`type` = ? " +
"AND t.`x` = ? " +
"AND t.`z` = ? " +
"AND c.`compression` = ?",
mapId,
tileType.getTypeId(),
tile.getX(),
tile.getY(),
compression.getTypeId()
);
if (result.next()) {
final Compression compression = Compression.forTypeId(result.getString("compression"));
final long lastModified = result.getTimestamp("changed").getTime();
final long size = result.getLong("size");
return new TileData() {
@Override
public CompressedInputStream readMapTile() throws IOException {
return SQLStorage.this.readMapTile(mapId, tileType, tile)
.orElseThrow(() -> new IOException("Tile no longer present!"));
}
@Override
public Compression getCompression() {
return compression;
}
@Override
public long getSize() {
return size;
}
@Override
public long getLastModified() {
return lastModified;
}
};
} else {
return null;
}
}, 2);
return Optional.ofNullable(tileData);
} catch (SQLException | NoSuchElementException ex) {
throw new IOException(ex);
}
}
@Override
public void deleteMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
try {
recoveringConnection(connection ->
executeUpdate(connection,
//language=SQL
"DELETE t " +
"FROM `bluemap_map_tile` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
" INNER JOIN `bluemap_map_tile_type` u " +
" ON t.`type` = u.`id` " +
"WHERE m.`map_id` = ? " +
"AND u.`type` = ? " +
"AND t.`x` = ? " +
"AND t.`z` = ?",
mapId,
tileType.getTypeId(),
tile.getX(),
tile.getY()
), 2);
} catch (SQLException ex) {
throw new IOException(ex);
}
}
@Override
public OutputStream writeMeta(String mapId, MetaType metaType) {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new WrappedOutputStream(byteOut, () -> {
int mapFK = getMapFK(mapId);
recoveringConnection(connection -> {
Blob dataBlob = connection.createBlob();
try {
try (OutputStream blobOut = dataBlob.setBinaryStream(1)) {
byteOut.writeTo(blobOut);
}
executeUpdate(connection,
//language=SQL
"REPLACE INTO `bluemap_map_meta` (`map`, `key`, `value`) " +
"VALUES (?, ?, ?)",
mapFK,
metaType.getTypeId(),
dataBlob
);
} finally {
dataBlob.free();
}
}, 2);
});
}
@Override
public Optional<CompressedInputStream> readMeta(String mapId, MetaType metaType) throws IOException {
try {
byte[] data = recoveringConnection(connection -> {
ResultSet result = executeQuery(connection,
//language=SQL
"SELECT t.`value` " +
"FROM `bluemap_map_meta` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`key` = ?",
mapId,
metaType.getTypeId()
);
if (result.next()) {
Blob dataBlob = result.getBlob("value");
return dataBlob.getBytes(1, (int) dataBlob.length());
} else {
return null;
}
}, 2);
if (data == null) return Optional.empty();
return Optional.of(new CompressedInputStream(new ByteArrayInputStream(data), Compression.NONE));
} catch (SQLException ex) {
throw new IOException(ex);
}
}
@Override
public void deleteMeta(String mapId, MetaType metaType) throws IOException {
try {
recoveringConnection(connection ->
executeUpdate(connection,
//language=SQL
"DELETE t " +
"FROM `bluemap_map_meta` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`key` = ?",
mapId,
metaType.getTypeId()
), 2);
} catch (SQLException ex) {
throw new IOException(ex);
}
}
@Override
public void purgeMap(String mapId) throws IOException {
try {
recoveringConnection(connection -> {
executeUpdate(connection,
//language=SQL
"DELETE t " +
"FROM `bluemap_map_tile` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ?",
mapId
);
executeUpdate(connection,
//language=SQL
"DELETE t " +
"FROM `bluemap_map_meta` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ?",
mapId
);
}, 2);
} catch (SQLException ex) {
throw new IOException(ex);
}
}
public void initialize() throws IOException {
try {
// initialize and get schema-version
String schemaVersionString = recoveringConnection(connection -> {
connection.createStatement().executeUpdate(
"CREATE TABLE IF NOT EXISTS `bluemap_storage_meta` (" +
"`key` varchar(255) NOT NULL, " +
"`value` varchar(255) DEFAULT NULL, " +
"PRIMARY KEY (`key`)" +
")");
ResultSet result = executeQuery(connection,
//language=SQL
"SELECT `value` FROM `bluemap_storage_meta` " +
"WHERE `key` = ?",
"schema_version"
);
if (result.next()) {
return result.getString("value");
} else {
executeUpdate(connection,
//language=SQL
"INSERT INTO `bluemap_storage_meta` (`key`, `value`) " +
"VALUES (?, ?)",
"schema_version", "0"
);
return "0";
}
}, 2);
int schemaVersion;
try {
schemaVersion = Integer.parseInt(schemaVersionString);
} catch (NumberFormatException ex) {
throw new IOException("Invalid schema-version number: " + schemaVersionString, ex);
}
// validate schema version
if (schemaVersion < 0 || schemaVersion > 1)
throw new IOException("Unknown schema-version: " + schemaVersion);
// update schema to current version
if (schemaVersion == 0) {
Logger.global.logInfo("Initializing database-schema...");
recoveringConnection(connection -> {
connection.createStatement().executeUpdate(
"CREATE TABLE `bluemap_map` (" +
"`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT," +
"`map_id` VARCHAR(255) NOT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE INDEX `map_id` (`map_id`)" +
");"
);
connection.createStatement().executeUpdate(
"CREATE TABLE `bluemap_map_tile_type` (" +
"`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT," +
"`type` VARCHAR(255) NOT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE INDEX `type` (`type`)" +
");"
);
connection.createStatement().executeUpdate(
"CREATE TABLE `bluemap_map_tile_compression` (" +
"`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT," +
"`compression` VARCHAR(255) NOT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE INDEX `compression` (`compression`)" +
");"
);
connection.createStatement().executeUpdate(
"CREATE TABLE `bluemap_map_meta` (" +
"`map` SMALLINT UNSIGNED NOT NULL," +
"`key` varchar(255) NOT NULL," +
"`value` LONGBLOB NOT NULL," +
"PRIMARY KEY (`map`, `key`)," +
"CONSTRAINT `fk_bluemap_map_meta_map` FOREIGN KEY (`map`) REFERENCES `bluemap_map` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT" +
")");
connection.createStatement().executeUpdate(
"CREATE TABLE `bluemap_map_tile` (" +
"`map` SMALLINT UNSIGNED NOT NULL," +
"`type` SMALLINT UNSIGNED NOT NULL," +
"`x` INT NOT NULL," +
"`z` INT NOT NULL," +
"`compression` SMALLINT UNSIGNED NOT NULL," +
"`changed` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
"`data` LONGBLOB NOT NULL," +
"PRIMARY KEY (`map`, `type`, `x`, `z`)," +
"CONSTRAINT `fk_bluemap_map_tile_map` FOREIGN KEY (`map`) REFERENCES `bluemap_map` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT," +
"CONSTRAINT `fk_bluemap_map_tile_type` FOREIGN KEY (`type`) REFERENCES `bluemap_map_tile_type` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT," +
"CONSTRAINT `fk_bluemap_map_tile_compression` FOREIGN KEY (`compression`) REFERENCES `bluemap_map_tile_compression` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT" +
");"
);
executeUpdate(connection,
//language=SQL
"UPDATE `bluemap_storage_meta` " +
"SET `value` = ? " +
"WHERE `key` = ?",
"1", "schema_version"
);
}, 2);
//schemaVersion = 1;
}
} catch (SQLException ex) {
throw new IOException(ex);
}
}
@Override
public void close() throws IOException {
if (dataSource instanceof AutoCloseable) {
try {
((AutoCloseable) dataSource).close();
} catch (Exception ex) {
throw new IOException("Failed to close datasource!", ex);
}
}
}
private ResultSet executeQuery(Connection connection, String sql, Object... parameters) throws SQLException {
// we only use this prepared statement once, but the DB-Driver caches those and reuses them
PreparedStatement statement = connection.prepareStatement(sql);
for (int i = 0; i < parameters.length; i++) {
statement.setObject(i+1, parameters[i]);
}
return statement.executeQuery();
}
@SuppressWarnings("UnusedReturnValue")
private int executeUpdate(Connection connection, String sql, Object... parameters) throws SQLException {
// we only use this prepared statement once, but the DB-Driver caches those and reuses them
PreparedStatement statement = connection.prepareStatement(sql);
for (int i = 0; i < parameters.length; i++) {
statement.setObject(i+1, parameters[i]);
}
return statement.executeUpdate();
}
@SuppressWarnings("SameParameterValue")
private void recoveringConnection(ConnectionConsumer action, int tries) throws SQLException, IOException {
recoveringConnection((ConnectionFunction<Void>) action, tries);
}
@SuppressWarnings("SameParameterValue")
private <R> R recoveringConnection(ConnectionFunction<R> action, int tries) throws SQLException, IOException {
SQLException sqlException = null;
for (int i = 0; i < tries; i++) {
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
R result;
try {
result = action.apply(connection);
connection.commit();
} catch (SQLRecoverableException ex) {
connection.rollback();
if (sqlException == null) {
sqlException = ex;
} else {
sqlException.addSuppressed(ex);
}
continue;
} catch (SQLException | RuntimeException ex) {
connection.rollback();
throw ex;
}
connection.setAutoCommit(true);
return result;
} catch (SQLException | IOException | RuntimeException ex) {
if (sqlException != null)
ex.addSuppressed(sqlException);
throw ex;
}
}
assert sqlException != null; // should never be null if we end up here
throw sqlException;
}
private int getMapFK(String mapId) throws SQLException {
try {
return Objects.requireNonNull(mapFKs.get(mapId));
} catch (CompletionException ex) {
Throwable cause = ex.getCause();
if (cause instanceof SQLException)
throw (SQLException) cause;
throw ex;
}
}
private int getMapTileTypeFK(TileType tileType) throws SQLException {
try {
return Objects.requireNonNull(mapTileTypeFKs.get(tileType));
} catch (CompletionException ex) {
Throwable cause = ex.getCause();
if (cause instanceof SQLException)
throw (SQLException) cause;
throw ex;
}
}
private int getMapTileCompressionFK(Compression compression) throws SQLException {
try {
return Objects.requireNonNull(mapTileCompressionFKs.get(compression));
} catch (CompletionException ex) {
Throwable cause = ex.getCause();
if (cause instanceof SQLException)
throw (SQLException) cause;
throw ex;
}
}
private int loadMapFK(String mapId) throws SQLException, IOException {
return lookupFK("bluemap_map", "id", "map_id", mapId);
}
private int loadMapTileTypeFK(TileType mapTileType) throws SQLException, IOException {
return lookupFK("bluemap_map_tile_type", "id", "type", mapTileType.getTypeId());
}
private int loadMapTileCompressionFK(Compression compression) throws SQLException, IOException {
return lookupFK("bluemap_map_tile_compression", "id", "compression", compression.getTypeId());
}
@SuppressWarnings("SameParameterValue")
private int lookupFK(String table, String idField, String valueField, String value) throws SQLException, IOException {
return recoveringConnection(connection -> {
int key;
ResultSet result = executeQuery(connection,
//language=SQL
"SELECT `" + idField + "` FROM `" + table + "` " +
"WHERE `" + valueField + "` = ?",
value
);
if (result.next()) {
key = result.getInt("id");
} else {
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO `" + table + "` (`" + valueField + "`) " +
"VALUES (?)",
Statement.RETURN_GENERATED_KEYS
);
statement.setString(1, value);
statement.executeUpdate();
ResultSet keys = statement.getGeneratedKeys();
if (!keys.next()) throw new IllegalStateException("No generated key returned!");
key = keys.getInt(1);
}
return key;
}, 2);
}
private DataSource createDataSource(String dbUrl, String user, String password) {
ConnectionFactory connectionFactory = new DriverManagerConnectionFactory(
dbUrl,
user,
password
);
return createDataSource(connectionFactory);
}
private DataSource createDataSource(String dbUrl, String user, String password, Driver driver) {
Properties properties = new Properties();
properties.put("user", user);
properties.put("password", password);
ConnectionFactory connectionFactory = new DriverConnectionFactory(
driver,
dbUrl,
properties
);
return createDataSource(connectionFactory);
}
private DataSource createDataSource(ConnectionFactory connectionFactory) {
PoolableConnectionFactory poolableConnectionFactory =
new PoolableConnectionFactory(connectionFactory, null);
poolableConnectionFactory.setPoolStatements(true);
poolableConnectionFactory.setMaxOpenPreparedStatements(20);
GenericObjectPoolConfig<PoolableConnection> objectPoolConfig = new GenericObjectPoolConfig<>();
objectPoolConfig.setMinIdle(1);
objectPoolConfig.setMaxIdle(Runtime.getRuntime().availableProcessors() * 2);
objectPoolConfig.setMaxTotal(-1);
ObjectPool<PoolableConnection> connectionPool =
new GenericObjectPool<>(poolableConnectionFactory, objectPoolConfig);
poolableConnectionFactory.setPool(connectionPool);
return new PoolingDataSource<>(connectionPool);
}
@FunctionalInterface
public interface ConnectionConsumer extends ConnectionFunction<Void> {
void accept(Connection connection) throws SQLException, IOException;
@Override
default Void apply(Connection connection) throws SQLException, IOException {
accept(connection);
return null;
}
}
@FunctionalInterface
public interface ConnectionFunction<R> {
R apply(Connection connection) throws SQLException, IOException;
}
}

View File

@ -62,42 +62,4 @@ private static Path getPartFile(Path file) {
return file.normalize().getParent().resolve(file.getFileName() + ".filepart"); return file.normalize().getParent().resolve(file.getFileName() + ".filepart");
} }
private static class WrappedOutputStream extends OutputStream {
private final OutputStream out;
private final ThrowingRunnable<IOException> onClose;
private WrappedOutputStream(OutputStream out, ThrowingRunnable<IOException> onClose) {
this.out = out;
this.onClose = onClose;
}
@Override
public void write(int b) throws IOException {
out.write(b);
}
@Override
public void write(byte[] b) throws IOException {
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
}
@Override
public void flush() throws IOException {
out.flush();
}
@Override
public void close() throws IOException {
out.close();
onClose.run();
}
}
} }

View File

@ -0,0 +1,48 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.util;
import java.util.function.Function;
@FunctionalInterface
public interface MappableFunction<T, R> extends Function<T, R> {
default <S> MappableFunction<T, S> map(Function<R, S> mapping) {
return t -> mapping.apply(this.apply(t));
}
default <S> MappableFunction<T, S> mapNullable(Function<R, S> mapping) {
return t -> {
R r = this.apply(t);
if (r == null) return null;
return mapping.apply(r);
};
}
static <T, R> MappableFunction<T, R> of(Function<T, R> function) {
return function::apply;
}
}

View File

@ -0,0 +1,82 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.util;
import java.io.IOException;
import java.io.InputStream;
public class WrappedInputStream extends InputStream {
private final InputStream in;
private final AutoCloseable onClose;
public WrappedInputStream(InputStream in, AutoCloseable onClose) {
this.in = in;
this.onClose = onClose;
}
@Override
public int read() throws IOException {
return in.read();
}
@Override
public int read(byte[] b) throws IOException {
return in.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return in.read(b, off, len);
}
@Override
public void close() throws IOException {
IOException ioExcetion = null;
try {
in.close();
} catch (IOException ex) {
ioExcetion = ex;
}
try {
onClose.close();
} catch (Exception ex) {
if (ioExcetion == null) {
if (ex instanceof IOException) {
ioExcetion = (IOException) ex;
} else {
ioExcetion = new IOException(ex);
}
} else {
ioExcetion.addSuppressed(ex);
}
}
if (ioExcetion != null) throw ioExcetion;
}
}

View File

@ -0,0 +1,87 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.util;
import java.io.IOException;
import java.io.OutputStream;
public class WrappedOutputStream extends OutputStream {
private final OutputStream out;
private final AutoCloseable onClose;
public WrappedOutputStream(OutputStream out, AutoCloseable onClose) {
this.out = out;
this.onClose = onClose;
}
@Override
public void write(int b) throws IOException {
out.write(b);
}
@Override
public void write(byte[] b) throws IOException {
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
}
@Override
public void flush() throws IOException {
out.flush();
}
@Override
public void close() throws IOException {
IOException ioExcetion = null;
try {
out.close();
} catch (IOException ex) {
ioExcetion = ex;
}
try {
onClose.close();
} catch (Exception ex) {
if (ioExcetion == null) {
if (ex instanceof IOException) {
ioExcetion = (IOException) ex;
} else {
ioExcetion = new IOException(ex);
}
} else {
ioExcetion.addSuppressed(ex);
}
}
if (ioExcetion != null) throw ioExcetion;
}
}

View File

@ -79,10 +79,11 @@ public void run() {
//just slow down processing if limit is reached //just slow down processing if limit is reached
hasPermit = processingSemaphore.tryAcquire(1, TimeUnit.SECONDS); hasPermit = processingSemaphore.tryAcquire(1, TimeUnit.SECONDS);
HttpResponse response = handler.handle(request); try (HttpResponse response = handler.handle(request)) {
sendResponse(response); sendResponse(response);
if (verbose) log(request, response); if (verbose) log(request, response);
}
} finally { } finally {
if (hasPermit) processingSemaphore.release(); if (hasPermit) processingSemaphore.release();
} }

View File

@ -91,13 +91,12 @@ public void write(OutputStream out) throws IOException {
if(data != null){ if(data != null){
chunkedPipe(data, out); chunkedPipe(data, out);
out.flush(); out.flush();
data.close();
} }
} }
@Override @Override
public void close() throws IOException { public void close() throws IOException {
data.close(); if (data != null) data.close();
} }
private void writeLine(OutputStreamWriter writer, String line) throws IOException { private void writeLine(OutputStreamWriter writer, String line) throws IOException {

View File

@ -31,13 +31,16 @@
import de.bluecolored.bluemap.common.rendermanager.RenderManager; import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.rendermanager.RenderTask; import de.bluecolored.bluemap.common.rendermanager.RenderTask;
import de.bluecolored.bluemap.common.web.FileRequestHandler; import de.bluecolored.bluemap.common.web.FileRequestHandler;
import de.bluecolored.bluemap.common.web.MapStorageRequestHandler;
import de.bluecolored.bluemap.core.BlueMap; import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.MinecraftVersion; import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.config.WebServerConfig; import de.bluecolored.bluemap.core.config.ConfigurationException;
import de.bluecolored.bluemap.core.config.old.WebServerConfig;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.logger.LoggerLogger; import de.bluecolored.bluemap.core.logger.LoggerLogger;
import de.bluecolored.bluemap.core.map.BmMap; import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.metrics.Metrics; import de.bluecolored.bluemap.core.metrics.Metrics;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileUtils; import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.webserver.HttpRequestHandler; import de.bluecolored.bluemap.core.webserver.HttpRequestHandler;
import de.bluecolored.bluemap.core.webserver.WebServer; import de.bluecolored.bluemap.core.webserver.WebServer;
@ -51,7 +54,7 @@
public class BlueMapCLI { public class BlueMapCLI {
public void renderMaps(BlueMapService blueMap, boolean watch, boolean forceRender, boolean forceGenerateWebapp) throws IOException, InterruptedException { public void renderMaps(BlueMapService blueMap, boolean watch, boolean forceRender, boolean forceGenerateWebapp) throws ConfigurationException, IOException, InterruptedException {
//metrics report //metrics report
if (blueMap.getCoreConfig().isMetricsEnabled()) Metrics.sendReportAsync("cli"); if (blueMap.getCoreConfig().isMetricsEnabled()) Metrics.sendReportAsync("cli");
@ -165,13 +168,24 @@ public void run() {
} }
} }
public void startWebserver(BlueMapService blueMap, boolean verbose) throws IOException { public void startWebserver(BlueMapService blueMap, boolean verbose) throws IOException, ConfigurationException, InterruptedException {
Logger.global.logInfo("Starting webserver ..."); Logger.global.logInfo("Starting webserver ...");
WebServerConfig config = blueMap.getWebServerConfig(); WebServerConfig config = blueMap.getWebServerConfig();
FileUtils.mkDirs(config.getWebRoot()); FileUtils.mkDirs(config.getWebRoot());
HttpRequestHandler requestHandler = new FileRequestHandler(config.getWebRoot().toPath(), "BlueMap v" + BlueMap.VERSION); HttpRequestHandler requestHandler = new FileRequestHandler(config.getWebRoot().toPath(), "BlueMap v" + BlueMap.VERSION);
try {
//use map-storage to provide map-tiles
Map<String, Storage> mapStorages = blueMap.getMapStorages();
requestHandler = new MapStorageRequestHandler(mapStorages::get, requestHandler);
} catch (ConfigurationException ex) {
Logger.global.logWarning(ex.getFormattedExplanation());
Logger.global.logError("Here is the full error:", ex);
Logger.global.logWarning("The webserver will still be started, but it will not be able to serve the map-tiles correctly!");
}
WebServer webServer = new WebServer( WebServer webServer = new WebServer(
config.getWebserverBindAddress(), config.getWebserverBindAddress(),
config.getWebserverPort(), config.getWebserverPort(),
@ -265,6 +279,7 @@ public static void main(String[] args) {
blueMap.getCoreConfig(); blueMap.getCoreConfig();
blueMap.getRenderConfig(); blueMap.getRenderConfig();
blueMap.getWebServerConfig(); blueMap.getWebServerConfig();
blueMap.getMapStorages();
//create resourcepacks folder //create resourcepacks folder
FileUtils.mkDirs(new File(configFolder, "resourcepacks")); FileUtils.mkDirs(new File(configFolder, "resourcepacks"));
@ -283,6 +298,12 @@ public static void main(String[] args) {
Logger.global.logError("Failed to parse provided arguments!", e); Logger.global.logError("Failed to parse provided arguments!", e);
BlueMapCLI.printHelp(); BlueMapCLI.printHelp();
System.exit(1); System.exit(1);
} catch (ConfigurationException e) {
Logger.global.logWarning(e.getFormattedExplanation());
Throwable cause = e.getRootCause();
if (cause != null) {
Logger.global.logError("Detailed error:", e);
}
} catch (IOException e) { } catch (IOException e) {
Logger.global.logError("An IO-error occurred!", e); Logger.global.logError("An IO-error occurred!", e);
System.exit(1); System.exit(1);

View File

@ -89,13 +89,10 @@ maps: [
# Default is true # Default is true
renderEdges: true renderEdges: true
# With this set to true, the generated files for this world are compressed using gzip to save A LOT of space. # This defines the storage-config that will be used to save this map.
# Files will be only 5% as big with compression! # You can find your storage configs next to this config file in the 'storages'-folder.
# Note: If you are using NGINX or Apache to host your map, you can configure them to serve the compressed files directly. # Default is "file"
# This is much better than disabling the compression. storage: "file"
# Changing this value requires a re-render of the map.
# Default is true
useCompression: true
# Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks. # Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks.
# If this is set to true BlueMap will render Chunks even if there is no light-data! # If this is set to true BlueMap will render Chunks even if there is no light-data!

View File

@ -89,13 +89,10 @@ maps: [
# Default is true # Default is true
renderEdges: true renderEdges: true
# With this set to true, the generated files for this world are compressed using gzip to save A LOT of space. # This defines the storage-config that will be used to save this map.
# Files will be only 5% as big with compression! # You can find your storage configs next to this config file in the 'storages'-folder.
# Note: If you are using NGINX or Apache to host your map, you can configure them to serve the compressed files directly. # Default is "file"
# This is much better than disabling the compression. storage: "file"
# Changing this value requires a re-render of the map.
# Default is true
useCompression: true
# Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks. # Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks.
# If this is set to true BlueMap will render Chunks even if there is no light-data! # If this is set to true BlueMap will render Chunks even if there is no light-data!

View File

@ -89,13 +89,10 @@ maps: [
# Default is true # Default is true
renderEdges: true renderEdges: true
# With this set to true, the generated files for this world are compressed using gzip to save A LOT of space. # This defines the storage-config that will be used to save this map.
# Files will be only 5% as big with compression! # You can find your storage configs next to this config file in the 'storages'-folder.
# Note: If you are using NGINX or Apache to host your map, you can configure them to serve the compressed files directly. # Default is "file"
# This is much better than disabling the compression. storage: "file"
# Changing this value requires a re-render of the map.
# Default is true
useCompression: true
# Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks. # Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks.
# If this is set to true BlueMap will render Chunks even if there is no light-data! # If this is set to true BlueMap will render Chunks even if there is no light-data!

View File

@ -89,13 +89,10 @@ maps: [
# Default is true # Default is true
renderEdges: true renderEdges: true
# With this set to true, the generated files for this world are compressed using gzip to save A LOT of space. # This defines the storage-config that will be used to save this map.
# Files will be only 5% as big with compression! # You can find your storage configs next to this config file in the 'storages'-folder.
# Note: If you are using NGINX or Apache to host your map, you can configure them to serve the compressed files directly. # Default is "file"
# This is much better than disabling the compression. storage: "file"
# Changing this value requires a re-render of the map.
# Default is true
useCompression: true
# Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks. # Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks.
# If this is set to true BlueMap will render Chunks even if there is no light-data! # If this is set to true BlueMap will render Chunks even if there is no light-data!

View File

@ -89,13 +89,10 @@ maps: [
# Default is true # Default is true
renderEdges: true renderEdges: true
# With this set to true, the generated files for this world are compressed using gzip to save A LOT of space. # This defines the storage-config that will be used to save this map.
# Files will be only 5% as big with compression! # You can find your storage configs next to this config file in the 'storages'-folder.
# Note: If you are using NGINX or Apache to host your map, you can configure them to serve the compressed files directly. # Default is "file"
# This is much better than disabling the compression. storage: "file"
# Changing this value requires a re-render of the map.
# Default is true
useCompression: true
# Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks. # Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks.
# If this is set to true BlueMap will render Chunks even if there is no light-data! # If this is set to true BlueMap will render Chunks even if there is no light-data!

View File

@ -89,13 +89,10 @@ maps: [
# Default is true # Default is true
renderEdges: true renderEdges: true
# With this set to true, the generated files for this world are compressed using gzip to save A LOT of space. # This defines the storage-config that will be used to save this map.
# Files will be only 5% as big with compression! # You can find your storage configs next to this config file in the 'storages'-folder.
# Note: If you are using NGINX or Apache to host your map, you can configure them to serve the compressed files directly. # Default is "file"
# This is much better than disabling the compression. storage: "file"
# Changing this value requires a re-render of the map.
# Default is true
useCompression: true
# Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks. # Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks.
# If this is set to true BlueMap will render Chunks even if there is no light-data! # If this is set to true BlueMap will render Chunks even if there is no light-data!

View File

@ -89,13 +89,10 @@ maps: [
# Default is true # Default is true
renderEdges: true renderEdges: true
# With this set to true, the generated files for this world are compressed using gzip to save A LOT of space. # This defines the storage-config that will be used to save this map.
# Files will be only 5% as big with compression! # You can find your storage configs next to this config file in the 'storages'-folder.
# Note: If you are using NGINX or Apache to host your map, you can configure them to serve the compressed files directly. # Default is "file"
# This is much better than disabling the compression. storage: "file"
# Changing this value requires a re-render of the map.
# Default is true
useCompression: true
# Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks. # Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks.
# If this is set to true BlueMap will render Chunks even if there is no light-data! # If this is set to true BlueMap will render Chunks even if there is no light-data!

View File

@ -89,13 +89,10 @@ maps: [
# Default is true # Default is true
renderEdges: true renderEdges: true
# With this set to true, the generated files for this world are compressed using gzip to save A LOT of space. # This defines the storage-config that will be used to save this map.
# Files will be only 5% as big with compression! # You can find your storage configs next to this config file in the 'storages'-folder.
# Note: If you are using NGINX or Apache to host your map, you can configure them to serve the compressed files directly. # Default is "file"
# This is much better than disabling the compression. storage: "file"
# Changing this value requires a re-render of the map.
# Default is true
useCompression: true
# Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks. # Normally BlueMap detects if a chunk has not yet generated it's light-data and omits rendering those chunks.
# If this is set to true BlueMap will render Chunks even if there is no light-data! # If this is set to true BlueMap will render Chunks even if there is no light-data!