Rework storages to make them extensible with addons

This commit is contained in:
Lukas Rieger (Blue) 2024-04-06 01:26:16 +02:00
parent 7e7b1e4f53
commit fdf242acdf
No known key found for this signature in database
GPG Key ID: AA33883B1BBA03E6
73 changed files with 3179 additions and 3309 deletions

View File

@ -244,7 +244,7 @@ private synchronized void loadMap(String id, MapConfig mapConfig) throws Configu
id,
name,
world,
storage,
storage.map(id),
getOrLoadResourcePack(),
mapConfig
);

View File

@ -25,7 +25,8 @@
package de.bluecolored.bluemap.common.api;
import de.bluecolored.bluemap.api.AssetStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -34,39 +35,39 @@
public class AssetStorageImpl implements AssetStorage {
private static final String ASSET_PATH = "assets/";
private final Storage storage;
private final MapStorage storage;
private final String mapId;
public AssetStorageImpl(Storage storage, String mapId) {
public AssetStorageImpl(MapStorage storage, String mapId) {
this.storage = storage;
this.mapId = mapId;
}
@Override
public OutputStream writeAsset(String name) throws IOException {
return storage.writeMeta(mapId, ASSET_PATH + name);
return storage.asset(name).write();
}
@Override
public Optional<InputStream> readAsset(String name) throws IOException {
return storage.readMeta(mapId, ASSET_PATH + name);
CompressedInputStream in = storage.asset(name).read();
if (in == null) return Optional.empty();
return Optional.of(in.decompress());
}
@Override
public boolean assetExists(String name) throws IOException {
return storage.readMetaInfo(mapId, ASSET_PATH + name).isPresent();
return storage.asset(name).exists();
}
@Override
public String getAssetUrl(String name) {
return "maps/" + mapId + "/" + Storage.escapeMetaName(ASSET_PATH + name);
return "maps/" + mapId + "/assets/" + MapStorage.escapeAssetName(name);
}
@Override
public void deleteAsset(String name) throws IOException {
storage.deleteMeta(mapId, ASSET_PATH + name);
storage.asset(name).delete();
}
}

View File

@ -81,6 +81,7 @@ public void registerStyle(String url) {
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public String createImage(BufferedImage image, String path) throws IOException {
path = path.replaceAll("[^a-zA-Z0-9_.\\-/]", "_");
@ -102,6 +103,7 @@ public String createImage(BufferedImage image, String path) throws IOException {
@Override
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public Map<String, String> availableImages() throws IOException {
Path webRoot = getWebRoot().toAbsolutePath();
String separator = webRoot.getFileSystem().getSeparator();

View File

@ -269,18 +269,11 @@ private Map<String, MapConfig> loadMapConfigs(Collection<ServerWorld> autoConfig
String name = worldFolder.getFileName() + " (" + dimensionName + ")";
if (i > 1) name = name + " (" + i + ")";
ConfigTemplate template;
switch (world.getDimension().getFormatted()) {
case "minecraft:the_nether":
template = createNetherMapTemplate(name, worldFolder, dimension, i - 1);
break;
case "minecraft:the_end":
template = createEndMapTemplate(name, worldFolder, dimension, i - 1);
break;
default:
template = createOverworldMapTemplate(name, worldFolder, dimension, i - 1);
break;
}
ConfigTemplate template = switch (world.getDimension().getFormatted()) {
case "minecraft:the_nether" -> createNetherMapTemplate(name, worldFolder, dimension, i - 1);
case "minecraft:the_end" -> createEndMapTemplate(name, worldFolder, dimension, i - 1);
default -> createOverworldMapTemplate(name, worldFolder, dimension, i - 1);
};
Files.writeString(
configFile,
@ -358,7 +351,7 @@ private Map<String, StorageConfig> loadStorageConfigs(Path defaultWebroot) throw
Path rawConfig = configManager.getRaw(configFile);
String id = rawConfig.getFileName().toString();
StorageConfig storageConfig = configManager.loadConfig(rawConfig, StorageConfig.class); // load superclass
StorageConfig storageConfig = configManager.loadConfig(rawConfig, StorageConfig.Base.class); // load superclass
storageConfig = configManager.loadConfig(rawConfig, storageConfig.getStorageType().getConfigType()); // load actual config type
storageConfigs.put(id, storageConfig);

View File

@ -0,0 +1,47 @@
package de.bluecolored.bluemap.common.config.storage;
import de.bluecolored.bluemap.core.storage.sql.Database;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import de.bluecolored.bluemap.core.storage.sql.commandset.MySQLCommandSet;
import de.bluecolored.bluemap.core.storage.sql.commandset.PostgreSQLCommandSet;
import de.bluecolored.bluemap.core.storage.sql.commandset.SqliteCommandSet;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.function.Function;
public interface Dialect extends Keyed {
Dialect MYSQL = new Impl(Key.bluemap("mysql"), MySQLCommandSet::new);
Dialect MARIADB = new Impl(Key.bluemap("mariadb"), MySQLCommandSet::new);
Dialect POSTGRESQL = new Impl(Key.bluemap("postgresql"), PostgreSQLCommandSet::new);
Dialect SQLITE = new Impl(Key.bluemap("sqlite"), SqliteCommandSet::new);
Registry<Dialect> REGISTRY = new Registry<>(
MYSQL,
MARIADB,
POSTGRESQL,
SQLITE
);
CommandSet createCommandSet(Database database);
@RequiredArgsConstructor
class Impl implements Dialect {
@Getter
private final Key key;
private final Function<Database, CommandSet> commandSetProvider;
@Override
public CommandSet createCommandSet(Database database) {
return commandSetProvider.apply(database);
}
}
}

View File

@ -25,8 +25,10 @@
package de.bluecolored.bluemap.common.config.storage;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.file.FileStorageSettings;
import de.bluecolored.bluemap.common.config.ConfigurationException;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.file.FileStorage;
import lombok.Getter;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.nio.file.Path;
@ -34,20 +36,19 @@
@SuppressWarnings("FieldMayBeFinal")
@DebugDump
@ConfigSerializable
public class FileConfig extends StorageConfig implements FileStorageSettings {
@Getter
public class FileConfig extends StorageConfig {
private Path root = Path.of("bluemap", "web", "maps");
private String compression = Compression.GZIP.getKey().getFormatted();
private Compression compression = Compression.GZIP;
@Override
public Path getRoot() {
return root;
public Compression getCompression() throws ConfigurationException {
return parseKey(Compression.REGISTRY, compression, "compression");
}
@Override
public Compression getCompression() {
return compression;
public FileStorage createStorage() throws ConfigurationException {
return new FileStorage(root, getCompression());
}
}

View File

@ -25,67 +25,146 @@
package de.bluecolored.bluemap.common.config.storage;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.sql.SQLStorageSettings;
import de.bluecolored.bluemap.common.config.ConfigurationException;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.sql.Database;
import de.bluecolored.bluemap.core.storage.sql.SQLStorage;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import de.bluecolored.bluemap.core.util.Key;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Driver;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@ConfigSerializable
public class SQLConfig extends StorageConfig implements SQLStorageSettings {
@Getter
public class SQLConfig extends StorageConfig {
private static final Pattern URL_DIALECT_PATTERN = Pattern.compile("jdbc:([^:]*)://.*");
private String connectionUrl = "jdbc:mysql://localhost/bluemap?permitMysqlScheme";
private Map<String, String> connectionProperties = new HashMap<>();
@DebugDump private String dialect = null;
@DebugDump private String driverJar = null;
@DebugDump private String driverClass = null;
private String connectionUrl = "jdbc:mysql://localhost/bluemap?permitMysqlScheme";
private Map<String, String> connectionProperties = new HashMap<>();
@DebugDump private Compression compression = Compression.GZIP;
@DebugDump private transient URL driverJarURL = null;
@DebugDump private int maxConnections = -1;
@Override
public Optional<URL> getDriverJar() throws MalformedURLException {
if (driverJar == null) return Optional.empty();
@DebugDump private String compression = Compression.GZIP.getKey().getFormatted();
if (driverJarURL == null) {
driverJarURL = Paths.get(driverJar).toUri().toURL();
@DebugDump
@Getter(AccessLevel.NONE)
private transient URL driverJarURL = null;
public Optional<URL> getDriverJar() throws ConfigurationException {
try {
if (driverJar == null) return Optional.empty();
if (driverJarURL == null) {
driverJarURL = Paths.get(driverJar).toUri().toURL();
}
return Optional.of(driverJarURL);
} catch (MalformedURLException ex) {
throw new ConfigurationException("""
The configured driver-jar path is not formatted correctly!
Please check your 'driver-jar' setting in your configuration and make sure you have the correct path configured.
""".strip(), ex);
}
return Optional.of(driverJarURL);
}
@Override
@SuppressWarnings("unused")
public Optional<String> getDriverClass() {
return Optional.ofNullable(driverClass);
}
@Override
public String getConnectionUrl() {
return connectionUrl;
public Compression getCompression() throws ConfigurationException {
return parseKey(Compression.REGISTRY, compression, "compression");
}
public Dialect getDialect() throws ConfigurationException {
String key = dialect;
// default from connection-url
if (key == null) {
Matcher matcher = URL_DIALECT_PATTERN.matcher(connectionUrl);
if (!matcher.find()) return Dialect.MYSQL;
key = Key.bluemap(matcher.group(1)).getFormatted();
}
return parseKey(Dialect.REGISTRY, key, "dialect");
}
@Override
public Map<String, String> getConnectionProperties() {
return connectionProperties;
public SQLStorage createStorage() throws ConfigurationException {
Driver driver = createDriver();
Database database;
if (driver != null) {
database = new Database(getConnectionUrl(), getConnectionProperties(), getMaxConnections(), driver);
} else {
database = new Database(getConnectionUrl(), getConnectionProperties(), getMaxConnections());
}
CommandSet commandSet = getDialect().createCommandSet(database);
return new SQLStorage(commandSet, getCompression());
}
@Override
public int getMaxConnections() {
return maxConnections;
}
private @Nullable Driver createDriver() throws ConfigurationException {
if (driverClass == null) return null;
@Override
public Compression getCompression() {
return compression;
try {
// load driver class
Class<?> driverClazz;
URL driverJarUrl = getDriverJar().orElse(null);
if (driverJarUrl != null) {
// sanity-check if file exists
if (!Files.exists(Path.of(driverJarUrl.toURI()))) {
throw new ConfigurationException("""
The configured driver-jar was not found!
Please check your 'driver-jar' setting in your configuration and make sure you have the correct path configured.
""".strip());
}
ClassLoader classLoader = new URLClassLoader(new URL[]{driverJarUrl});
driverClazz = Class.forName(driverClass, true, classLoader);
} else {
driverClazz = Class.forName(driverClass);
}
// create driver
return (Driver) driverClazz.getDeclaredConstructor().newInstance();
} catch (ClassCastException ex) {
throw new ConfigurationException("""
The configured driver-class was found but is not of the correct class-type!
Please check your 'driver-class' setting in your configuration and make sure you have the correct class configured.
""".strip(), ex);
} catch (ClassNotFoundException ex) {
throw new ConfigurationException("""
The configured driver-class was not found!
Please check your 'driver-class' setting in your configuration and make sure you have the correct class configured.
""".strip(), ex);
} catch (ConfigurationException ex) {
throw ex;
} catch (Exception ex) {
throw new ConfigurationException("""
BlueMap failed to load the configured SQL-Driver!
Please check your 'driver-jar' and 'driver-class' settings in your configuration.
""".strip(), ex);
}
}
}

View File

@ -28,6 +28,8 @@
import de.bluecolored.bluemap.common.config.ConfigurationException;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.util.Locale;
@ -35,34 +37,39 @@
@SuppressWarnings("FieldMayBeFinal")
@DebugDump
@ConfigSerializable
public class StorageConfig {
public abstract class StorageConfig {
private Key storageType = StorageType.FILE.getKey();
public Key getStorageTypeKey() {
return storageType;
}
private String storageType = StorageType.FILE.getKey().getFormatted();
public StorageType getStorageType() throws ConfigurationException {
StorageType type = StorageType.REGISTRY.get(storageType);
return parseKey(StorageType.REGISTRY, storageType, "storage-type");
}
public abstract Storage createStorage() throws ConfigurationException;
static <T extends Keyed> T parseKey(Registry<T> registry, String key, String typeName) throws ConfigurationException {
T type = registry.get(Key.parse(key, Key.BLUEMAP_NAMESPACE));
if (type == null) {
// try legacy config format
Key legacyFormatKey = Key.bluemap(storageType.getValue().toLowerCase(Locale.ROOT));
type = StorageType.REGISTRY.get(legacyFormatKey);
Key legacyFormatKey = Key.bluemap(key.toLowerCase(Locale.ROOT));
type = registry.get(legacyFormatKey);
}
if (type == null)
throw new ConfigurationException("No storage-type found for key: " + storageType + "!");
throw new ConfigurationException("No " + typeName + " found for key: " + key + "!");
return type;
}
public Storage createStorage() throws Exception {
if (this.getClass().equals(StorageConfig.class))
throw new UnsupportedOperationException("Can not create a Storage from the StorageConfig superclass.");
@ConfigSerializable
public static class Base extends StorageConfig {
@Override
public Storage createStorage() {
throw new UnsupportedOperationException();
}
return getStorageType().getStorageFactory(this.getClass()).provide(this);
}
}

View File

@ -24,56 +24,31 @@
*/
package de.bluecolored.bluemap.common.config.storage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.file.FileStorage;
import de.bluecolored.bluemap.core.storage.sql.SQLStorage;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
public class StorageType implements Keyed {
public interface StorageType extends Keyed {
public static final StorageType FILE = new StorageType( Key.bluemap("file"), FileConfig.class, FileStorage::new );
public static final StorageType SQL = new StorageType( Key.bluemap("sql"), SQLConfig.class, SQLStorage::create );
StorageType FILE = new Impl(Key.bluemap("file"), FileConfig.class);
StorageType SQL = new Impl(Key.bluemap("sql"), SQLConfig.class);
public static final Registry<StorageType> REGISTRY = new Registry<>(
Registry<StorageType> REGISTRY = new Registry<>(
FILE,
SQL
);
private final Key key;
private final Class<? extends StorageConfig> configType;
private final StorageFactory<? extends StorageConfig> storageFactory;
Class<? extends StorageConfig> getConfigType();
public <C extends StorageConfig> StorageType(Key key, Class<C> configType, StorageFactory<C> storageFactory) {
this.key = key;
this.configType = configType;
this.storageFactory = storageFactory;
}
@RequiredArgsConstructor
@Getter
class Impl implements StorageType {
@Override
public Key getKey() {
return key;
}
private final Key key;
private final Class<? extends StorageConfig> configType;
public Class<? extends StorageConfig> getConfigType() {
return configType;
}
@SuppressWarnings("unchecked")
public <C extends StorageConfig> StorageFactory<C> getStorageFactory(Class<C> configType) {
if (!configType.isAssignableFrom(this.configType)) throw new ClassCastException(this.configType + " can not be cast to " + configType);
return (StorageFactory<C>) storageFactory;
}
@FunctionalInterface
public interface StorageFactory<C extends StorageConfig> {
Storage provideRaw(C config) throws Exception;
@SuppressWarnings("unchecked")
default Storage provide(StorageConfig config) throws Exception {
return provideRaw((C) config);
}
}
}

View File

@ -35,8 +35,8 @@
import de.bluecolored.bluemap.common.plugin.skins.PlayerSkinUpdater;
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.serverinterface.ServerEventListener;
import de.bluecolored.bluemap.common.serverinterface.Server;
import de.bluecolored.bluemap.common.serverinterface.ServerEventListener;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.common.web.*;
import de.bluecolored.bluemap.common.web.http.HttpServer;
@ -201,7 +201,7 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti
mapRequestHandler = new MapRequestHandler(map, serverInterface, pluginConfig, Predicate.not(pluginState::isPlayerHidden));
} else {
Storage storage = blueMap.getOrLoadStorage(mapConfig.getStorage());
mapRequestHandler = new MapRequestHandler(id, storage);
mapRequestHandler = new MapRequestHandler(storage.map(id));
}
routingRequestHandler.register(
@ -240,9 +240,11 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti
throw new ConfigurationException("BlueMap failed to bind to the configured address.\n" +
"This usually happens when the configured port (" + webserverConfig.getPort() + ") is already in use by some other program.", ex);
} catch (IOException ex) {
throw new ConfigurationException("BlueMap failed to initialize the webserver.\n" +
"Check your webserver-config if everything is configured correctly.\n" +
"(Make sure you DON'T use the same port for bluemap that you also use for your minecraft server)", ex);
throw new ConfigurationException("""
BlueMap failed to initialize the webserver.
Check your webserver-config if everything is configured correctly.
(Make sure you DON'T use the same port for bluemap that you also use for your minecraft server)
""".strip(), ex);
}
}
@ -342,8 +344,8 @@ public void run() {
TimerTask metricsTask = new TimerTask() {
@Override
public void run() {
if (Plugin.this.serverInterface.isMetricsEnabled().getOr(coreConfig::isMetrics))
Metrics.sendReport(Plugin.this.implementationType);
if (serverInterface.isMetricsEnabled().getOr(coreConfig::isMetrics))
Metrics.sendReport(implementationType, configManager.getMinecraftVersion().getVersionString());
}
};
daemonTimer.scheduleAtFixedRate(metricsTask, TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(30));
@ -539,7 +541,7 @@ public void savePlayerStates() {
Predicate.not(pluginState::isPlayerHidden)
);
try (
OutputStream out = map.getStorage().writeMeta(map.getId(), BmMap.META_FILE_PLAYERS);
OutputStream out = map.getStorage().players().write();
Writer writer = new OutputStreamWriter(out)
) {
writer.write(dataSupplier.get());

View File

@ -52,6 +52,7 @@
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.map.MapRenderState;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.World;
@ -67,8 +68,6 @@
public class Commands<S> {
public static final String DEFAULT_MARKER_SET_ID = "markers";
private final Plugin plugin;
private final CommandDispatcher<S> dispatcher;
private final Function<S, CommandSource> commandSourceInterface;
@ -892,8 +891,13 @@ public int storagesCommand(CommandContext<S> context) {
source.sendMessage(Text.of(TextColor.BLUE, "Storages loaded by BlueMap:"));
for (var entry : plugin.getBlueMap().getConfig().getStorageConfigs().entrySet()) {
String storageTypeKey = "?";
try {
storageTypeKey = entry.getValue().getStorageType().getKey().getFormatted();
} catch (ConfigurationException ignore) {} // should never happen
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, entry.getKey())
.setHoverText(Text.of(entry.getValue().getStorageTypeKey().getFormatted()))
.setHoverText(Text.of(storageTypeKey))
.setClickAction(Text.ClickAction.RUN_COMMAND, "/bluemap storages " + entry.getKey())
);
}
@ -916,7 +920,7 @@ public int storagesInfoCommand(CommandContext<S> context) {
Collection<String> mapIds;
try {
mapIds = storage.collectMapIds();
mapIds = storage.mapIds().toList();
} catch (IOException ex) {
Logger.global.logError("Unexpected exception trying to load mapIds from storage '" + storageId + "'!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to access this storage. Please check the console for more details..."));
@ -929,7 +933,7 @@ public int storagesInfoCommand(CommandContext<S> context) {
} else {
for (String mapId : mapIds) {
BmMap map = plugin.getBlueMap().getMaps().get(mapId);
boolean isLoaded = map != null && map.getStorage().equals(storage);
boolean isLoaded = map != null && map.getStorage().equals(storage.map(mapId));
if (isLoaded) {
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, mapId, TextColor.GREEN, TextFormat.ITALIC, " (loaded)"));
@ -947,9 +951,9 @@ public int storagesDeleteMapCommand(CommandContext<S> context) {
String storageId = context.getArgument("storage", String.class);
String mapId = context.getArgument("map", String.class);
Storage storage;
MapStorage storage;
try {
storage = plugin.getBlueMap().getOrLoadStorage(storageId);
storage = plugin.getBlueMap().getOrLoadStorage(storageId).map(mapId);
} catch (ConfigurationException | InterruptedException ex) {
Logger.global.logError("Unexpected exception trying to load storage '" + storageId + "'!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to load this storage. Please check the console for more details..."));

View File

@ -92,7 +92,7 @@ public CompletableFuture<Void> updateSkin(final UUID playerUuid) {
BufferedImage playerHead = playerMarkerIconFactory.apply(playerUuid, skin.get());
for (BmMap map : maps.values()) {
try (OutputStream out = map.getStorage().writeMeta(map.getId(), "assets/playerheads/" + playerUuid + ".png")) {
try (OutputStream out = map.getStorage().asset("playerheads/" + playerUuid + ".png").write()) {
ImageIO.write(playerHead, "png", out);
} catch (IOException ex) {
Logger.global.logError("Failed to write player skin to storage: " + playerUuid, ex);

View File

@ -57,8 +57,8 @@ public void doWork() throws Exception {
try {
// purge the map
map.getStorage().purgeMap(map.getId(), progressInfo -> {
this.progress = progressInfo.getProgress();
map.getStorage().delete(progress -> {
this.progress = progress;
return !this.cancelled;
});

View File

@ -25,20 +25,20 @@
package de.bluecolored.bluemap.common.rendermanager;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import java.util.Objects;
public class StorageDeleteTask implements RenderTask {
private final Storage storage;
private final MapStorage storage;
private final String mapId;
private volatile double progress;
private volatile boolean hasMoreWork;
private volatile boolean cancelled;
public StorageDeleteTask(Storage storage, String mapId) {
public StorageDeleteTask(MapStorage storage, String mapId) {
this.storage = Objects.requireNonNull(storage);
this.mapId = Objects.requireNonNull(mapId);
this.progress = 0d;
@ -55,8 +55,8 @@ public void doWork() throws Exception {
if (this.cancelled) return;
// purge the map
storage.purgeMap(mapId, progressInfo -> {
this.progress = progressInfo.getProgress();
storage.delete(progress -> {
this.progress = progress;
return !this.cancelled;
});
}

View File

@ -30,6 +30,7 @@
import de.bluecolored.bluemap.common.serverinterface.Server;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import org.jetbrains.annotations.Nullable;
@ -40,20 +41,20 @@
public class MapRequestHandler extends RoutingRequestHandler {
public MapRequestHandler(BmMap map, Server serverInterface, PluginConfig pluginConfig, Predicate<UUID> playerFilter) {
this(map.getId(), map.getStorage(),
this(map.getStorage(),
createPlayersDataSupplier(map, serverInterface, pluginConfig, playerFilter),
new LiveMarkersDataSupplier(map.getMarkerSets()));
}
public MapRequestHandler(String mapId, Storage mapStorage) {
this(mapId, mapStorage, null, null);
public MapRequestHandler(MapStorage mapStorage) {
this(mapStorage, null, null);
}
public MapRequestHandler(String mapId, Storage mapStorage,
public MapRequestHandler(MapStorage mapStorage,
@Nullable Supplier<String> livePlayersDataSupplier,
@Nullable Supplier<String> liveMarkerDataSupplier) {
register(".*", new MapStorageRequestHandler(mapId, mapStorage));
register(".*", new MapStorageRequestHandler(mapStorage));
if (livePlayersDataSupplier != null) {
register("live/players\\.json", "", new JsonDataRequestHandler(

View File

@ -24,44 +24,38 @@
*/
package de.bluecolored.bluemap.common.web;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.ContentTypeRegistry;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.web.http.*;
import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.TileInfo;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.io.*;
import java.util.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@DebugDump
@RequiredArgsConstructor
public class MapStorageRequestHandler implements HttpRequestHandler {
private static final Pattern TILE_PATTERN = Pattern.compile("tiles/([\\d/]+)/x(-?[\\d/]+)z(-?[\\d/]+).*");
private final String mapId;
private final Storage mapStorage;
public MapStorageRequestHandler(BmMap map) {
this.mapId = map.getId();
this.mapStorage = map.getStorage();
}
public MapStorageRequestHandler(String mapId, Storage mapStorage) {
this.mapId = mapId;
this.mapStorage = mapStorage;
}
private final MapStorage mapStorage;
@SuppressWarnings("resource")
@Override
public HttpResponse handle(HttpRequest request) {
String path = request.getPath();
@ -78,58 +72,36 @@ public HttpResponse handle(HttpRequest request) {
int lod = Integer.parseInt(tileMatcher.group(1));
int x = Integer.parseInt(tileMatcher.group(2).replace("/", ""));
int z = Integer.parseInt(tileMatcher.group(3).replace("/", ""));
Optional<TileInfo> optTileInfo = mapStorage.readMapTileInfo(mapId, lod, new Vector2i(x, z));
if (optTileInfo.isPresent()) {
TileInfo tileInfo = optTileInfo.get();
GridStorage gridStorage = lod == 0 ? mapStorage.hiresTiles() : mapStorage.lowresTiles(lod);
CompressedInputStream in = gridStorage.read(x, z);
if (in == null) return new HttpResponse(HttpStatusCode.NO_CONTENT);
// check e-tag
String eTag = calculateETag(path, tileInfo);
HttpHeader etagHeader = request.getHeader("If-None-Match");
if (etagHeader != null){
if(etagHeader.getValue().equals(eTag)) {
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
}
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("Cache-Control", "public");
response.addHeader("Cache-Control", "max-age=" + TimeUnit.DAYS.toSeconds(1));
// check modified-since
long lastModified = tileInfo.getLastModified();
HttpHeader modHeader = request.getHeader("If-Modified-Since");
if (modHeader != null){
try {
long since = stringToTimestamp(modHeader.getValue());
if (since + 1000 >= lastModified){
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
} catch (IllegalArgumentException ignored){}
}
if (lod == 0) response.addHeader("Content-Type", "application/octet-stream");
else response.addHeader("Content-Type", "image/png");
CompressedInputStream compressedIn = tileInfo.readMapTile();
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("ETag", eTag);
if (lastModified > 0)
response.addHeader("Last-Modified", timestampToString(lastModified));
response.addHeader("Cache-Control", "public");
response.addHeader("Cache-Control", "max-age=" + TimeUnit.DAYS.toSeconds(1));
if (lod == 0) response.addHeader("Content-Type", "application/octet-stream");
else response.addHeader("Content-Type", "image/png");
writeToResponse(compressedIn, response, request);
return response;
}
writeToResponse(in, response, request);
return response;
}
// provide meta-data
Optional<InputStream> optIn = mapStorage.readMeta(mapId, path);
if (optIn.isPresent()) {
CompressedInputStream compressedIn = new CompressedInputStream(optIn.get(), Compression.NONE);
CompressedInputStream in = switch (path) {
case "settings.json" -> mapStorage.settings().read();
case "textures.json" -> mapStorage.textures().read();
case "live/markers.json" -> mapStorage.markers().read();
case "live/players.json" -> mapStorage.players().read();
default -> null;
};
if (in != null){
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
response.addHeader("Cache-Control", "public");
response.addHeader("Cache-Control", "max-age=" + TimeUnit.DAYS.toSeconds(1));
response.addHeader("Content-Type", ContentTypeRegistry.fromFileName(path));
writeToResponse(compressedIn, response, request);
writeToResponse(in, response, request);
return response;
}
@ -139,27 +111,23 @@ public HttpResponse handle(HttpRequest request) {
return new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
return new HttpResponse(HttpStatusCode.NO_CONTENT);
}
private String calculateETag(String path, TileInfo tileInfo) {
return Long.toHexString(tileInfo.getSize()) + Integer.toHexString(path.hashCode()) + Long.toHexString(tileInfo.getLastModified());
return new HttpResponse(HttpStatusCode.NOT_FOUND);
}
private void writeToResponse(CompressedInputStream data, HttpResponse response, HttpRequest request) throws IOException {
Compression compression = data.getCompression();
if (
compression != Compression.NONE &&
request.hasHeaderValue("Accept-Encoding", compression.getTypeId())
request.hasHeaderValue("Accept-Encoding", compression.getId())
) {
response.addHeader("Content-Encoding", compression.getTypeId());
response.addHeader("Content-Encoding", compression.getId());
response.setData(data);
} else if (
compression != Compression.GZIP &&
!response.hasHeaderValue("Content-Type", "image/png") &&
request.hasHeaderValue("Accept-Encoding", Compression.GZIP.getTypeId())
request.hasHeaderValue("Accept-Encoding", Compression.GZIP.getId())
) {
response.addHeader("Content-Encoding", Compression.GZIP.getTypeId());
response.addHeader("Content-Encoding", Compression.GZIP.getId());
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
try (OutputStream os = Compression.GZIP.compress(byteOut)) {
IOUtils.copyLarge(data.decompress(), os);
@ -167,41 +135,7 @@ private void writeToResponse(CompressedInputStream data, HttpResponse response,
byte[] compressedData = byteOut.toByteArray();
response.setData(new ByteArrayInputStream(compressedData));
} else {
response.setData(new BufferedInputStream(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 "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);
response.setData(data.decompress());
}
}

View File

@ -6,7 +6,7 @@
# 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: "bluemap:file"
storage-type: file
# The path to the folder on your file-system where bluemap will save the rendered map
# The default is: "bluemap/web/maps"
@ -17,4 +17,4 @@ root: "${root}"
# - GZIP
# - NONE
# The default is: GZIP
compression: GZIP
compression: gzip

View File

@ -6,7 +6,7 @@
# 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: "bluemap:sql"
storage-type: sql
# The JDBC-Connection URL that is used to connect to the database.
# The format for this url is usually something like: jdbc:[driver]://[host]:[port]/[database]
@ -39,7 +39,7 @@ max-connections: -1
# The compression-type that bluemap will use to compress generated map-data.
# Available compression-types are:
# - GZIP
# - NONE
# The default is: GZIP
compression: GZIP
# - gzip
# - none
# The default is: gzip
compression: gzip

View File

@ -36,7 +36,8 @@
import de.bluecolored.bluemap.core.map.lowres.LowresTileManager;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.World;
@ -44,19 +45,12 @@
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
@DebugDump
public class BmMap {
public static final String META_FILE_SETTINGS = "settings.json";
public static final String META_FILE_TEXTURES = "textures.json";
public static final String META_FILE_RENDER_STATE = ".rstate";
public static final String META_FILE_MARKERS = "live/markers.json";
public static final String META_FILE_PLAYERS = "live/players.json";
private static final Gson GSON = ResourcesGson.addAdapter(new GsonBuilder())
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.registerTypeAdapter(BmMap.class, new MapSettingsSerializer())
@ -65,7 +59,7 @@ public class BmMap {
private final String id;
private final String name;
private final World world;
private final Storage storage;
private final MapStorage storage;
private final MapSettings mapSettings;
private final ResourcePack resourcePack;
@ -82,7 +76,7 @@ public class BmMap {
private long renderTimeSumNanos;
private long tilesRendered;
public BmMap(String id, String name, World world, Storage storage, ResourcePack resourcePack, MapSettings settings) throws IOException {
public BmMap(String id, String name, World world, MapStorage storage, ResourcePack resourcePack, MapSettings settings) throws IOException {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.world = Objects.requireNonNull(world);
@ -98,7 +92,7 @@ public BmMap(String id, String name, World world, Storage storage, ResourcePack
saveTextureGallery();
this.hiresModelManager = new HiresModelManager(
storage.tileStorage(id, 0),
storage.hiresTiles(),
this.resourcePack,
this.textureGallery,
settings,
@ -106,7 +100,7 @@ public BmMap(String id, String name, World world, Storage storage, ResourcePack
);
this.lowresTileManager = new LowresTileManager(
storage.mapStorage(id),
storage,
new Grid(settings.getLowresTileSize()),
settings.getLodCount(),
settings.getLodFactor()
@ -145,7 +139,7 @@ public synchronized void save() {
// only save texture gallery if not present in storage
try {
if (storage.readMetaInfo(id, META_FILE_TEXTURES).isEmpty())
if (!storage.textures().exists())
saveTextureGallery();
} catch (IOException e) {
Logger.global.logError("Failed to read texture gallery", e);
@ -153,18 +147,16 @@ public synchronized void save() {
}
private void loadRenderState() throws IOException {
Optional<InputStream> rstateData = storage.readMeta(id, META_FILE_RENDER_STATE);
if (rstateData.isPresent()) {
try (InputStream in = rstateData.get()){
this.renderState.load(in);
} catch (IOException ex) {
Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex);
}
try (CompressedInputStream in = storage.renderState().read()){
if (in != null)
this.renderState.load(in.decompress());
} catch (IOException ex) {
Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex);
}
}
public synchronized void saveRenderState() {
try (OutputStream out = storage.writeMeta(id, META_FILE_RENDER_STATE)) {
try (OutputStream out = storage.renderState().write()) {
this.renderState.save(out);
} catch (IOException ex){
Logger.global.logError("Failed to save render-state for map: '" + this.id + "'!", ex);
@ -172,20 +164,18 @@ public synchronized void saveRenderState() {
}
private TextureGallery loadTextureGallery() throws IOException {
TextureGallery gallery = null;
Optional<InputStream> texturesData = storage.readMeta(id, META_FILE_TEXTURES);
if (texturesData.isPresent()) {
try (InputStream in = texturesData.get()){
gallery = TextureGallery.readTexturesFile(in);
} catch (IOException ex) {
Logger.global.logError("Failed to load textures for map '" + getId() + "'!", ex);
}
try (CompressedInputStream in = storage.textures().read()){
if (in != null)
return TextureGallery.readTexturesFile(in.decompress());
} catch (IOException ex) {
Logger.global.logError("Failed to load textures for map '" + getId() + "'!", ex);
}
return gallery != null ? gallery : new TextureGallery();
return new TextureGallery();
}
private void saveTextureGallery() {
try (OutputStream out = storage.writeMeta(id, META_FILE_TEXTURES)) {
try (OutputStream out = storage.textures().write()) {
this.textureGallery.writeTexturesFile(out);
} catch (IOException ex) {
Logger.global.logError("Failed to save textures for map '" + getId() + "'!", ex);
@ -199,7 +189,7 @@ public synchronized void resetTextureGallery() {
private void saveMapSettings() {
try (
OutputStream out = storage.writeMeta(id, META_FILE_SETTINGS);
OutputStream out = storage.settings().write();
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)
) {
GSON.toJson(this, writer);
@ -210,7 +200,7 @@ private void saveMapSettings() {
public synchronized void saveMarkerState() {
try (
OutputStream out = storage.writeMeta(id, META_FILE_MARKERS);
OutputStream out = storage.markers().write();
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)
) {
MarkerGson.INSTANCE.toJson(this.markerSets, writer);
@ -220,9 +210,7 @@ public synchronized void saveMarkerState() {
}
public synchronized void savePlayerState() {
try (
OutputStream out = storage.writeMeta(id, META_FILE_PLAYERS)
) {
try (OutputStream out = storage.players().write()) {
out.write("{}".getBytes(StandardCharsets.UTF_8));
} catch (Exception ex) {
Logger.global.logError("Failed to save markers for map '" + getId() + "'!", ex);
@ -241,7 +229,7 @@ public World getWorld() {
return world;
}
public Storage getStorage() {
public MapStorage getStorage() {
return storage;
}
@ -284,12 +272,8 @@ public int hashCode() {
@Override
public boolean equals(Object obj) {
if (obj instanceof BmMap) {
BmMap that = (BmMap) obj;
if (obj instanceof BmMap that)
return this.id.equals(that.id);
}
return false;
}

View File

@ -30,7 +30,7 @@
import de.bluecolored.bluemap.core.map.TextureGallery;
import de.bluecolored.bluemap.core.map.TileMetaConsumer;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.World;
import lombok.Getter;
@ -40,17 +40,17 @@
public class HiresModelManager {
private final Storage.TileStorage storage;
private final GridStorage storage;
private final HiresModelRenderer renderer;
@Getter
private final Grid tileGrid;
public HiresModelManager(Storage.TileStorage storage, ResourcePack resourcePack, TextureGallery textureGallery, RenderSettings renderSettings, Grid tileGrid) {
public HiresModelManager(GridStorage storage, ResourcePack resourcePack, TextureGallery textureGallery, RenderSettings renderSettings, Grid tileGrid) {
this(storage, new HiresModelRenderer(resourcePack, textureGallery, renderSettings), tileGrid);
}
public HiresModelManager(Storage.TileStorage storage, HiresModelRenderer renderer, Grid tileGrid) {
public HiresModelManager(GridStorage storage, HiresModelRenderer renderer, Grid tileGrid) {
this.storage = storage;
this.renderer = renderer;
@ -81,7 +81,7 @@ public void render(World world, Vector2i tile, TileMetaConsumer tileMetaConsumer
private void save(final TileModel model, Vector2i tile) {
try (
OutputStream out = storage.write(tile);
OutputStream out = storage.write(tile.getX(), tile.getY());
PRBMWriter modelWriter = new PRBMWriter(out)
) {
modelWriter.write(model);

View File

@ -28,10 +28,10 @@
import com.github.benmanes.caffeine.cache.*;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.util.Vector2iCache;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.util.Grid;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.Nullable;
@ -44,7 +44,7 @@ public class LowresLayer {
private static final Vector2iCache VECTOR_2_I_CACHE = new Vector2iCache();
private final Storage.MapStorage mapStorage;
private final GridStorage storage;
private final Grid tileGrid;
private final int lodFactor;
@ -54,10 +54,10 @@ public class LowresLayer {
@Nullable private final LowresLayer nextLayer;
public LowresLayer(
Storage.MapStorage mapStorage, Grid tileGrid, int lodCount, int lodFactor,
GridStorage storage, Grid tileGrid, int lodFactor,
int lod, @Nullable LowresLayer nextLayer
) {
this.mapStorage = mapStorage;
this.storage = storage;
this.tileGrid = tileGrid;
this.lodFactor = lodFactor;
@ -83,7 +83,7 @@ public void write(@NonNull Vector2i key, @NonNull LowresTile value) {}
@Override
public void delete(@NonNull Vector2i key, @Nullable LowresTile value, @NonNull RemovalCause cause) {
saveTile(key, value, cause);
saveTile(key, value);
}
})
.build(tileWeakInstanceCache::get);
@ -95,7 +95,7 @@ public void save() {
}
private LowresTile createTile(Vector2i tilePos) {
try (InputStream in = mapStorage.read(lod, tilePos).orElse(null)) {
try (InputStream in = storage.read(tilePos.getX(), tilePos.getY())) {
if (in != null) return new LowresTile(tileGrid.getGridSize(), in);
} catch (IOException e) {
Logger.global.logError("Failed to load tile " + tilePos + " (lod: " + lod + ")", e);
@ -105,17 +105,17 @@ private LowresTile createTile(Vector2i tilePos) {
return new LowresTile(tileGrid.getGridSize());
}
private void saveTile(Vector2i tilePos, @Nullable LowresTile tile, RemovalCause removalCause) {
private void saveTile(Vector2i tilePos, @Nullable LowresTile tile) {
if (tile == null) return;
// check if storage is closed
if (mapStorage.getStorage().isClosed()){
if (storage.isClosed()){
Logger.global.logDebug("Tried to save tile " + tilePos + " (lod: " + lod + ") but storage is already closed.");
return;
}
// save the tile
try (OutputStream out = mapStorage.write(lod, tilePos)) {
try (OutputStream out = storage.write(tilePos.getX(), tilePos.getY())) {
tile.save(out);
} catch (IOException e) {
Logger.global.logError("Failed to save tile " + tilePos + " (lod: " + lod + ")", e);

View File

@ -25,9 +25,9 @@
package de.bluecolored.bluemap.core.map.lowres;
import de.bluecolored.bluemap.core.map.TileMetaConsumer;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.util.math.Color;
public class LowresTileManager implements TileMetaConsumer {
@ -36,14 +36,14 @@ public class LowresTileManager implements TileMetaConsumer {
private final LowresLayer[] layers;
public LowresTileManager(Storage.MapStorage mapStorage, Grid tileGrid, int lodCount, int lodFactor) {
public LowresTileManager(MapStorage storage, Grid tileGrid, int lodCount, int lodFactor) {
this.tileGrid = tileGrid;
this.lodFactor = lodFactor;
this.lodCount = lodCount;
this.layers = new LowresLayer[lodCount];
for (int i = lodCount - 1; i >= 0; i--) {
this.layers[i] = new LowresLayer(mapStorage, tileGrid, lodCount, lodFactor, i + 1,
this.layers[i] = new LowresLayer(storage.lowresTiles(i + 1), tileGrid, lodFactor, i + 1,
(i == lodCount - 1) ? null : layers[i + 1]);
}
}

View File

@ -24,7 +24,8 @@
*/
package de.bluecolored.bluemap.core.metrics;
import com.google.gson.JsonObject;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
@ -39,18 +40,22 @@
public class Metrics {
private static final String METRICS_REPORT_URL = "https://metrics.bluecolored.de/bluemap/";
private static final Gson GSON = new GsonBuilder()
.create();
public static void sendReportAsync(String implementation) {
new Thread(() -> sendReport(implementation), "BlueMap-Plugin-SendMetricsReport").start();
public static void sendReportAsync(String implementation, String mcVersion) {
new Thread(() -> sendReport(implementation, mcVersion), "BlueMap-Plugin-SendMetricsReport").start();
}
public static void sendReport(String implementation) {
JsonObject data = new JsonObject();
data.addProperty("implementation", implementation);
data.addProperty("version", BlueMap.VERSION);
public static void sendReport(String implementation, String mcVersion) {
Report report = new Report(
implementation,
BlueMap.VERSION,
mcVersion
);
try {
sendData(data.toString());
sendData(GSON.toJson(report));
} catch (IOException | RuntimeException ex) {
Logger.global.logDebug("Failed to send Metrics-Report: " + ex);
}
@ -86,4 +91,10 @@ private static String sendData(String data) throws IOException {
}
record Report (
String implementation,
String version,
String mcVersion
) {}
}

View File

@ -1,92 +0,0 @@
/*
* 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 io.airlift.compress.zstd.ZstdInputStream;
import io.airlift.compress.zstd.ZstdOutputStream;
import net.jpountz.lz4.LZ4FrameInputStream;
import net.jpountz.lz4.LZ4FrameOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.NoSuchElementException;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.InflaterInputStream;
public enum Compression {
NONE("none", "", out -> out, in -> in),
GZIP("gzip", ".gz", GZIPOutputStream::new, GZIPInputStream::new),
DEFLATE("deflate", ".deflate", DeflaterOutputStream::new, InflaterInputStream::new),
ZSTD("zstd", ".zst", ZstdOutputStream::new, ZstdInputStream::new),
LZ4("lz4", ".lz4", LZ4FrameOutputStream::new, LZ4FrameInputStream::new);
private final String typeId;
private final String fileSuffix;
private final StreamTransformer<OutputStream> compressor;
private final StreamTransformer<InputStream> decompressor;
Compression(String typeId, String fileSuffix,
StreamTransformer<OutputStream> compressor,
StreamTransformer<InputStream> decompressor) {
this.fileSuffix = fileSuffix;
this.typeId = typeId;
this.compressor = compressor;
this.decompressor = decompressor;
}
public String getTypeId() {
return typeId;
}
public String getFileSuffix() {
return fileSuffix;
}
public OutputStream compress(OutputStream out) throws IOException {
return compressor.apply(out);
}
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

@ -0,0 +1,100 @@
package de.bluecolored.bluemap.core.storage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.OutputStream;
import java.util.stream.Stream;
/**
* A storage storing items on an infinite grid (x,z), each position on the grid can hold one item.
*/
public interface GridStorage {
/**
* Returns an {@link OutputStream} that can be used to write an item into this storage at the given position
* (overwriting any existing item).
* The OutputStream is expected to be closed by the caller of this method.
*/
OutputStream write(int x, int z) throws IOException;
/**
* Returns a {@link CompressedInputStream} that can be used to read the item from this storage at the given position
* or null if there is no item stored.
* The CompressedInputStream is expected to be closed by the caller of this method.
*/
@Nullable CompressedInputStream read(int x, int z) throws IOException;
/**
* Deletes the item from this storage at the given position
*/
void delete(int x, int z) throws IOException;
/**
* Tests if there is an item stored on the given position in this storage
*/
boolean exists(int x, int z) throws IOException;
/**
* Returns a stream over all <b>existing</b> items in this storage
*/
Stream<Cell> stream() throws IOException;
/**
* Checks if this storage is closed
*/
boolean isClosed();
interface Cell extends SingleItemStorage {
/**
* Returns the x position of this item in the grid
*/
int getX();
/**
* Returns the z position of this item in the grid
*/
int getZ();
}
@SuppressWarnings("ClassCanBeRecord")
@Getter
@RequiredArgsConstructor
class GridStorageCell implements Cell {
private final GridStorage storage;
private final int x, z;
@Override
public OutputStream write() throws IOException {
return storage.write(x, z);
}
@Override
public CompressedInputStream read() throws IOException {
return storage.read(x, z);
}
@Override
public void delete() throws IOException {
storage.delete(x, z);
}
@Override
public boolean exists() throws IOException {
return storage.exists(x, z);
}
@Override
public boolean isClosed() {
return storage.isClosed();
}
}
}

View File

@ -0,0 +1,80 @@
package de.bluecolored.bluemap.core.storage;
import java.io.IOException;
import java.util.function.DoublePredicate;
public interface MapStorage {
/**
* Returns the {@link GridStorage} holding the maps hires-tiles
*/
GridStorage hiresTiles();
/**
* Returns the {@link GridStorage} holding the maps lowres-tiles of the given lod level
*/
GridStorage lowresTiles(int lod);
/**
* Returns a {@link SingleItemStorage} for a map asset with the given name
*/
SingleItemStorage asset(String name);
/**
* Returns a {@link SingleItemStorage} for the render-state data of this map
*/
SingleItemStorage renderState();
/**
* Returns a {@link SingleItemStorage} for the settings (settings.json) of this map
*/
SingleItemStorage settings();
/**
* Returns a {@link SingleItemStorage} for the texture-data (textures.json) of this map
*/
SingleItemStorage textures();
/**
* Returns a {@link SingleItemStorage} for the marker-data (live/markers.json) of this map
*/
SingleItemStorage markers();
/**
* Returns a {@link SingleItemStorage} for the player-data (live/players.json) of this map
*/
SingleItemStorage players();
/**
* Deletes the entire map from the storage
*/
default void delete() throws IOException {
delete(info -> true);
}
/**
* Deletes the entire map from the storage
* @param onProgress a function that takes in a progress-percentage and returns true
* if the deletion should continue or false if it should be aborted.
* No guarantees are made on how often (if at all) this method is actually being called and if the
* progress is actually aborted when false is returned.
*/
void delete(DoublePredicate onProgress) throws IOException;
/**
* Tests whether this map currently exists on the storage or not
*/
boolean exists() throws IOException;
/**
* Checks if this storage is closed
*/
boolean isClosed();
static String escapeAssetName(String name) {
return name
.replaceAll("[^\\w\\d.\\-_/]", "_")
.replace("..", "_.");
}
}

View File

@ -1,36 +0,0 @@
/*
* 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 interface MetaInfo {
InputStream readMeta() throws IOException;
long getSize();
}

View File

@ -0,0 +1,40 @@
package de.bluecolored.bluemap.core.storage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.OutputStream;
public interface SingleItemStorage {
/**
* Returns an {@link OutputStream} that can be used to write the item-data of this storage
* (overwriting any existing item).
* The OutputStream is expected to be closed by the caller of this method.
*/
OutputStream write() throws IOException;
/**
* Returns a {@link CompressedInputStream} that can be used to read the item-data from this storage
* or null if there is nothing stored.
* The CompressedInputStream is expected to be closed by the caller of this method.
*/
@Nullable CompressedInputStream read() throws IOException;
/**
* Deletes the item from this storage
*/
void delete() throws IOException;
/**
* Tests if this item of this storage exists
*/
boolean exists() throws IOException;
/**
* Checks if this storage is closed
*/
boolean isClosed();
}

View File

@ -1,143 +1,30 @@
/*
* 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 com.flowpowered.math.vector.Vector2i;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
public abstract class Storage implements Closeable {
public interface Storage extends Closeable {
public abstract void initialize() throws IOException;
/**
* Does everything necessary to initialize this storage.
* (E.g. create tables on a database if they don't exist or upgrade older data).
*/
void initialize() throws IOException;
public abstract OutputStream writeMapTile(String mapId, int lod, Vector2i tile) throws IOException;
/**
* Returns the {@link MapStorage} for the given mapId
*/
MapStorage map(String mapId);
public abstract Optional<CompressedInputStream> readMapTile(String mapId, int lod, Vector2i tile) throws IOException;
/**
* Fetches and returns a stream of all map-id's in this storage
*/
Stream<String> mapIds() throws IOException;
public abstract Optional<TileInfo> readMapTileInfo(String mapId, int lod, Vector2i tile) throws IOException;
public abstract void deleteMapTile(String mapId, int lod, Vector2i tile) throws IOException;
public abstract OutputStream writeMeta(String mapId, String name) throws IOException;
public abstract Optional<InputStream> readMeta(String mapId, String name) throws IOException;
public abstract Optional<MetaInfo> readMetaInfo(String mapId, String name) throws IOException;
public abstract void deleteMeta(String mapId, String name) throws IOException;
public abstract void purgeMap(String mapId, Function<ProgressInfo, Boolean> onProgress) throws IOException;
public abstract Collection<String> collectMapIds() throws IOException;
public MapStorage mapStorage(final String mapId) {
return new MapStorage(mapId);
}
public TileStorage tileStorage(final String mapId, final int lod) {
return new TileStorage(mapId, lod);
}
public abstract boolean isClosed();
public class MapStorage {
private final String mapId;
private MapStorage(String mapId) {
this.mapId = mapId;
}
public OutputStream write(int lod, Vector2i tile) throws IOException {
return writeMapTile(mapId, lod, tile);
}
public Optional<CompressedInputStream> read(int lod, Vector2i tile) throws IOException {
return readMapTile(mapId, lod, tile);
}
public void delete(int lod, Vector2i tile) throws IOException {
deleteMapTile(mapId, lod, tile);
}
public Storage getStorage() {
return Storage.this;
}
}
public class TileStorage {
private final String mapId;
private final int lod;
private TileStorage(String mapId, int lod) {
this.mapId = mapId;
this.lod = lod;
}
public OutputStream write(Vector2i tile) throws IOException {
return writeMapTile(mapId, lod, tile);
}
public Optional<CompressedInputStream> read(Vector2i tile) throws IOException {
return readMapTile(mapId, lod, tile);
}
public void delete(Vector2i tile) throws IOException {
deleteMapTile(mapId, lod, tile);
}
public Storage getStorage() {
return Storage.this;
}
}
public static class ProgressInfo {
private final double progress;
public ProgressInfo(double progress) {
this.progress = progress;
}
public double getProgress() {
return progress;
}
}
public static String escapeMetaName(String name) {
return name.replaceAll("[^\\w\\d.\\-_/]", "_").replace("..", "_.");
}
/**
* Checks if this storage is closed
*/
boolean isClosed();
}

View File

@ -1,39 +0,0 @@
/*
* 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;
public interface TileInfo {
CompressedInputStream readMapTile() throws IOException;
Compression getCompression();
long getSize();
long getLastModified();
}

View File

@ -0,0 +1,33 @@
package de.bluecolored.bluemap.core.storage.compression;
import de.bluecolored.bluemap.core.util.Key;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.*;
@RequiredArgsConstructor
public class BufferedCompression implements Compression {
@Getter private final Key key;
@Getter private final String id;
@Getter private final String fileSuffix;
private final StreamTransformer<OutputStream> compressor;
private final StreamTransformer<InputStream> decompressor;
@Override
public OutputStream compress(OutputStream out) throws IOException {
return new BufferedOutputStream(compressor.apply(out));
}
@Override
public InputStream decompress(InputStream in) throws IOException {
return new BufferedInputStream(decompressor.apply(in));
}
@FunctionalInterface
public interface StreamTransformer<T> {
T apply(T original) throws IOException;
}
}

View File

@ -22,26 +22,40 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
package de.bluecolored.bluemap.core.storage.compression;
import de.bluecolored.bluemap.core.util.stream.DelegateInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* An InputStream that is aware of the {@link Compression} that it's data is compressed with.
*/
public class CompressedInputStream extends DelegateInputStream {
private final Compression compression;
/**
* Creates a new CompressedInputStream from an <b>already compressed</b> {@link InputStream} and the {@link Compression}
* it is compressed with.
* This does <b>not</b> compress the provided InputStream.
*/
public CompressedInputStream(InputStream in, Compression compression) {
super(in);
this.compression = compression;
}
/**
* Returns the decompressed {@link InputStream}
*/
public InputStream decompress() throws IOException {
return compression.decompress(in);
}
/**
* Returns the {@link Compression} this InputStream's data is compressed with
*/
public Compression getCompression() {
return compression;
}

View File

@ -0,0 +1,67 @@
/*
* 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.compression;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import io.airlift.compress.zstd.ZstdInputStream;
import io.airlift.compress.zstd.ZstdOutputStream;
import net.jpountz.lz4.LZ4FrameInputStream;
import net.jpountz.lz4.LZ4FrameOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.InflaterInputStream;
public interface Compression extends Keyed {
Compression NONE = new NoCompression(Key.bluemap("none"), "none", "");
Compression GZIP = new BufferedCompression(Key.bluemap("gzip"), "gzip", ".gz", GZIPOutputStream::new, GZIPInputStream::new);
Compression DEFLATE = new BufferedCompression(Key.bluemap("deflate"), "deflate", ".deflate", DeflaterOutputStream::new, InflaterInputStream::new);
Compression ZSTD = new BufferedCompression(Key.bluemap("zstd"), "zstd", ".zst", ZstdOutputStream::new, ZstdInputStream::new);
Compression LZ4 = new BufferedCompression(Key.bluemap("lz4"), "lz4", ".lz4", LZ4FrameOutputStream::new, LZ4FrameInputStream::new);
Registry<Compression> REGISTRY = new Registry<>(
NONE,
GZIP,
DEFLATE,
ZSTD,
LZ4
);
String getId();
String getFileSuffix();
OutputStream compress(OutputStream out) throws IOException;
InputStream decompress(InputStream in) throws IOException;
}

View File

@ -0,0 +1,28 @@
package de.bluecolored.bluemap.core.storage.compression;
import de.bluecolored.bluemap.core.util.Key;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@RequiredArgsConstructor
public class NoCompression implements Compression {
@Getter private final Key key;
@Getter private final String id;
@Getter private final String fileSuffix;
@Override
public OutputStream compress(OutputStream out) throws IOException {
return out;
}
@Override
public InputStream decompress(InputStream in) throws IOException {
return in;
}
}

View File

@ -0,0 +1,124 @@
package de.bluecolored.bluemap.core.storage.file;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import de.bluecolored.bluemap.core.util.DeletingPathVisitor;
import de.bluecolored.bluemap.core.util.FileHelper;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.function.DoublePredicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Getter
public class FileMapStorage extends PathBasedMapStorage {
private final Path root;
public FileMapStorage(Path root, Compression compression) {
super(
compression,
".prbm",
".png"
);
this.root = root;
}
@Override
public SingleItemStorage file(Path file, Compression compression) {
return new FileItemStorage(root.resolve(file), compression);
}
@Override
@SuppressWarnings("resource")
public Stream<Path> files(Path path) throws IOException {
return Files.walk(root.resolve(path))
.filter(Files::isRegularFile);
}
@Override
public void delete(DoublePredicate onProgress) throws IOException {
if (!Files.exists(root)) return;
final int subFilesCount;
final LinkedList<Path> subFiles;
// collect sub-files to be able to provide progress-updates
try (Stream<Path> pathStream = Files.walk(root, 3)) {
subFiles = pathStream.collect(Collectors.toCollection(LinkedList::new));
}
subFilesCount = subFiles.size();
// delete subFiles first to be able to track the progress and cancel
while (!subFiles.isEmpty()) {
Path subFile = subFiles.getLast();
Files.walkFileTree(subFile, DeletingPathVisitor.INSTANCE);
subFiles.removeLast();
if (!onProgress.test(1d - (subFiles.size() / (double) subFilesCount)))
return;
}
// make sure everything is deleted
if (Files.exists(root))
Files.walkFileTree(root, DeletingPathVisitor.INSTANCE);
}
@Override
public boolean exists() throws IOException {
return Files.exists(root);
}
@Override
public boolean isClosed() {
return false;
}
@RequiredArgsConstructor
private static class FileItemStorage implements SingleItemStorage {
private final Path file;
private final Compression compression;
@Override
public OutputStream write() throws IOException {
return compression.compress(FileHelper.createFilepartOutputStream(file));
}
@Override
public CompressedInputStream read() throws IOException {
if (!Files.exists(file)) return null;
try {
return new CompressedInputStream(Files.newInputStream(file), compression);
} catch (FileNotFoundException | NoSuchFileException ex) {
return null;
}
}
@Override
public void delete() throws IOException {
Files.delete(file);
}
@Override
public boolean exists() {
return Files.exists(file);
}
@Override
public boolean isClosed() {
return false;
}
}
}

View File

@ -1,62 +1,43 @@
/*
* 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.file;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.*;
import de.bluecolored.bluemap.core.util.DeletingPathVisitor;
import de.bluecolored.bluemap.core.util.FileHelper;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import java.io.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@DebugDump
public class FileStorage extends Storage {
public class FileStorage implements Storage {
private final Path root;
private final Compression hiresCompression;
public FileStorage(FileStorageSettings config) {
this.root = config.getRoot();
this.hiresCompression = config.getCompression();
}
private final LoadingCache<String, FileMapStorage> mapStorages;
public FileStorage(Path root, Compression compression) {
this.root = root;
this.hiresCompression = compression;
mapStorages = Caffeine.newBuilder()
.build(id -> new FileMapStorage(root.resolve(id), compression));
}
@Override
public void initialize() {}
public void initialize() throws IOException {}
@Override
public FileMapStorage map(String mapId) {
return mapStorages.get(mapId);
}
@SuppressWarnings("resource")
@Override
public Stream<String> mapIds() throws IOException {
return Files.list(root)
.filter(Files::isDirectory)
.map(Path::getFileName)
.map(Path::toString);
}
@Override
public boolean isClosed() {
@ -66,186 +47,4 @@ public boolean isClosed() {
@Override
public void close() throws IOException {}
@Override
public OutputStream writeMapTile(String mapId, int lod, Vector2i tile) throws IOException {
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
Path file = getFilePath(mapId, lod, tile);
OutputStream os = FileHelper.createFilepartOutputStream(file);
return new BufferedOutputStream(compression.compress(os));
}
@Override
public Optional<CompressedInputStream> readMapTile(String mapId, int lod, Vector2i tile) throws IOException {
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
Path file = getFilePath(mapId, lod, tile);
if (!Files.exists(file)) return Optional.empty();
InputStream is = Files.newInputStream(file, StandardOpenOption.READ);
return Optional.of(new CompressedInputStream(is, compression));
}
@Override
public Optional<TileInfo> readMapTileInfo(String mapId, int lod, Vector2i tile) throws IOException {
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
Path file = getFilePath(mapId, lod, 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 TileInfo() {
@Override
public CompressedInputStream readMapTile() throws IOException {
return FileStorage.this.readMapTile(mapId, lod, 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
public void deleteMapTile(String mapId, int lod, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, lod, tile);
Files.deleteIfExists(file);
}
@Override
public OutputStream writeMeta(String mapId, String name) throws IOException {
Path file = getMetaFilePath(mapId, name);
OutputStream os = FileHelper.createFilepartOutputStream(file);
return new BufferedOutputStream(os);
}
@Override
public Optional<InputStream> readMeta(String mapId, String name) throws IOException {
Path file = getMetaFilePath(mapId, name);
if (!Files.exists(file)) return Optional.empty();
InputStream is = Files.newInputStream(file, StandardOpenOption.READ);
return Optional.of(new BufferedInputStream(is));
}
@Override
public Optional<MetaInfo> readMetaInfo(String mapId, String name) throws IOException {
Path file = getMetaFilePath(mapId, name);
if (!Files.exists(file)) return Optional.empty();
final long size = Files.size(file);
return Optional.of(new MetaInfo() {
@Override
public InputStream readMeta() throws IOException {
return FileStorage.this.readMeta(mapId, name)
.orElseThrow(() -> new IOException("Meta no longer present!"));
}
@Override
public long getSize() {
return size;
}
});
}
@Override
public void deleteMeta(String mapId, String name) throws IOException {
Path file = getMetaFilePath(mapId, name);
Files.deleteIfExists(file);
}
@Override
public void purgeMap(String mapId, Function<ProgressInfo, Boolean> onProgress) throws IOException {
final Path directory = getFilePath(mapId);
if (!Files.exists(directory)) return;
final int subFilesCount;
final LinkedList<Path> subFiles;
// collect sub-files to be able to provide progress-updates
try (Stream<Path> pathStream = Files.walk(directory, 3)) {
subFiles = pathStream.collect(Collectors.toCollection(LinkedList::new));
}
subFilesCount = subFiles.size();
// delete subFiles first to be able to track the progress and cancel
while (!subFiles.isEmpty()) {
Path subFile = subFiles.getLast();
Files.walkFileTree(subFile, DeletingPathVisitor.INSTANCE);
subFiles.removeLast();
if (!onProgress.apply(
new ProgressInfo(1d - (subFiles.size() / (double) subFilesCount))
)) return;
}
// make sure everything is deleted
if (Files.exists(directory))
Files.walkFileTree(directory, DeletingPathVisitor.INSTANCE);
}
@Override
public Collection<String> collectMapIds() throws IOException {
try (Stream<Path> fileStream = Files.list(root)) {
return fileStream
.filter(Files::isDirectory)
.map(path -> path.getFileName().toString())
.collect(Collectors.toList());
}
}
public Path getFilePath(String mapId, int lod, Vector2i tile){
String path = "x" + tile.getX() + "z" + tile.getY();
char[] cs = path.toCharArray();
List<String> folders = new ArrayList<>();
StringBuilder folder = new StringBuilder();
for (char c : cs){
folder.append(c);
if (c >= '0' && c <= '9'){
folders.add(folder.toString());
folder.delete(0, folder.length());
}
}
String fileName = folders.remove(folders.size() - 1);
Path p = getFilePath(mapId).resolve("tiles").resolve(Integer.toString(lod));
for (String s : folders){
p = p.resolve(s);
}
if (lod == 0) {
return p.resolve(fileName + ".prbm" + hiresCompression.getFileSuffix());
} else {
return p.resolve(fileName + ".png");
}
}
public Path getFilePath(String mapId) {
return root.resolve(mapId);
}
public Path getMetaFilePath(String mapId, String name) {
return getFilePath(mapId).resolve(escapeMetaName(name)
.replace("/", root.getFileSystem().getSeparator()));
}
}

View File

@ -1,37 +0,0 @@
/*
* 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.file;
import de.bluecolored.bluemap.core.storage.Compression;
import java.nio.file.Path;
public interface FileStorageSettings {
Path getRoot();
Compression getCompression();
}

View File

@ -0,0 +1,147 @@
package de.bluecolored.bluemap.core.storage.file;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@RequiredArgsConstructor
class PathBasedGridStorage implements GridStorage {
private static final Pattern ITEM_PATH_PATTERN = Pattern.compile("x(-?\\d+)z(-?\\d+)");
private final PathBasedMapStorage storage;
private final Path root;
private final String suffix;
private final Compression compression;
@Override
public OutputStream write(int x, int z) throws IOException {
return item(x, z).write();
}
@Override
public CompressedInputStream read(int x, int z) throws IOException {
return item(x, z).read();
}
@Override
public void delete(int x, int z) throws IOException {
item(x, z).delete();
}
@Override
public boolean exists(int x, int z) throws IOException {
return item(x, z).exists();
}
@Override
public Stream<Cell> stream() throws IOException {
return storage.files(root)
.<Cell>map(itemPath -> {
Path path = itemPath;
if (!path.startsWith(root)) return null;
path = root.relativize(path);
String name = path.toString();
name = name.replace(root.getFileSystem().getSeparator(), "");
if (!name.endsWith(suffix)) return null;
name = name.substring(name.length() - suffix.length());
Matcher matcher = ITEM_PATH_PATTERN.matcher(name);
if (!matcher.matches()) return null;
int x = Integer.parseInt(matcher.group(1));
int z = Integer.parseInt(matcher.group(2));
return new PathCell(x, z, itemPath);
})
.filter(Objects::nonNull);
}
@Override
public boolean isClosed() {
return storage.isClosed();
}
public SingleItemStorage item(int x, int z) {
return storage.file(root.resolve(getGridPath(x, z)), compression);
}
public Path getGridPath(int x, int z) {
StringBuilder sb = new StringBuilder()
.append('x')
.append(x)
.append('z')
.append(z);
LinkedList<String> folders = new LinkedList<>();
StringBuilder folder = new StringBuilder();
sb.chars().forEach(i -> {
char c = (char) i;
folder.append(c);
if (c >= '0' && c <= '9') {
folders.add(folder.toString());
folder.delete(0, folder.length());
}
});
String fileName = folders.removeLast();
folders.add(fileName + suffix);
return Path.of(folders.removeFirst(), folders.toArray(String[]::new));
}
@RequiredArgsConstructor
private class PathCell implements Cell {
@Getter
private final int x, z;
private final Path path;
private SingleItemStorage storage;
@Override
public OutputStream write() throws IOException {
return storage().write();
}
@Override
public CompressedInputStream read() throws IOException {
return storage().read();
}
@Override
public void delete() throws IOException {
storage().delete();
}
@Override
public boolean exists() throws IOException {
return storage().exists();
}
@Override
public boolean isClosed() {
return PathBasedGridStorage.this.isClosed();
}
private SingleItemStorage storage() {
if (storage == null)
storage = PathBasedGridStorage.this.storage.file(path, compression);
return storage;
}
}
}

View File

@ -0,0 +1,102 @@
package de.bluecolored.bluemap.core.storage.file;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Path;
import java.util.stream.Stream;
public abstract class PathBasedMapStorage implements MapStorage {
public static final Path SETTINGS_PATH = Path.of("settings.json");
public static final Path TEXTURES_PATH = Path.of("textures.json");
public static final Path RENDER_STATE_PATH = Path.of(".rstate");
public static final Path MARKERS_PATH = Path.of("live", "markers.json");
public static final Path PLAYERS_PATH = Path.of("live", "players.json");
private final GridStorage hiresGridStorage;
private final LoadingCache<Integer, GridStorage> lowresGridStorages;
public PathBasedMapStorage(Compression compression, String hiresSuffix, String lowresSuffix) {
this.hiresGridStorage = new PathBasedGridStorage(
this,
Path.of("tiles", "0"),
hiresSuffix + compression.getFileSuffix(),
compression
);
this.lowresGridStorages = Caffeine.newBuilder().build(lod -> new PathBasedGridStorage(
this,
Path.of("tiles", String.valueOf(lod)),
lowresSuffix,
Compression.NONE
));
}
@Override
public GridStorage hiresTiles() {
return hiresGridStorage;
}
@Override
public GridStorage lowresTiles(int lod) {
return lowresGridStorages.get(lod);
}
public Path getAssetPath(String name) {
String[] parts = MapStorage.escapeAssetName(name)
.split("/");
return Path.of("assets", parts);
}
@Override
public SingleItemStorage asset(String name) {
return file(getAssetPath(name), Compression.NONE);
}
@Override
public SingleItemStorage renderState() {
return file(RENDER_STATE_PATH, Compression.NONE);
}
@Override
public SingleItemStorage settings() {
return file(SETTINGS_PATH, Compression.NONE);
}
@Override
public SingleItemStorage textures() {
return file(TEXTURES_PATH, Compression.NONE);
}
@Override
public SingleItemStorage markers() {
return file(MARKERS_PATH, Compression.NONE);
}
@Override
public SingleItemStorage players() {
return file(PLAYERS_PATH, Compression.NONE);
}
/**
* Returns a {@link SingleItemStorage} for a file with the given path and compression.
* The file does not have to actually exist.
*/
public abstract SingleItemStorage file(Path file, Compression compression);
/**
* Returns a stream with all file-paths of existing files at or below the given path.
* (Including files in potential sub-folders)<br>
* Basically, this method should mimic the functionality of
* {@link java.nio.file.Files#walk(Path, FileVisitOption...)}
*/
public abstract Stream<Path> files(Path path) throws IOException;
}

View File

@ -0,0 +1,149 @@
package de.bluecolored.bluemap.core.storage.sql;
import de.bluecolored.bluemap.core.logger.Logger;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
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.Closeable;
import java.io.IOException;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.sql.SQLRecoverableException;
import java.time.Duration;
import java.util.Map;
import java.util.Properties;
@Getter
@RequiredArgsConstructor
public class Database implements Closeable {
private final DataSource dataSource;
private boolean isClosed = false;
public Database(String url, Map<String, String> properties, int maxPoolSize) {
Properties props = new Properties();
props.putAll(properties);
this.dataSource = createDataSource(new DriverManagerConnectionFactory(url, props), maxPoolSize);
}
public Database(String url, Map<String, String> properties, int maxPoolSize, Driver driver) {
Properties props = new Properties();
props.putAll(properties);
ConnectionFactory connectionFactory = new DriverConnectionFactory(
driver,
url,
props
);
this.dataSource = createDataSource(connectionFactory, maxPoolSize);
}
public void run(ConnectionConsumer action) throws IOException {
run((ConnectionFunction<Void>) action);
}
public <R> R run(ConnectionFunction<R> action) throws IOException {
SQLException sqlException = null;
try {
for (int i = 0; i < 2; i++) {
try (Connection connection = dataSource.getConnection()) {
try {
R result = action.apply(connection);
connection.commit();
return result;
} catch (SQLRecoverableException ex) {
if (sqlException == null) {
sqlException = ex;
} else {
sqlException.addSuppressed(ex);
}
}
}
}
} catch (SQLException ex) {
if (sqlException != null)
ex.addSuppressed(sqlException);
throw new IOException(ex);
} catch (IOException | RuntimeException ex) {
if (sqlException != null)
ex.addSuppressed(sqlException);
throw ex;
}
throw new IOException(sqlException);
}
@Override
public void close() throws IOException {
isClosed = true;
if (dataSource instanceof AutoCloseable) {
try {
((AutoCloseable) dataSource).close();
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
throw new IOException("Failed to close datasource!", ex);
}
}
}
private DataSource createDataSource(ConnectionFactory connectionFactory, int maxPoolSize) {
PoolableConnectionFactory poolableConnectionFactory =
new PoolableConnectionFactory(() -> {
Logger.global.logDebug("Creating new SQL-Connection...");
return connectionFactory.createConnection();
}, null);
poolableConnectionFactory.setPoolStatements(true);
poolableConnectionFactory.setMaxOpenPreparedStatements(20);
poolableConnectionFactory.setDefaultAutoCommit(false);
poolableConnectionFactory.setAutoCommitOnReturn(false);
poolableConnectionFactory.setRollbackOnReturn(true);
poolableConnectionFactory.setFastFailValidation(true);
GenericObjectPoolConfig<PoolableConnection> objectPoolConfig = new GenericObjectPoolConfig<>();
objectPoolConfig.setTestWhileIdle(true);
objectPoolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(10));
objectPoolConfig.setNumTestsPerEvictionRun(3);
objectPoolConfig.setBlockWhenExhausted(true);
objectPoolConfig.setMinIdle(1);
objectPoolConfig.setMaxIdle(Runtime.getRuntime().availableProcessors());
objectPoolConfig.setMaxTotal(maxPoolSize);
objectPoolConfig.setMaxWaitMillis(Duration.ofSeconds(30).toMillis());
ObjectPool<PoolableConnection> connectionPool =
new GenericObjectPool<>(poolableConnectionFactory, objectPoolConfig);
poolableConnectionFactory.setPool(connectionPool);
return new PoolingDataSource<>(connectionPool);
}
@FunctionalInterface
public interface ConnectionConsumer extends ConnectionFunction<Void> {
void accept(java.sql.Connection connection) throws SQLException, IOException;
@Override
default Void apply(java.sql.Connection connection) throws SQLException, IOException {
accept(connection);
return null;
}
}
@FunctionalInterface
public interface ConnectionFunction<R> {
R apply(java.sql.Connection connection) throws SQLException, IOException;
}
}

View File

@ -1,37 +0,0 @@
/*
* 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 de.bluecolored.bluemap.core.storage.sql.dialect.MySQLDialect;
import java.net.MalformedURLException;
public class MySQLStorage extends SQLStorage {
public MySQLStorage(SQLStorageSettings config) throws MalformedURLException, SQLDriverException {
super(MySQLDialect.INSTANCE, config);
}
}

View File

@ -0,0 +1,55 @@
package de.bluecolored.bluemap.core.storage.sql;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.function.IntFunction;
public class PageSpliterator<T> implements Spliterator<T> {
private final IntFunction<T[]> pageSupplier;
private T[] lastBatch;
private int pos;
private int page;
public PageSpliterator(IntFunction<T[]> pageSupplier) {
this.pageSupplier = pageSupplier;
}
@Override
public synchronized boolean tryAdvance(Consumer<? super T> action) {
if (!refill()) return false;
action.accept(lastBatch[pos++]);
return true;
}
@Override
public synchronized Spliterator<T> trySplit() {
if (!refill()) return null;
int from = pos;
pos = lastBatch.length;
return Spliterators.spliterator(
lastBatch, from, pos,
characteristics()
);
}
@Override
public long estimateSize() {
return Long.MAX_VALUE;
}
@Override
public int characteristics() {
return 0;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private synchronized boolean refill() {
if (lastBatch != null && pos < lastBatch.length) return true;
pos = 0;
lastBatch = pageSupplier.apply(page++);
return lastBatch != null && lastBatch.length > 0;
}
}

View File

@ -1,144 +0,0 @@
/*
* 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 de.bluecolored.bluemap.core.storage.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.sql.dialect.Dialect;
import de.bluecolored.bluemap.core.storage.sql.dialect.PostgresDialect;
import de.bluecolored.bluemap.core.util.stream.OnCloseOutputStream;
import java.io.*;
import java.net.MalformedURLException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
public class PostgreSQLStorage extends SQLStorage {
public PostgreSQLStorage(SQLStorageSettings config) throws MalformedURLException, SQLDriverException {
super(PostgresDialect.INSTANCE, config);
}
public PostgreSQLStorage(Dialect dialect, SQLStorageSettings config) throws MalformedURLException, SQLDriverException {
super(dialect, config);
}
@Override
public OutputStream writeMapTile(String mapId, int lod, Vector2i tile) throws IOException {
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new OnCloseOutputStream(new BufferedOutputStream(compression.compress(byteOut)), () -> {
int mapFK = getMapFK(mapId);
int tileCompressionFK = getMapTileCompressionFK(compression);
recoveringConnection(connection -> {
executeUpdate(connection, this.dialect.writeMapTile(),
mapFK,
lod,
tile.getX(),
tile.getY(),
tileCompressionFK,
byteOut.toByteArray()
);
}, 2);
});
}
@Override
public OutputStream writeMeta(String mapId, String name) {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new OnCloseOutputStream(byteOut, () -> {
int mapFK = getMapFK(mapId);
recoveringConnection(connection -> {
executeUpdate(connection, this.dialect.writeMeta(),
mapFK,
name,
byteOut.toByteArray()
);
}, 2);
});
}
@Override
public Optional<CompressedInputStream> readMapTile(String mapId, int lod, Vector2i tile) throws IOException {
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
try {
byte[] data = recoveringConnection(connection -> {
ResultSet result = executeQuery(connection, this.dialect.readMapTile(),
mapId,
lod,
tile.getX(),
tile.getY(),
compression.getTypeId()
);
if (result.next()) {
return result.getBytes(1);
} else {
return null;
}
}, 2);
if (data == null) {
return Optional.empty();
}
InputStream inputStream = new ByteArrayInputStream(data);
return Optional.of(new CompressedInputStream(inputStream, compression));
} catch (SQLException ex) {
throw new IOException(ex);
}
}
@Override
public Optional<InputStream> readMeta(String mapId, String name) throws IOException {
try {
byte[] data = recoveringConnection(connection -> {
ResultSet result = executeQuery(connection, this.dialect.readMeta(),
mapId,
escapeMetaName(name)
);
if (result.next()) {
return result.getBytes(1);
} else {
return null;
}
}, 2);
if (data == null) {
return Optional.empty();
}
InputStream inputStream = new ByteArrayInputStream(data);
return Optional.of(inputStream);
} catch (SQLException ex) {
throw new IOException(ex);
}
}
}

View File

@ -1,45 +0,0 @@
/*
* 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;
public class SQLDriverException extends Exception {
public SQLDriverException() {
super();
}
public SQLDriverException(String message) {
super(message);
}
public SQLDriverException(Throwable cause) {
super(cause);
}
public SQLDriverException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,140 @@
package de.bluecolored.bluemap.core.storage.sql;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import java.io.IOException;
import java.util.function.DoublePredicate;
public class SQLMapStorage implements MapStorage {
public static final String SETTINGS_META_NAME = "settings.json";
public static final String TEXTURES_META_NAME = "textures.json";
public static final String RENDER_STATE_META_NAME = ".rstate";
public static final String MARKERS_META_NAME = "live/markers.json";
public static final String PLAYERS_META_NAME = "live/players.json";
private final String mapId;
private final CommandSet sql;
private final SQLTileStorage hiresTileStorage;
private final LoadingCache<Integer, GridStorage> lowresGridStorages;
private final SingleItemStorage renderStateStorage;
private final SingleItemStorage settingsStorage;
private final SingleItemStorage texturesStorage;
private final SingleItemStorage markersStorage;
private final SingleItemStorage playersStorage;
public SQLMapStorage(String mapId, CommandSet sql, Compression compression) {
this.mapId = mapId;
this.sql = sql;
this.hiresTileStorage = new SQLTileStorage(
sql,
mapId,
0,
compression
);
this.lowresGridStorages = Caffeine.newBuilder().build(lod -> new SQLTileStorage(
sql,
mapId,
lod,
Compression.NONE
));
renderStateStorage = meta(RENDER_STATE_META_NAME, Compression.NONE);
settingsStorage = meta(SETTINGS_META_NAME, Compression.NONE);
texturesStorage = meta(TEXTURES_META_NAME, Compression.NONE);
markersStorage = meta(MARKERS_META_NAME, Compression.NONE);
playersStorage = meta(PLAYERS_META_NAME, Compression.NONE);
}
@Override
public GridStorage hiresTiles() {
return hiresTileStorage;
}
@Override
public GridStorage lowresTiles(int lod) {
return lowresGridStorages.get(lod);
}
public String getAssetMetaName(String assetName) {
return "assets/" + MapStorage.escapeAssetName(assetName);
}
@Override
public SingleItemStorage asset(String name) {
return meta(getAssetMetaName(name), Compression.NONE);
}
@Override
public SingleItemStorage renderState() {
return renderStateStorage;
}
@Override
public SingleItemStorage settings() {
return settingsStorage;
}
@Override
public SingleItemStorage textures() {
return texturesStorage;
}
@Override
public SingleItemStorage markers() {
return markersStorage;
}
@Override
public SingleItemStorage players() {
return playersStorage;
}
@Override
public void delete(DoublePredicate onProgress) throws IOException {
// delete tiles in 1000er steps to track progress
int tileCount = sql.countAllMapTiles(mapId);
if (tileCount > 0) {
int totalDeleted = 0;
int deleted = 0;
do {
deleted = sql.purgeMapTiles(mapId, 1000);
totalDeleted += deleted;
if (onProgress.test((double) totalDeleted / tileCount))
return;
} while (deleted > 0 && totalDeleted < tileCount);
}
// finally purge the map
sql.purgeMap(mapId);
}
@Override
public boolean exists() throws IOException {
return sql.hasMap(mapId);
}
private SingleItemStorage meta(String name, Compression compression) {
return new SQLMetaItemStorage(sql, mapId, name, compression);
}
@Override
public boolean isClosed() {
return sql.isClosed();
}
}

View File

@ -0,0 +1,51 @@
package de.bluecolored.bluemap.core.storage.sql;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import de.bluecolored.bluemap.core.util.stream.OnCloseOutputStream;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.io.*;
@RequiredArgsConstructor
public class SQLMetaItemStorage implements SingleItemStorage {
private final CommandSet sql;
private final String mapId;
private final String itemName;
private final Compression compression;
@Override
public OutputStream write() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
return new OnCloseOutputStream(compression.compress(bytes),
() -> sql.writeMapMeta(mapId, itemName, bytes.toByteArray())
);
}
@Override
public @Nullable CompressedInputStream read() throws IOException {
byte[] data = sql.readMapMeta(mapId, itemName);
if (data == null) return null;
return new CompressedInputStream(new ByteArrayInputStream(data), compression);
}
@Override
public void delete() throws IOException {
sql.deleteMapMeta(mapId, itemName);
}
@Override
public boolean exists() throws IOException {
return sql.hasMapMeta(mapId, itemName);
}
@Override
public boolean isClosed() {
return sql.isClosed();
}
}

View File

@ -1,703 +1,59 @@
/*
* 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.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.storage.*;
import de.bluecolored.bluemap.core.storage.sql.dialect.DialectType;
import de.bluecolored.bluemap.core.storage.sql.dialect.Dialect;
import de.bluecolored.bluemap.core.util.stream.OnCloseOutputStream;
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 org.intellij.lang.annotations.Language;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import lombok.RequiredArgsConstructor;
import javax.sql.DataSource;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.*;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletionException;
import java.util.function.Function;
import java.io.IOException;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public abstract class SQLStorage extends Storage {
@RequiredArgsConstructor
public class SQLStorage implements Storage {
private final DataSource dataSource;
protected final Dialect dialect;
protected final Compression hiresCompression;
private final LoadingCache<String, Integer> mapFKs = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.build(this::loadMapFK);
private final LoadingCache<Compression, Integer> mapTileCompressionFKs = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.build(this::loadMapTileCompressionFK);
private volatile boolean closed;
public SQLStorage(Dialect dialect, SQLStorageSettings config) throws MalformedURLException, SQLDriverException {
this.dialect = dialect;
this.closed = false;
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 SQLDriverException("Failed to create an instance of the driver-class", 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.getConnectionUrl(), config.getConnectionProperties(), config.getMaxConnections(), driver);
} else {
Class.forName(config.getDriverClass().get());
this.dataSource = createDataSource(config.getConnectionUrl(), config.getConnectionProperties(), config.getMaxConnections());
}
} else {
this.dataSource = createDataSource(config.getConnectionUrl(), config.getConnectionProperties(), config.getMaxConnections());
}
} catch (ClassNotFoundException ex) {
throw new SQLDriverException("The driver-class does not exist.", ex);
//throw new ConfigurationException("The path to your driver-jar is invalid. Check your sql-storage-config!", ex);
//throw new ConfigurationException("The driver-class does not exist. Check your sql-storage-config!", ex);
}
this.hiresCompression = config.getCompression();
}
private final CommandSet sql;
private final Compression compression;
private final LoadingCache<String, SQLMapStorage> mapStorages = Caffeine.newBuilder()
.build(this::create);
@Override
public OutputStream writeMapTile(String mapId, int lod, Vector2i tile) throws IOException {
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new OnCloseOutputStream(new BufferedOutputStream(compression.compress(byteOut)), () -> {
int mapFK = getMapFK(mapId);
int tileCompressionFK = getMapTileCompressionFK(compression);
recoveringConnection(connection -> {
Blob dataBlob = connection.createBlob();
try {
try (OutputStream blobOut = dataBlob.setBinaryStream(1)) {
byteOut.writeTo(blobOut);
}
executeUpdate(connection, this.dialect.writeMapTile(),
mapFK,
lod,
tile.getX(),
tile.getY(),
tileCompressionFK,
dataBlob
);
} finally {
dataBlob.free();
}
}, 2);
});
}
@Override
public Optional<CompressedInputStream> readMapTile(String mapId, int lod, Vector2i tile) throws IOException {
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
try {
byte[] data = recoveringConnection(connection -> {
ResultSet result = executeQuery(connection,
this.dialect.readMapTile(),
mapId,
lod,
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<TileInfo> readMapTileInfo(final String mapId, int lod, final Vector2i tile) throws IOException {
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
try {
TileInfo tileInfo = recoveringConnection(connection -> {
ResultSet result = executeQuery(connection,
this.dialect.readMapTileInfo(),
mapId,
lod,
tile.getX(),
tile.getY(),
compression.getTypeId()
);
if (result.next()) {
final long lastModified = result.getTimestamp("changed").getTime();
final long size = result.getLong("size");
return new TileInfo() {
@Override
public CompressedInputStream readMapTile() throws IOException {
return SQLStorage.this.readMapTile(mapId, lod, 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(tileInfo);
} catch (SQLException | NoSuchElementException ex) {
throw new IOException(ex);
}
}
@Override
public void deleteMapTile(String mapId, int lod, Vector2i tile) throws IOException {
try {
recoveringConnection(connection ->
executeUpdate(connection,this.dialect.deleteMapTile(),
mapId,
lod,
tile.getX(),
tile.getY()
), 2);
} catch (SQLException ex) {
throw new IOException(ex);
}
}
@Override
public OutputStream writeMeta(String mapId, String name) {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new OnCloseOutputStream(byteOut, () -> {
int mapFK = getMapFK(mapId);
recoveringConnection(connection -> {
Blob dataBlob = connection.createBlob();
try {
try (OutputStream blobOut = dataBlob.setBinaryStream(1)) {
byteOut.writeTo(blobOut);
}
executeUpdate(connection,
this.dialect.writeMeta(),
mapFK,
escapeMetaName(name),
dataBlob
);
} finally {
dataBlob.free();
}
}, 2);
});
}
@Override
public Optional<InputStream> readMeta(String mapId, String name) throws IOException {
try {
byte[] data = recoveringConnection(connection -> {
ResultSet result = executeQuery(connection,
this.dialect.readMeta(),
mapId,
escapeMetaName(name)
);
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 Optional<MetaInfo> readMetaInfo(String mapId, String name) throws IOException {
try {
MetaInfo tileInfo = recoveringConnection(connection -> {
ResultSet result = executeQuery(connection,
this.dialect.readMetaSize(),
mapId,
escapeMetaName(name)
);
if (result.next()) {
final long size = result.getLong("size");
return new MetaInfo() {
@Override
public InputStream readMeta() throws IOException {
return SQLStorage.this.readMeta(mapId, name)
.orElseThrow(() -> new IOException("Tile no longer present!"));
}
@Override
public long getSize() {
return size;
}
};
} else {
return null;
}
}, 2);
return Optional.ofNullable(tileInfo);
} catch (SQLException | NoSuchElementException ex) {
throw new IOException(ex);
}
}
@Override
public void deleteMeta(String mapId, String name) throws IOException {
try {
recoveringConnection(connection ->
executeUpdate(connection,
this.dialect.deleteMeta(),
mapId,
escapeMetaName(name)
), 2);
} catch (SQLException ex) {
throw new IOException(ex);
}
}
@Override
public void purgeMap(String mapId, Function<ProgressInfo, Boolean> onProgress) throws IOException {
synchronized (mapFKs) {
try {
recoveringConnection(connection -> {
executeUpdate(connection,
this.dialect.purgeMapTile(),
mapId
);
executeUpdate(connection,
this.dialect.purgeMapMeta(),
mapId
);
executeUpdate(connection,
this.dialect.purgeMap(),
mapId
);
}, 2);
mapFKs.invalidate(mapId);
} catch (SQLException ex) {
throw new IOException(ex);
}
}
}
@Override
public Collection<String> collectMapIds() throws IOException {
try {
return recoveringConnection(connection -> {
ResultSet result = executeQuery(connection,
this.dialect.selectMapIds()
);
Collection<String> mapIds = new ArrayList<>();
while (result.next()) {
mapIds.add(result.getString("map_id"));
}
return mapIds;
}, 2);
} catch (SQLException ex) {
throw new IOException(ex);
}
}
@SuppressWarnings("UnusedAssignment")
public void initialize() throws IOException {
try {
sql.initializeTables();
}
// initialize and get schema-version
String schemaVersionString = recoveringConnection(connection -> {
connection.createStatement().executeUpdate(
this.dialect.initializeStorageMeta());
private SQLMapStorage create(String mapId) {
return new SQLMapStorage(mapId, sql, compression);
}
ResultSet result = executeQuery(connection,
this.dialect.selectStorageMeta(),
"schema_version"
);
@Override
public MapStorage map(String mapId) {
return mapStorages.get(mapId);
}
if (result.next()) {
return result.getString("value");
} else {
executeUpdate(connection,
this.dialect.insertStorageMeta(),
"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 > 3)
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(
this.dialect.initializeMap()
);
connection.createStatement().executeUpdate(
this.dialect.initializeMapTileCompression()
);
connection.createStatement().executeUpdate(
this.dialect.initializeMapMeta());
connection.createStatement().executeUpdate(
this.dialect.initializeMapTile()
);
executeUpdate(connection,
this.dialect.updateStorageMeta(),
"3", "schema_version"
);
}, 2);
schemaVersion = 3;
}
if (schemaVersion == 1)
throw new IOException("Outdated database schema: " + schemaVersion +
" (Cannot automatically update, reset your database and reload bluemap to fix this)");
if (schemaVersion == 2) {
Logger.global.logInfo("Updating database schema: Renaming bluemap_map_meta keys to new format...");
recoveringConnection(connection -> {
// delete potential files that are already in the new format to avoid constraint-issues
executeUpdate(connection,
this.dialect.deleteMapMeta(),
"settings.json", "textures.json", ".rstate"
);
// rename files
executeUpdate(connection,
this.dialect.updateMapMeta(),
"settings.json", "settings"
);
executeUpdate(connection,
this.dialect.updateMapMeta(),
"textures.json", "textures"
);
executeUpdate(connection,
this.dialect.updateMapMeta(),
".rstate", "render_state"
);
// update schemaVersion
executeUpdate(connection,
this.dialect.updateStorageMeta(),
"3", "schema_version"
);
}, 2);
schemaVersion = 3;
}
} catch (SQLException ex) {
throw new IOException(ex);
}
@Override
public Stream<String> mapIds() {
return StreamSupport.stream(
new PageSpliterator<>(page -> {
try {
return sql.listMapIds(page * 1000, 1000);
} catch (IOException ex) { throw new RuntimeException(ex); }
}),
false
);
}
@Override
public boolean isClosed() {
return closed;
return sql.isClosed();
}
@Override
public void close() throws IOException {
this.closed = true;
if (dataSource instanceof AutoCloseable) {
try {
((AutoCloseable) dataSource).close();
} catch (Exception ex) {
throw new IOException("Failed to close datasource!", ex);
}
}
}
protected ResultSet executeQuery(Connection connection, @Language("sql") String sql, Object... parameters) throws SQLException {
return prepareStatement(connection, sql, parameters).executeQuery();
}
@SuppressWarnings("UnusedReturnValue")
protected int executeUpdate(Connection connection, @Language("sql") String sql, Object... parameters) throws SQLException {
return prepareStatement(connection, sql, parameters).executeUpdate();
}
private PreparedStatement prepareStatement(Connection connection, @Language("sql") 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;
}
@SuppressWarnings("SameParameterValue")
protected void recoveringConnection(ConnectionConsumer action, int tries) throws SQLException, IOException {
recoveringConnection((ConnectionFunction<Void>) action, tries);
}
@SuppressWarnings("SameParameterValue")
protected <R> R recoveringConnection(ConnectionFunction<R> action, int tries) throws SQLException, IOException {
SQLException sqlException = null;
try {
for (int i = 0; i < tries; i++) {
try (Connection connection = dataSource.getConnection()) {
try {
R result = action.apply(connection);
connection.commit();
return result;
} catch (SQLRecoverableException ex) {
if (sqlException == null) {
sqlException = ex;
} else {
sqlException.addSuppressed(ex);
}
}
}
}
} 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;
}
protected 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;
}
}
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 {
synchronized (mapFKs) {
return lookupFK("bluemap_map", "id", "map_id", mapId);
}
}
private int loadMapTileCompressionFK(Compression compression) throws SQLException, IOException {
return lookupFK("bluemap_map_tile_compression", "id", "compression", compression.getTypeId());
}
@SuppressWarnings({"SameParameterValue", "SqlResolve"})
private int lookupFK(String table, String idField, String valueField, String value) throws SQLException, IOException {
return recoveringConnection(connection -> {
int key;
ResultSet result = executeQuery(connection,
this.dialect.lookupFK(table,idField,valueField),
value
);
if (result.next()) {
key = result.getInt("id");
} else {
PreparedStatement statement = connection.prepareStatement(
this.dialect.insertFK(table,valueField),
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, Map<String, String> properties, int maxPoolSize) {
Properties props = new Properties();
props.putAll(properties);
return createDataSource(new DriverManagerConnectionFactory(dbUrl, props), maxPoolSize);
}
private DataSource createDataSource(String dbUrl, Map<String, String> properties, int maxPoolSize, Driver driver) {
Properties props = new Properties();
props.putAll(properties);
ConnectionFactory connectionFactory = new DriverConnectionFactory(
driver,
dbUrl,
props
);
return createDataSource(connectionFactory, maxPoolSize);
}
private DataSource createDataSource(ConnectionFactory connectionFactory, int maxPoolSize) {
PoolableConnectionFactory poolableConnectionFactory =
new PoolableConnectionFactory(() -> {
Logger.global.logDebug("Creating new SQL-Connection...");
return connectionFactory.createConnection();
}, null);
poolableConnectionFactory.setPoolStatements(true);
poolableConnectionFactory.setMaxOpenPreparedStatements(20);
poolableConnectionFactory.setDefaultAutoCommit(false);
poolableConnectionFactory.setAutoCommitOnReturn(false);
poolableConnectionFactory.setRollbackOnReturn(true);
poolableConnectionFactory.setFastFailValidation(true);
GenericObjectPoolConfig<PoolableConnection> objectPoolConfig = new GenericObjectPoolConfig<>();
objectPoolConfig.setTestWhileIdle(true);
objectPoolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(10));
objectPoolConfig.setNumTestsPerEvictionRun(3);
objectPoolConfig.setBlockWhenExhausted(true);
objectPoolConfig.setMinIdle(1);
objectPoolConfig.setMaxIdle(Runtime.getRuntime().availableProcessors());
objectPoolConfig.setMaxTotal(maxPoolSize);
objectPoolConfig.setMaxWaitMillis(Duration.ofSeconds(30).toMillis());
ObjectPool<PoolableConnection> connectionPool =
new GenericObjectPool<>(poolableConnectionFactory, objectPoolConfig);
poolableConnectionFactory.setPool(connectionPool);
return new PoolingDataSource<>(connectionPool);
}
public static SQLStorage create(SQLStorageSettings settings) throws Exception {
String dbUrl = settings.getConnectionUrl();
String provider = dbUrl.strip().split(":", 3)[1];
return DialectType.getStorage(provider,settings);
}
@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;
sql.close();
}
}

View File

@ -1,48 +0,0 @@
/*
* 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 de.bluecolored.bluemap.core.storage.Compression;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.Optional;
public interface SQLStorageSettings {
Optional<URL> getDriverJar() throws MalformedURLException;
Optional<String> getDriverClass();
String getConnectionUrl();
Map<String, String> getConnectionProperties();
int getMaxConnections();
Compression getCompression();
}

View File

@ -0,0 +1,65 @@
package de.bluecolored.bluemap.core.storage.sql;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import de.bluecolored.bluemap.core.util.stream.OnCloseOutputStream;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@RequiredArgsConstructor
public class SQLTileStorage implements GridStorage {
private final CommandSet sql;
private final String mapId;
private final int lod;
private final Compression compression;
@Override
public OutputStream write(int x, int z) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
return new OnCloseOutputStream(compression.compress(bytes),
() -> sql.writeMapTile(mapId, lod, x, z, compression, bytes.toByteArray())
);
}
@Override
public @Nullable CompressedInputStream read(int x, int z) throws IOException {
byte[] data = sql.readMapTile(mapId, lod, x, z, compression);
if (data == null) return null;
return new CompressedInputStream(new ByteArrayInputStream(data), compression);
}
@Override
public void delete(int x, int z) throws IOException {
sql.deleteMapTile(mapId, lod, x, z, compression);
}
@Override
public boolean exists(int x, int z) throws IOException {
return sql.hasMapTile(mapId, lod, x, z, compression);
}
@Override
public Stream<Cell> stream() throws IOException {
return StreamSupport.stream(
new PageSpliterator<>(page -> {
try {
return sql.listMapTiles(mapId, lod, compression, page * 1000, 1000);
} catch (IOException ex) { throw new RuntimeException(ex); }
}),
false
).map(tilePosition -> new GridStorageCell(this, tilePosition.x(), tilePosition.z()));
}
@Override
public boolean isClosed() {
return sql.isClosed();
}
}

View File

@ -1,37 +0,0 @@
/*
* 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 de.bluecolored.bluemap.core.storage.sql.dialect.SqliteDialect;
import java.net.MalformedURLException;
public class SQLiteStorage extends PostgreSQLStorage {
public SQLiteStorage(SQLStorageSettings config) throws MalformedURLException, SQLDriverException {
super(SqliteDialect.INSTANCE, config);
}
}

View File

@ -0,0 +1,383 @@
package de.bluecolored.bluemap.core.storage.sql.commandset;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.sql.Database;
import lombok.RequiredArgsConstructor;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@SuppressWarnings("SqlSourceToSinkFlow")
@RequiredArgsConstructor
public abstract class AbstractCommandSet implements CommandSet {
private final Database db;
final LoadingCache<String, Integer> mapKeys = Caffeine.newBuilder()
.build(this::mapKey);
final LoadingCache<Compression, Integer> compressionKeys = Caffeine.newBuilder()
.build(this::compressionKey);
@Language("sql")
public abstract String createMapTableStatement();
@Language("sql")
public abstract String createCompressionTableStatement();
@Language("sql")
public abstract String createMapMetaTableStatement();
@Language("sql")
public abstract String createMapTileTableStatement();
@Language("sql")
public abstract String fixLegacyCompressionIdsStatement();
public void initializeTables() throws IOException {
db.run(connection -> {
executeUpdate(connection, createMapTableStatement());
executeUpdate(connection, createCompressionTableStatement());
executeUpdate(connection, createMapMetaTableStatement());
executeUpdate(connection, createMapTileTableStatement());
});
db.run(connection -> executeUpdate(connection, fixLegacyCompressionIdsStatement()));
}
@Language("sql")
public abstract String writeMapTileStatement();
@Override
public int writeMapTile(
String mapId, int lod, int x, int z, Compression compression,
byte[] bytes
) throws IOException {
int mapKey = Objects.requireNonNull(mapKeys.get(mapId));
int compressionKey = Objects.requireNonNull(compressionKeys.get(compression));
return db.run(connection -> {
return executeUpdate(connection,
writeMapTileStatement(),
mapKey, lod, x, z, compressionKey,
bytes
);
});
}
@Language("sql")
public abstract String readMapTileStatement();
@Override
public byte @Nullable [] readMapTile(String mapId, int lod, int x, int z, Compression compression) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
readMapTileStatement(),
mapId, lod, x, z, compression.getKey().getFormatted()
);
if (!result.next()) return null;
return result.getBytes(1);
});
}
@Language("sql")
public abstract String deleteMapTileStatement();
@Override
public int deleteMapTile(String mapId, int lod, int x, int z, Compression compression) throws IOException {
return db.run(connection -> {
return executeUpdate(connection,
deleteMapTileStatement(),
mapId, lod, x, z, compression.getKey().getFormatted()
);
});
}
@Language("sql")
public abstract String hasMapTileStatement();
@Override
public boolean hasMapTile(String mapId, int lod, int x, int z, Compression compression) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
hasMapTileStatement(),
mapId, lod, x, z, compression.getKey().getFormatted()
);
if (!result.next()) throw new IllegalStateException("Counting query returned empty result!");
return result.getBoolean(1);
});
}
@Language("sql")
public abstract String countAllMapTilesStatement();
@Override
public int countAllMapTiles(String mapId) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
countAllMapTilesStatement(),
mapId
);
if (!result.next()) throw new IllegalStateException("Counting query returned empty result!");
return result.getInt(1);
});
}
@Language("sql")
public abstract String purgeMapTilesStatement();
@Override
public int purgeMapTiles(String mapId, int limit) throws IOException {
return db.run(connection -> {
return executeUpdate(connection,
purgeMapTilesStatement(),
mapId, limit
);
});
}
@Language("sql")
public abstract String listMapTilesStatement();
@Override
public TilePosition[] listMapTiles(String mapId, int lod, Compression compression, int start, int count) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
listMapTilesStatement(),
mapId, lod, compression.getKey().getFormatted(),
count, start
);
TilePosition[] tiles = new TilePosition[count];
int i = 0;
while (result.next()) {
tiles[i++] = new TilePosition(
result.getInt(1),
result.getInt(2)
);
}
if (i < count) {
TilePosition[] trimmed = new TilePosition[i];
System.arraycopy(tiles, 0, trimmed, 0, i);
tiles = trimmed;
}
return tiles;
});
}
@Language("sql")
public abstract String writeMapMetaStatement();
@Override
public int writeMapMeta(String mapId, String itemName, byte[] bytes) throws IOException {
int mapKey = Objects.requireNonNull(mapKeys.get(mapId));
return db.run(connection -> {
return executeUpdate(connection,
writeMapMetaStatement(),
mapKey, itemName,
bytes
);
});
}
@Language("sql")
public abstract String readMapMetaStatement();
@Override
public byte @Nullable [] readMapMeta(String mapId, String itemName) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
readMapMetaStatement(),
mapId, itemName
);
if (!result.next()) return null;
return result.getBytes(1);
});
}
@Language("sql")
public abstract String deleteMapMetaStatement();
@Override
public int deleteMapMeta(String mapId, String itemName) throws IOException {
return db.run(connection -> {
return executeUpdate(connection,
deleteMapMetaStatement(),
mapId, itemName
);
});
}
@Language("sql")
public abstract String hasMapMetaStatement();
@Override
public boolean hasMapMeta(String mapId, String itemName) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
hasMapMetaStatement(),
mapId, itemName
);
if (!result.next()) throw new IllegalStateException("Counting query returned empty result!");
return result.getBoolean(1);
});
}
@Language("sql")
public abstract String purgeMapTileTableStatement();
@Language("sql")
public abstract String purgeMapMetaTableStatement();
@Language("sql")
public abstract String deleteMapStatement();
@Override
public void purgeMap(String mapId) throws IOException {
db.run(connection -> {
executeUpdate(connection,
purgeMapTileTableStatement(),
mapId
);
executeUpdate(connection,
purgeMapMetaTableStatement(),
mapId
);
executeUpdate(connection,
deleteMapStatement(),
mapId
);
});
}
@Language("sql")
public abstract String hasMapStatement();
@Override
public boolean hasMap(String mapId) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
hasMapStatement(),
mapId
);
if (!result.next()) throw new IllegalStateException("Counting query returned empty result!");
return result.getBoolean(1);
});
}
@Language("sql")
public abstract String listMapIdsStatement();
@Override
public String[] listMapIds(int start, int count) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
listMapIdsStatement(),
count, start
);
List<String> mapIds = new ArrayList<>();
while (result.next()) {
mapIds.add(result.getString(1));
}
return mapIds.toArray(String[]::new);
});
}
@Language("sql")
public abstract String findMapKeyStatement();
@Language("sql")
public abstract String createMapKeyStatement();
public int mapKey(String mapId) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
findMapKeyStatement(),
mapId
);
if (result.next())
return result.getInt(1);
PreparedStatement statement = connection.prepareStatement(
createMapKeyStatement(),
Statement.RETURN_GENERATED_KEYS
);
statement.setString(1, mapId);
statement.executeUpdate();
ResultSet keys = statement.getGeneratedKeys();
if (!keys.next()) throw new IllegalStateException("No generated key returned!");
return keys.getInt(1);
});
}
@Language("sql")
public abstract String findCompressionKeyStatement();
@Language("sql")
public abstract String createCompressionKeyStatement();
public int compressionKey(Compression compression) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
findCompressionKeyStatement(),
compression.getKey().getFormatted()
);
if (result.next())
return result.getInt(1);
PreparedStatement statement = connection.prepareStatement(
createCompressionKeyStatement(),
Statement.RETURN_GENERATED_KEYS
);
statement.setString(1, compression.getKey().getFormatted());
statement.executeUpdate();
ResultSet keys = statement.getGeneratedKeys();
if (!keys.next()) throw new IllegalStateException("No generated key returned!");
return keys.getInt(1);
});
}
@Override
public boolean isClosed() {
return db.isClosed();
}
@Override
public void close() throws IOException {
db.close();
}
protected static ResultSet executeQuery(Connection connection, @Language("sql") String sql, Object... parameters) throws SQLException {
return prepareStatement(connection, sql, parameters).executeQuery();
}
@SuppressWarnings("UnusedReturnValue")
protected static int executeUpdate(Connection connection, @Language("sql") String sql, Object... parameters) throws SQLException {
return prepareStatement(connection, sql, parameters).executeUpdate();
}
private static PreparedStatement prepareStatement(Connection connection, @Language("sql") 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;
}
}

View File

@ -0,0 +1,57 @@
package de.bluecolored.bluemap.core.storage.sql.commandset;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.io.IOException;
public interface CommandSet extends Closeable {
void initializeTables() throws IOException;
int writeMapTile(
String mapId, int lod, int x, int z, Compression compression,
byte[] bytes
) throws IOException;
byte @Nullable [] readMapTile(
String mapId, int lod, int x, int z, Compression compression
) throws IOException;
int deleteMapTile(
String mapId, int lod, int x, int z, Compression compression
) throws IOException;
boolean hasMapTile(
String mapId, int lod, int x, int z, Compression compression
) throws IOException;
TilePosition[] listMapTiles(
String mapId, int lod, Compression compression,
int start, int count
) throws IOException;
int countAllMapTiles(String mapId) throws IOException;
int purgeMapTiles(String mapId, int limit) throws IOException;
int writeMapMeta(String mapId, String itemName, byte[] bytes) throws IOException;
byte @Nullable [] readMapMeta(String mapId, String itemName) throws IOException;
int deleteMapMeta(String mapId, String itemName) throws IOException;
boolean hasMapMeta(String mapId, String itemName) throws IOException;
void purgeMap(String mapId) throws IOException;
boolean hasMap(String mapId) throws IOException;
String[] listMapIds(int start, int count) throws IOException;
boolean isClosed();
record TilePosition (int x, int z) {}
}

View File

@ -0,0 +1,353 @@
package de.bluecolored.bluemap.core.storage.sql.commandset;
import de.bluecolored.bluemap.core.storage.sql.Database;
import org.intellij.lang.annotations.Language;
public class MySQLCommandSet extends AbstractCommandSet {
public MySQLCommandSet(Database db) {
super(db);
}
@Override
@Language("mysql")
public String createMapTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map` (
`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
`map_id` VARCHAR(190) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `map_id` (`map_id`)
) COLLATE 'utf8mb4_bin'
""";
}
@Override
@Language("mysql")
public String createCompressionTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_tile_compression` (
`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
`compression` VARCHAR(190) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `compression` (`compression`)
) COLLATE 'utf8mb4_bin'
""";
}
@Override
@Language("mysql")
public String createMapMetaTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_meta` (
`map` SMALLINT UNSIGNED NOT NULL,
`key` varchar(190) 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 CASCADE
) COLLATE 'utf8mb4_bin'
""";
}
@Override
@Language("mysql")
public String createMapTileTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_tile` (
`map` SMALLINT UNSIGNED NOT NULL,
`lod` SMALLINT UNSIGNED NOT NULL,
`x` INT NOT NULL,
`z` INT NOT NULL,
`compression` SMALLINT UNSIGNED NOT NULL,
`data` LONGBLOB NOT NULL,
PRIMARY KEY (`map`, `lod`, `x`, `z`),
CONSTRAINT `fk_bluemap_map_tile_map`
FOREIGN KEY (`map`)
REFERENCES `bluemap_map` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_map_tile_compression`
FOREIGN KEY (`compression`)
REFERENCES `bluemap_map_tile_compression` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE
) COLLATE 'utf8mb4_bin'
""";
}
@Override
@Language("mysql")
public String fixLegacyCompressionIdsStatement() {
return """
UPDATE IGNORE `bluemap_map_tile_compression`
SET `compression` = CONCAT('bluemap:', `compression`)
WHERE NOT `compression` LIKE '%:%'
""";
}
@Override
@Language("mysql")
public String writeMapTileStatement() {
return """
REPLACE
INTO `bluemap_map_tile` (`map`, `lod`, `x`, `z`, `compression`, `data`)
VALUES (?, ?, ?, ?, ?, ?)
""";
}
@Override
@Language("mysql")
public String readMapTileStatement() {
return """
SELECT t.`data`
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND t.`x` = ?
AND t.`z` = ?
AND c.`compression` = ?
""";
}
@Override
@Language("mysql")
public String deleteMapTileStatement() {
return """
DELETE
FROM `bluemap_map_tile`
WHERE `map` IN (
SELECT m.`id`
FROM `bluemap_map` m
WHERE m.`map_id` = ?
)
AND `lod` = ?
AND `x` = ?
AND `z` = ?
AND `compression` IN (
SELECT c.`id`
FROM `bluemap_map_tile_compression` c
WHERE c.`compression` = ?
)
""";
}
@Override
@Language("mysql")
public String hasMapTileStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND t.`x` = ?
AND t.`z` = ?
AND c.`compression` = ?
""";
}
@Override
@Language("mysql")
public String countAllMapTilesStatement() {
return """
SELECT COUNT(*)
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
""";
}
@Override
@Language("mysql")
public String purgeMapTilesStatement() {
return """
DELETE
FROM `bluemap_map_tile`
WHERE `map` IN (
SELECT `id`
FROM `bluemap_map`
WHERE `map_id` = ?
)
LIMIT ?
""";
}
@Override
@Language("mysql")
public String listMapTilesStatement() {
return """
SELECT t.`x`, t.`z`
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND c.`compression` = ?
LIMIT ? OFFSET ?
""";
}
@Override
@Language("mysql")
public String writeMapMetaStatement() {
return """
REPLACE
INTO `bluemap_map_meta` (`map`, `key`, `value`)
VALUES (?, ?, ?)
""";
}
@Override
@Language("mysql")
public String readMapMetaStatement() {
return """
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` = ?
""";
}
@Override
@Language("mysql")
public String deleteMapMetaStatement() {
return """
DELETE
FROM `bluemap_map_meta`
WHERE `map` IN (
SELECT `id`
FROM `bluemap_map`
WHERE `map_id` = ?
)
AND `key` = ?
""";
}
@Override
@Language("mysql")
public String hasMapMetaStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map_meta` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
AND t.`key` = ?
""";
}
@Override
@Language("mysql")
public String purgeMapTileTableStatement() {
return """
DELETE
FROM `bluemap_map_tile`
WHERE `map` IN (
SELECT m.`id`
FROM `bluemap_map` m
WHERE m.`map_id` = ?
)
""";
}
@Override
@Language("mysql")
public String purgeMapMetaTableStatement() {
return """
DELETE
FROM `bluemap_map_meta`
WHERE `map` IN (
SELECT m.`id`
FROM `bluemap_map` m
WHERE m.`map_id` = ?
)
""";
}
@Override
@Language("mysql")
public String deleteMapStatement() {
return """
DELETE
FROM `bluemap_map`
WHERE `map_id` = ?
""";
}
@Override
@Language("mysql")
public String hasMapStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map` m
WHERE m.`map_id` = ?
""";
}
@Override
@Language("mysql")
public String listMapIdsStatement() {
return """
SELECT `map_id`
FROM `bluemap_map` m
LIMIT ? OFFSET ?
""";
}
@Override
@Language("mysql")
public String findMapKeyStatement() {
return """
SELECT `id`
FROM `bluemap_map`
WHERE map_id = ?
""";
}
@Override
@Language("mysql")
public String createMapKeyStatement() {
return """
INSERT
INTO `bluemap_map` (`map_id`)
VALUES (?)
""";
}
@Override
@Language("mysql")
public String findCompressionKeyStatement() {
return """
SELECT `id`
FROM `bluemap_map_tile_compression`
WHERE `compression` = ?
""";
}
@Override
@Language("mysql")
public String createCompressionKeyStatement() {
return """
INSERT
INTO `bluemap_map_tile_compression` (`compression`)
VALUES (?)
""";
}
}

View File

@ -0,0 +1,352 @@
package de.bluecolored.bluemap.core.storage.sql.commandset;
import de.bluecolored.bluemap.core.storage.sql.Database;
import org.intellij.lang.annotations.Language;
public class PostgreSQLCommandSet extends AbstractCommandSet {
public PostgreSQLCommandSet(Database db) {
super(db);
}
@Override
@Language("postgresql")
public String createMapTableStatement() {
return """
CREATE TABLE IF NOT EXISTS bluemap_map (
id SMALLSERIAL PRIMARY KEY,
map_id VARCHAR(190) UNIQUE NOT NULL
)
""";
}
@Override
@Language("postgresql")
public String createCompressionTableStatement() {
return """
CREATE TABLE IF NOT EXISTS bluemap_map_tile_compression (
id SMALLSERIAL PRIMARY KEY,
compression VARCHAR(190) UNIQUE NOT NULL
)
""";
}
@Override
@Language("postgresql")
public String createMapMetaTableStatement() {
return """
CREATE TABLE IF NOT EXISTS bluemap_map_meta (
map SMALLINT NOT NULL
REFERENCES bluemap_map(id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
key VARCHAR(190) NOT NULL,
value BYTEA NOT NULL,
PRIMARY KEY (map, key)
)
""";
}
@Override
@Language("postgresql")
public String createMapTileTableStatement() {
return """
CREATE TABLE IF NOT EXISTS bluemap_map_tile (
map SMALLINT NOT NULL
REFERENCES bluemap_map (id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
lod SMALLINT NOT NULL,
x INT NOT NULL,
z INT NOT NULL,
compression SMALLINT NOT NULL
REFERENCES bluemap_map_tile_compression (id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
data BYTEA NOT NULL,
PRIMARY KEY (map, lod, x, z)
)
""";
}
@Override
@Language("postgresql")
public String fixLegacyCompressionIdsStatement() {
return """
UPDATE bluemap_map_tile_compression
SET compression = CONCAT('bluemap:', compression)
WHERE NOT compression LIKE '%:%'
""";
}
@Override
@Language("postgresql")
public String writeMapTileStatement() {
return """
INSERT
INTO bluemap_map_tile (map, lod, x, z, compression, data)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (map, lod, x, z)
DO UPDATE SET
compression = excluded.compression,
data = excluded.data
""";
}
@Override
@Language("postgresql")
public String readMapTileStatement() {
return """
SELECT t.data
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
INNER JOIN bluemap_map_tile_compression c
ON t.compression = c.id
WHERE m.map_id = ?
AND t.lod = ?
AND t.x = ?
AND t.z = ?
AND c.compression = ?
""";
}
@Override
@Language("postgresql")
public String deleteMapTileStatement() {
return """
DELETE
FROM bluemap_map_tile
WHERE map IN (
SELECT m.id
FROM bluemap_map m
WHERE m.map_id = ?
)
AND lod = ?
AND x = ?
AND z = ?
AND compression IN (
SELECT c.id
FROM bluemap_map_tile_compression c
WHERE c.compression = ?
)
""";
}
@Override
@Language("postgresql")
public String hasMapTileStatement() {
return """
SELECT COUNT(*) > 0
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
INNER JOIN bluemap_map_tile_compression c
ON t.compression = c.id
WHERE m.map_id = ?
AND t.lod = ?
AND t.x = ?
AND t.z = ?
AND c.compression = ?
""";
}
@Override
@Language("postgresql")
public String countAllMapTilesStatement() {
return """
SELECT COUNT(*)
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
WHERE m.map_id = ?
""";
}
@Override
@Language("postgresql")
public String purgeMapTilesStatement() {
return """
DELETE
FROM bluemap_map_tile
WHERE CTID IN (
SELECT CTID
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
WHERE m.map_id = ?
LIMIT ?
)
""";
}
@Override
@Language("postgresql")
public String listMapTilesStatement() {
return """
SELECT t.x, t.z
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
INNER JOIN bluemap_map_tile_compression c
ON t.compression = c.id
WHERE m.map_id = ?
AND t.lod = ?
AND c.compression = ?
LIMIT ? OFFSET ?
""";
}
@Override
@Language("postgresql")
public String writeMapMetaStatement() {
return """
INSERT
INTO bluemap_map_meta (map, key, value)
VALUES (?, ?, ?)
ON CONFLICT (map, key)
DO UPDATE SET
value = excluded.value
""";
}
@Override
@Language("postgresql")
public String readMapMetaStatement() {
return """
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 = ?
""";
}
@Override
@Language("postgresql")
public String deleteMapMetaStatement() {
return """
DELETE
FROM bluemap_map_meta
WHERE map IN (
SELECT id
FROM bluemap_map
WHERE map_id = ?
)
AND key = ?
""";
}
@Override
@Language("postgresql")
public String hasMapMetaStatement() {
return """
SELECT COUNT(*) > 0
FROM bluemap_map_meta t
INNER JOIN bluemap_map m
ON t.map = m.id
WHERE m.map_id = ?
AND t.key = ?
""";
}
@Override
@Language("postgresql")
public String purgeMapTileTableStatement() {
return """
DELETE
FROM bluemap_map_tile
WHERE map IN (
SELECT m.id
FROM bluemap_map m
WHERE m.map_id = ?
)
""";
}
@Override
@Language("postgresql")
public String purgeMapMetaTableStatement() {
return """
DELETE
FROM bluemap_map_meta
WHERE map IN (
SELECT m.id
FROM bluemap_map m
WHERE m.map_id = ?
)
""";
}
@Override
@Language("postgresql")
public String deleteMapStatement() {
return """
DELETE
FROM bluemap_map
WHERE map_id = ?
""";
}
@Override
@Language("postgresql")
public String hasMapStatement() {
return """
SELECT COUNT(*) > 0
FROM bluemap_map m
WHERE m.map_id = ?
""";
}
@Override
@Language("postgresql")
public String listMapIdsStatement() {
return """
SELECT map_id
FROM bluemap_map m
LIMIT ? OFFSET ?
""";
}
@Override
@Language("postgresql")
public String findMapKeyStatement() {
return """
SELECT id
FROM bluemap_map
WHERE map_id = ?
""";
}
@Override
@Language("postgresql")
public String createMapKeyStatement() {
return """
INSERT
INTO bluemap_map (map_id)
VALUES (?)
""";
}
@Override
@Language("postgresql")
public String findCompressionKeyStatement() {
return """
SELECT id
FROM bluemap_map_tile_compression
WHERE compression = ?
""";
}
@Override
@Language("postgresql")
public String createCompressionKeyStatement() {
return """
INSERT
INTO bluemap_map_tile_compression (compression)
VALUES (?)
""";
}
}

View File

@ -0,0 +1,351 @@
package de.bluecolored.bluemap.core.storage.sql.commandset;
import de.bluecolored.bluemap.core.storage.sql.Database;
import org.intellij.lang.annotations.Language;
public class SqliteCommandSet extends AbstractCommandSet {
public SqliteCommandSet(Database db) {
super(db);
}
@Override
@Language("sqlite")
public String createMapTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map` (
`id` INTEGER UNSIGNED PRIMARY KEY AUTOINCREMENT,
`map_id` TEXT UNIQUE NOT NULL
) STRICT
""";
}
@Override
@Language("sqlite")
public String createCompressionTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_tile_compression` (
`id` INTEGER UNSIGNED PRIMARY KEY AUTOINCREMENT,
`compression` TEXT UNIQUE NOT NULL
) STRICT
""";
}
@Override
@Language("sqlite")
public String createMapMetaTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_meta` (
`map` INTEGER UNSIGNED NOT NULL,
`key` TEXT NOT NULL,
`value` BLOB NOT NULL,
PRIMARY KEY (`map`, `key`),
CONSTRAINT `fk_bluemap_map_meta_map`
FOREIGN KEY (`map`)
REFERENCES `bluemap_map` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE
) STRICT
""";
}
@Override
@Language("sqlite")
public String createMapTileTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_tile` (
`map` INTEGER UNSIGNED NOT NULL,
`lod` INTEGER UNSIGNED NOT NULL,
`x` INTEGER NOT NULL,
`z` INTEGER NOT NULL,
`compression` INTEGER UNSIGNED NOT NULL,
`data` BLOB NOT NULL,
PRIMARY KEY (`map`, `lod`, `x`, `z`),
CONSTRAINT `fk_bluemap_map_tile_map`
FOREIGN KEY (`map`)
REFERENCES `bluemap_map` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_map_tile_compression`
FOREIGN KEY (`compression`)
REFERENCES `bluemap_map_tile_compression` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE
) STRICT
""";
}
@Override
@Language("sqlite")
public String fixLegacyCompressionIdsStatement() {
return """
UPDATE `bluemap_map_tile_compression`
SET `compression` = CONCAT('bluemap:', `compression`)
WHERE NOT `compression` LIKE '%:%'
""";
}
@Override
@Language("sqlite")
public String writeMapTileStatement() {
return """
REPLACE
INTO `bluemap_map_tile` (`map`, `lod`, `x`, `z`, `compression`, `data`)
VALUES (?, ?, ?, ?, ?, ?)
""";
}
@Override
@Language("sqlite")
public String readMapTileStatement() {
return """
SELECT t.`data`
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND t.`x` = ?
AND t.`z` = ?
AND c.`compression` = ?
""";
}
@Override
@Language("sqlite")
public String deleteMapTileStatement() {
return """
DELETE
FROM `bluemap_map_tile`
WHERE `map` IN (
SELECT m.`id`
FROM `bluemap_map` m
WHERE m.`map_id` = ?
)
AND `lod` = ?
AND `x` = ?
AND `z` = ?
AND `compression` IN (
SELECT c.`id`
FROM `bluemap_map_tile_compression` c
WHERE c.`compression` = ?
)
""";
}
@Override
@Language("sqlite")
public String hasMapTileStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND t.`x` = ?
AND t.`z` = ?
AND c.`compression` = ?
""";
}
@Override
@Language("sqlite")
public String countAllMapTilesStatement() {
return """
SELECT COUNT(*)
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
""";
}
@Override
@Language("sqlite")
public String purgeMapTilesStatement() {
return """
DELETE
FROM bluemap_map_tile
WHERE ROWID IN (
SELECT ROWID
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
WHERE m.map_id = ?
LIMIT ?
)
""";
}
@Override
@Language("sqlite")
public String listMapTilesStatement() {
return """
SELECT t.`x`, t.`z`
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND c.`compression` = ?
LIMIT ? OFFSET ?
""";
}
@Override
@Language("sqlite")
public String writeMapMetaStatement() {
return """
REPLACE
INTO `bluemap_map_meta` (`map`, `key`, `value`)
VALUES (?, ?, ?)
""";
}
@Override
@Language("sqlite")
public String readMapMetaStatement() {
return """
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` = ?
""";
}
@Override
@Language("sqlite")
public String deleteMapMetaStatement() {
return """
DELETE
FROM `bluemap_map_meta`
WHERE `map` IN (
SELECT `id`
FROM `bluemap_map`
WHERE `map_id` = ?
)
AND `key` = ?
""";
}
@Override
@Language("sqlite")
public String hasMapMetaStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map_meta` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
AND t.`key` = ?
""";
}
@Override
@Language("sqlite")
public String purgeMapTileTableStatement() {
return """
DELETE
FROM `bluemap_map_tile`
WHERE `map` IN (
SELECT m.`id`
FROM `bluemap_map` m
WHERE m.`map_id` = ?
)
""";
}
@Override
@Language("sqlite")
public String purgeMapMetaTableStatement() {
return """
DELETE
FROM `bluemap_map_meta`
WHERE `map` IN (
SELECT m.`id`
FROM `bluemap_map` m
WHERE m.`map_id` = ?
)
""";
}
@Override
@Language("sqlite")
public String deleteMapStatement() {
return """
DELETE
FROM `bluemap_map`
WHERE `map_id` = ?
""";
}
@Override
@Language("sqlite")
public String hasMapStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map` m
WHERE m.`map_id` = ?
""";
}
@Override
@Language("sqlite")
public String listMapIdsStatement() {
return """
SELECT `map_id`
FROM `bluemap_map` m
LIMIT ? OFFSET ?
""";
}
@Override
@Language("sqlite")
public String findMapKeyStatement() {
return """
SELECT `id`
FROM `bluemap_map`
WHERE map_id = ?
""";
}
@Override
@Language("sqlite")
public String createMapKeyStatement() {
return """
INSERT
INTO `bluemap_map` (`map_id`)
VALUES (?)
""";
}
@Override
@Language("sqlite")
public String findCompressionKeyStatement() {
return """
SELECT `id`
FROM `bluemap_map_tile_compression`
WHERE `compression` = ?
""";
}
@Override
@Language("sqlite")
public String createCompressionKeyStatement() {
return """
INSERT
INTO `bluemap_map_tile_compression` (`compression`)
VALUES (?)
""";
}
}

View File

@ -1,102 +0,0 @@
/*
* 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.dialect;
import org.intellij.lang.annotations.Language;
public interface Dialect {
@Language("sql")
String writeMapTile();
@Language("sql")
String readMapTile();
@Language("sql")
String readMapTileInfo();
@Language("sql")
String deleteMapTile();
@Language("sql")
String writeMeta();
@Language("sql")
String readMeta();
@Language("sql")
String readMetaSize();
@Language("sql")
String deleteMeta();
@Language("sql")
String purgeMapTile();
@Language("sql")
String purgeMapMeta();
@Language("sql")
String purgeMap();
@Language("sql")
String selectMapIds();
@Language("sql")
String initializeStorageMeta();
@Language("sql")
String selectStorageMeta();
@Language("sql")
String insertStorageMeta();
@Language("sql")
String initializeMap();
@Language("sql")
String initializeMapTileCompression();
@Language("sql")
String initializeMapMeta();
@Language("sql")
String initializeMapTile();
@Language("sql")
String updateStorageMeta(); // can be use twice in init
@Language("sql")
String deleteMapMeta();
@Language("sql")
String updateMapMeta(); // can be used twice in init
@Language("sql")
String lookupFK(String table, String idField, String valueField);
@Language("sql")
String insertFK(String table, String valueField);
}

View File

@ -1,66 +0,0 @@
/*
* 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.dialect;
import de.bluecolored.bluemap.core.storage.sql.*;
public enum DialectType {
MYSQL (MySQLStorage::new, "mysql"),
MARIADB (MySQLStorage::new, "mariadb"),
POSTGRESQL (PostgreSQLStorage::new, "postgresql"),
SQLITE (SQLiteStorage::new, "sqlite");
private static final DialectType FALLBACK = MYSQL;
private final SQLStorageFactory storageFactory;
private final String dialectName;
DialectType(SQLStorageFactory storageFactory, String dialectName) {
this.storageFactory = storageFactory;
this.dialectName = dialectName;
}
public String getDialectName() {
return dialectName;
}
public static SQLStorage getStorage(String dialectName, SQLStorageSettings settings) throws Exception {
for (DialectType dialect : values()) {
if (dialect.getDialectName().equals(dialectName)) {
return dialect.storageFactory.provide(settings);
}
}
// unknown dialect, use fallback
return FALLBACK.storageFactory.provide(settings);
}
@FunctionalInterface
public interface SQLStorageFactory {
SQLStorage provide(SQLStorageSettings config) throws Exception;
}
}

View File

@ -1,274 +0,0 @@
/*
* 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.dialect;
import org.intellij.lang.annotations.Language;
public class MySQLDialect implements Dialect {
public static final MySQLDialect INSTANCE = new MySQLDialect();
private MySQLDialect() {};
@Override
@Language("MySQL")
public String writeMapTile() {
return "REPLACE INTO `bluemap_map_tile` (`map`, `lod`, `x`, `z`, `compression`, `data`) " +
"VALUES (?, ?, ?, ?, ?, ?)";
}
@Override
@Language("MySQL")
public String readMapTile() {
return "SELECT t.`data` " +
"FROM `bluemap_map_tile` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
" INNER JOIN `bluemap_map_tile_compression` c " +
" ON t.`compression` = c.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`lod` = ? " +
"AND t.`x` = ? " +
"AND t.`z` = ? " +
"AND c.`compression` = ?";
}
@Override
@Language("MySQL")
public String readMapTileInfo() {
return "SELECT 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_compression` c " +
" ON t.`compression` = c.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`lod` = ? " +
"AND t.`x` = ? " +
"AND t.`z` = ? " +
"AND c.`compression` = ?";
}
@Override
@Language("MySQL")
public String deleteMapTile() {
return "DELETE t " +
"FROM `bluemap_map_tile` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`lod` = ? " +
"AND t.`x` = ? " +
"AND t.`z` = ?";
}
@Override
@Language("MySQL")
public String writeMeta() {
return "REPLACE INTO `bluemap_map_meta` (`map`, `key`, `value`) " +
"VALUES (?, ?, ?)";
}
@Override
@Language("MySQL")
public String readMeta() {
return "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` = ?";
}
@Override
@Language("MySQL")
public String readMetaSize() {
return "SELECT LENGTH(t.`value`) as 'size' " +
"FROM `bluemap_map_meta` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`key` = ?";
}
@Override
@Language("MySQL")
public String deleteMeta() {
return "DELETE t " +
"FROM `bluemap_map_meta` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`key` = ?";
}
@Override
@Language("MySQL")
public String purgeMapTile() {
return "DELETE t " +
"FROM `bluemap_map_tile` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ?";
}
@Override
@Language("MySQL")
public String purgeMapMeta() {
return "DELETE t " +
"FROM `bluemap_map_meta` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ?";
}
@Override
@Language("MySQL")
public String purgeMap() {
return "DELETE " +
"FROM `bluemap_map` " +
"WHERE `map_id` = ?";
}
@Override
@Language("MySQL")
public String selectMapIds() {
return "SELECT `map_id` FROM `bluemap_map`";
}
@Override
@Language("MySQL")
public String initializeStorageMeta() {
return "CREATE TABLE IF NOT EXISTS `bluemap_storage_meta` (" +
"`key` varchar(190) NOT NULL, " +
"`value` varchar(255) DEFAULT NULL, " +
"PRIMARY KEY (`key`)" +
") COLLATE 'utf8mb4_bin'";
}
@Override
@Language("MySQL")
public String selectStorageMeta() {
return "SELECT `value` FROM `bluemap_storage_meta` " +
"WHERE `key` = ?";
}
@Override
@Language("MySQL")
public String insertStorageMeta() {
return "INSERT INTO `bluemap_storage_meta` (`key`, `value`) " +
"VALUES (?, ?)";
}
@Override
@Language("MySQL")
public String initializeMap() {
return "CREATE TABLE `bluemap_map` (" +
"`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT," +
"`map_id` VARCHAR(190) NOT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE INDEX `map_id` (`map_id`)" +
") COLLATE 'utf8mb4_bin';";
}
@Override
@Language("MySQL")
public String initializeMapTileCompression() {
return "CREATE TABLE `bluemap_map_tile_compression` (" +
"`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT," +
"`compression` VARCHAR(190) NOT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE INDEX `compression` (`compression`)" +
") COLLATE 'utf8mb4_bin';";
}
@Override
@Language("MySQL")
public String initializeMapMeta() {
return "CREATE TABLE `bluemap_map_meta` (" +
"`map` SMALLINT UNSIGNED NOT NULL," +
"`key` varchar(190) 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" +
") COLLATE 'utf8mb4_bin'";
}
@Override
@Language("MySQL")
public String initializeMapTile() {
return "CREATE TABLE `bluemap_map_tile` (" +
"`map` SMALLINT UNSIGNED NOT NULL," +
"`lod` 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`, `lod`, `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_compression` FOREIGN KEY (`compression`) REFERENCES `bluemap_map_tile_compression` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT" +
") COLLATE 'utf8mb4_bin';";
}
@Override
@Language("MySQL")
public String updateStorageMeta() {
return "UPDATE `bluemap_storage_meta` " +
"SET `value` = ? " +
"WHERE `key` = ?";
}
@Override
@Language("MySQL")
public String deleteMapMeta() {
return "DELETE FROM `bluemap_map_meta`" +
"WHERE `key` IN (?, ?, ?)";
}
@Override
@Language("MySQL")
public String updateMapMeta() {
return "UPDATE `bluemap_map_meta` " +
"SET `key` = ? " +
"WHERE `key` = ?";
}
@Override
@Language("MySQL")
public String lookupFK(String table, String idField, String valueField) {
return "SELECT `" + idField + "` FROM `" + table + "` " +
"WHERE `" + valueField + "` = ?";
}
@Override
@Language("MySQL")
public String insertFK(String table, String valueField) {
return "INSERT INTO `" + table + "` (`" + valueField + "`) " +
"VALUES (?)";
}
}

View File

@ -1,264 +0,0 @@
/*
* 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.dialect;
import org.intellij.lang.annotations.Language;
public class PostgresDialect implements Dialect {
public static final PostgresDialect INSTANCE = new PostgresDialect();
private PostgresDialect() {};
@Override
@Language("PostgreSQL")
public String writeMapTile() {
return "INSERT INTO bluemap_map_tile (map, lod, x, z, compression, data) " +
"VALUES (?, ?, ?, ?, ?, ?) " +
"ON CONFLICT (map, lod, x, z) DO UPDATE SET compression = EXCLUDED.compression, data = EXCLUDED.data";
}
@Override
@Language("PostgreSQL")
public String readMapTile() {
return "SELECT t.data " +
"FROM bluemap_map_tile t " +
" INNER JOIN bluemap_map m " +
" ON t.map = m.id " +
" INNER JOIN bluemap_map_tile_compression c " +
" ON t.compression = c.id " +
"WHERE m.map_id = ? " +
"AND t.lod = ? " +
"AND t.x = ? " +
"AND t.z = ? " +
"AND c.compression = ?";
}
@Override
@Language("PostgreSQL")
public String readMapTileInfo() {
return "SELECT t.changed, OCTET_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_compression c " +
" ON t.compression = c.id " +
"WHERE m.map_id = ? " +
"AND t.lod = ? " +
"AND t.x = ? " +
"AND t.z = ? " +
"AND c.compression = ?";
}
@Override
@Language("PostgreSQL")
public String deleteMapTile() {
return "DELETE FROM bluemap_map_tile t " +
"USING bluemap_map m " +
"WHERE t.map = m.id " +
"AND m.map_id = ? " +
"AND t.lod = ? " +
"AND t.x = ? " +
"AND t.z = ?";
}
@Override
@Language("PostgreSQL")
public String writeMeta() {
return "INSERT INTO bluemap_map_meta (map, key, value) " +
"VALUES (?, ?, ?) " +
"ON CONFLICT (map, key) DO UPDATE SET value = EXCLUDED.value";
}
@Override
@Language("PostgreSQL")
public String readMeta() {
return "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 = ?";
}
@Override
@Language("PostgreSQL")
public String readMetaSize() {
return "SELECT OCTET_LENGTH(t.value) as size " +
"FROM bluemap_map_meta t " +
" INNER JOIN bluemap_map m " +
" ON t.map = m.id " +
"WHERE m.map_id = ? " +
"AND t.key = ?";
}
@Override
@Language("PostgreSQL")
public String deleteMeta() {
return "DELETE FROM bluemap_map_meta t " +
"USING bluemap_map m " +
"WHERE t.map = m.id " +
"AND m.map_id = ? " +
"AND t.key = ?";
}
@Override
@Language("PostgreSQL")
public String purgeMapTile() {
return "DELETE FROM bluemap_map_tile t " +
"USING bluemap_map m " +
"WHERE t.map = m.id " +
"AND m.map_id = ?";
}
@Override
@Language("PostgreSQL")
public String purgeMapMeta() {
return "DELETE FROM bluemap_map_meta t " +
"USING bluemap_map m " +
"WHERE t.map = m.id " +
"AND m.map_id = ?";
}
@Override
@Language("PostgreSQL")
public String purgeMap() {
return "DELETE FROM bluemap_map " +
"WHERE map_id = ?";
}
@Override
@Language("PostgreSQL")
public String selectMapIds() {
return "SELECT map_id FROM bluemap_map";
}
@Override
@Language("PostgreSQL")
public String initializeStorageMeta() {
return "CREATE TABLE IF NOT EXISTS bluemap_storage_meta (" +
"key varchar(255) PRIMARY KEY, " +
"value varchar(255)" +
")";
}
@Override
@Language("PostgreSQL")
public String selectStorageMeta() {
return "SELECT value FROM bluemap_storage_meta " +
"WHERE key = ?";
}
@Override
@Language("PostgreSQL")
public String insertStorageMeta() {
return "INSERT INTO bluemap_storage_meta (key, value) " +
"VALUES (?, ?) " +
"ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value";
}
@Override
@Language("PostgreSQL")
public String initializeMap() {
return "CREATE TABLE IF NOT EXISTS bluemap_map (" +
"id SERIAL PRIMARY KEY, " +
"map_id VARCHAR(255) UNIQUE NOT NULL" +
")";
}
@Override
@Language("PostgreSQL")
public String initializeMapTileCompression() {
return "CREATE TABLE IF NOT EXISTS bluemap_map_tile_compression (" +
"id SERIAL PRIMARY KEY, " +
"compression VARCHAR(255) UNIQUE NOT NULL" +
")";
}
@Override
@Language("PostgreSQL")
public String initializeMapMeta() {
return "CREATE TABLE IF NOT EXISTS bluemap_map_meta (" +
"map SMALLINT REFERENCES bluemap_map(id) ON UPDATE RESTRICT ON DELETE RESTRICT, " +
"key varchar(255) NOT NULL, " +
"value BYTEA NOT NULL, " +
"PRIMARY KEY (map, key)" +
")";
}
@Override
@Language("PostgreSQL")
public String initializeMapTile() {
return "CREATE TABLE IF NOT EXISTS bluemap_map_tile (" +
"map SMALLINT REFERENCES bluemap_map(id) ON UPDATE RESTRICT ON DELETE RESTRICT, " +
"lod SMALLINT NOT NULL, " +
"x INT NOT NULL, " +
"z INT NOT NULL, " +
"compression SMALLINT REFERENCES bluemap_map_tile_compression(id) ON UPDATE RESTRICT ON DELETE RESTRICT, " +
"changed TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"data BYTEA NOT NULL, " +
"PRIMARY KEY (map, lod, x, z)" +
")";
}
@Override
@Language("PostgreSQL")
public String updateStorageMeta() {
return "UPDATE bluemap_storage_meta " +
"SET value = ? " +
"WHERE key = ?";
}
@Override
@Language("PostgreSQL")
public String deleteMapMeta() {
return "DELETE FROM bluemap_map_meta " +
"WHERE key IN (?, ?, ?)";
}
@Override
@Language("PostgreSQL")
public String updateMapMeta() {
return "UPDATE bluemap_map_meta " +
"SET key = ? " +
"WHERE key = ?";
}
@Override
@Language("PostgreSQL")
public String lookupFK(String table, String idField, String valueField) {
return "SELECT " + idField + " FROM " + table +
" WHERE " + valueField + " = ?";
}
@Override
@Language("PostgreSQL")
public String insertFK(String table, String valueField) {
return "INSERT INTO " + table + " (" + valueField + ") " +
"VALUES (?)";
}
}

View File

@ -1,280 +0,0 @@
/*
* 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.dialect;
import org.intellij.lang.annotations.Language;
public class SqliteDialect implements Dialect {
public static final SqliteDialect INSTANCE = new SqliteDialect();
private SqliteDialect() {}
@Override
@Language("sqlite")
public String writeMapTile() {
return "REPLACE INTO `bluemap_map_tile` (`map`, `lod`, `x`, `z`, `compression`, `data`) " +
"VALUES (?, ?, ?, ?, ?, ?)";
}
@Override
@Language("sqlite")
public String readMapTile() {
return "SELECT t.`data` " +
"FROM `bluemap_map_tile` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
" INNER JOIN `bluemap_map_tile_compression` c " +
" ON t.`compression` = c.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`lod` = ? " +
"AND t.`x` = ? " +
"AND t.`z` = ? " +
"AND c.`compression` = ?";
}
@Override
@Language("sqlite")
public String readMapTileInfo() {
return "SELECT 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_compression` c " +
" ON t.`compression` = c.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`lod` = ? " +
"AND t.`x` = ? " +
"AND t.`z` = ? " +
"AND c.`compression` = ?";
}
@Override
@Language("sqlite")
public String deleteMapTile() {
return "DELETE FROM `bluemap_map_tile` " +
"WHERE `map` IN( " +
" SELECT `id` " +
" FROM `bluemap_map` " +
" WHERE `map_id` = ? " +
" LIMIT 1 " +
") " +
"AND `lod` = ? " +
"AND `x` = ? " +
"AND `z` = ? ";
}
@Override
@Language("sqlite")
public String writeMeta() {
return "REPLACE INTO `bluemap_map_meta` (`map`, `key`, `value`) " +
"VALUES (?, ?, ?)";
}
@Override
@Language("sqlite")
public String readMeta() {
return "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` = ?";
}
@Override
@Language("sqlite")
public String readMetaSize() {
return "SELECT LENGTH(t.`value`) as 'size' " +
"FROM `bluemap_map_meta` t " +
" INNER JOIN `bluemap_map` m " +
" ON t.`map` = m.`id` " +
"WHERE m.`map_id` = ? " +
"AND t.`key` = ?";
}
@Override
@Language("sqlite")
public String deleteMeta() {
return "DELETE FROM `bluemap_map_meta` " +
"WHERE `map` IN( " +
" SELECT `id` " +
" FROM `bluemap_map` " +
" WHERE `map_id` = ? " +
" LIMIT 1 " +
") " +
"AND `key` = ?";
}
@Override
@Language("sqlite")
public String purgeMapTile() {
return "DELETE FROM `bluemap_map_tile` " +
"WHERE `map` IN( " +
" SELECT `id` " +
" FROM `bluemap_map` " +
" WHERE `map_id` = ? " +
" LIMIT 1 " +
")";
}
@Override
@Language("sqlite")
public String purgeMapMeta() {
return "DELETE FROM `bluemap_map_meta` " +
"WHERE `map` IN( " +
" SELECT `id` " +
" FROM `bluemap_map` " +
" WHERE `map_id` = ? " +
" LIMIT 1 " +
")";
}
@Override
@Language("sqlite")
public String purgeMap() {
return "DELETE " +
"FROM `bluemap_map` " +
"WHERE `map_id` = ?";
}
@Override
@Language("sqlite")
public String selectMapIds() {
return "SELECT `map_id` FROM `bluemap_map`";
}
@Override
@Language("sqlite")
public String initializeStorageMeta() {
return "CREATE TABLE IF NOT EXISTS `bluemap_storage_meta` (" +
"`key` varchar(255) NOT NULL, " +
"`value` varchar(255) DEFAULT NULL, " +
"PRIMARY KEY (`key`)" +
")";
}
@Override
@Language("sqlite")
public String selectStorageMeta() {
return "SELECT `value` FROM `bluemap_storage_meta` " +
"WHERE `key` = ?";
}
@Override
@Language("sqlite")
public String insertStorageMeta() {
return "INSERT INTO `bluemap_storage_meta` (`key`, `value`) " +
"VALUES (?, ?)";
}
@Override
@Language("sqlite")
public String initializeMap() {
return "CREATE TABLE `bluemap_map` (" +
"`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"`map_id` VARCHAR(255) NOT NULL," +
"UNIQUE (`map_id`)" +
");";
}
@Override
@Language("sqlite")
public String initializeMapTileCompression() {
return "CREATE TABLE `bluemap_map_tile_compression` (" +
"`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
"`compression` VARCHAR(255) NOT NULL," +
"UNIQUE (`compression`)" +
");";
}
@Override
@Language("sqlite")
public String initializeMapMeta() {
return "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" +
")";
}
@Override
@Language("sqlite")
public String initializeMapTile() {
return "CREATE TABLE `bluemap_map_tile` (" +
"`map` SMALLINT UNSIGNED NOT NULL," +
"`lod` SMALLINT UNSIGNED NOT NULL," +
"`x` INT NOT NULL," +
"`z` INT NOT NULL," +
"`compression` SMALLINT UNSIGNED NOT NULL," +
"`changed` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP," +
"`data` LONGBLOB NOT NULL," +
"PRIMARY KEY (`map`, `lod`, `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_compression` FOREIGN KEY (`compression`) REFERENCES `bluemap_map_tile_compression` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT" +
");";
}
@Override
@Language("sqlite")
public String updateStorageMeta() {
return "UPDATE `bluemap_storage_meta` " +
"SET `value` = ? " +
"WHERE `key` = ?";
}
@Override
@Language("sqlite")
public String deleteMapMeta() {
return "DELETE FROM `bluemap_map_meta`" +
"WHERE `key` IN (?, ?, ?)";
}
@Override
@Language("sqlite")
public String updateMapMeta() {
return "UPDATE `bluemap_map_meta` " +
"SET `key` = ? " +
"WHERE `key` = ?";
}
@Override
@Language("sqlite")
public String lookupFK(String table, String idField, String valueField) {
return "SELECT `" + idField + "` FROM `" + table + "` " +
"WHERE `" + valueField + "` = ?";
}
@Override
@Language("sqlite")
public String insertFK(String table, String valueField) {
return "INSERT INTO `" + table + "` (`" + valueField + "`) " +
"VALUES (?)";
}
}

View File

@ -1,144 +0,0 @@
/*
* 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.threejs;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import de.bluecolored.bluemap.core.util.Preconditions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a ThreeJS BufferAttribute
*/
public class BufferAttribute {
private int itemSize;
private boolean normalized;
private float[] values;
/**
* Creates a new {@link BufferAttribute} with the defined item-size
*/
public BufferAttribute(float[] values, int itemSize) {
Preconditions.checkArgument(values.length % itemSize == 0, "The length of the values-array is not a multiple of the item-size!");
this.values = values;
this.itemSize = itemSize;
this.normalized = false;
}
/**
* Creates a new {@link BufferAttribute} with the defined item-size and the
* defined threejs "normalized" attribute
*/
public BufferAttribute(float[] values, int itemSize, boolean normalized) {
Preconditions.checkArgument(values.length % itemSize == 0, "The length of the values-array is not a multiple of the item-size!");
this.values = values;
this.itemSize = itemSize;
this.normalized = normalized;
}
public void writeJson(JsonWriter json) throws IOException {
json.beginObject();
json.name("type").value("Float32Array");
json.name("itemSize").value(itemSize);
json.name("normalized").value(normalized);
json.name("array").beginArray();
for (int i = 0; i < values.length; i++) {
// rounding and remove ".0" to save string space
double d = Math.round(values[i] * 10000d) / 10000d;
if (d == (long) d) json.value((long) d);
else json.value(d);
}
json.endArray();
json.endObject();
}
public int getItemSize() {
return this.itemSize;
}
public boolean isNormalized() {
return this.normalized;
}
public int getValueCount() {
return this.values.length;
}
public int getItemCount() {
return Math.floorDiv(getValueCount(), getItemSize());
}
public float[] values() {
return values;
}
public static BufferAttribute readJson(JsonReader json) throws IOException {
List<Float> list = new ArrayList<>(1000);
int itemSize = 1;
boolean normalized = false;
json.beginObject(); //root
while (json.hasNext()){
String name = json.nextName();
if(name.equals("array")){
json.beginArray(); //array
while (json.hasNext()){
list.add((float) json.nextDouble());
}
json.endArray(); //array
}
else if (name.equals("itemSize")) {
itemSize = json.nextInt();
}
else if (name.equals("normalized")) {
normalized = json.nextBoolean();
}
else json.skipValue();
}
json.endObject(); //root
//collect values in array
float[] values = new float[list.size()];
for (int i = 0; i < values.length; i++) {
values[i] = list.get(i);
}
return new BufferAttribute(values, itemSize, normalized);
}
}

View File

@ -1,214 +0,0 @@
/*
* 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.threejs;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.*;
import java.util.Map.Entry;
public class BufferGeometry {
public Map<String, BufferAttribute> attributes;
public MaterialGroup[] groups;
private BufferGeometry() {}
public BufferGeometry(float[] position, float[] normal, float[] color, float[] uv, MaterialGroup[] groups) {
this.attributes = new HashMap<>();
addAttribute("position", new BufferAttribute(position, 3));
addAttribute("normal", new BufferAttribute(normal, 3));
addAttribute("color", new BufferAttribute(color, 3));
addAttribute("uv", new BufferAttribute(uv, 2));
this.groups = groups;
}
public void addAttribute(String name, BufferAttribute attribute) {
this.attributes.put(name, attribute);
}
public boolean isValid() {
int faceCount = getFaceCount();
for (BufferAttribute attribute : attributes.values()) {
if (attribute.getItemCount() != faceCount) return false;
}
return true;
}
public int getFaceCount() {
if (attributes.isEmpty()) return 0;
BufferAttribute attribute = attributes.values().iterator().next();
return attribute.getItemCount();
}
public String toJson() {
try {
StringWriter sw = new StringWriter();
Gson gson = new GsonBuilder().create();
JsonWriter json = gson.newJsonWriter(sw);
json.beginObject(); // main-object
json.name("tileGeometry").beginObject(); // tile-geometry-object
// set special values
json.name("type").value("BufferGeometry");
json.name("uuid").value(UUID.randomUUID().toString().toUpperCase());
json.name("data").beginObject(); // data
json.name("attributes").beginObject(); // attributes
for (Entry<String, BufferAttribute> entry : attributes.entrySet()) {
json.name(entry.getKey());
entry.getValue().writeJson(json);
}
json.endObject(); // attributes
json.name("groups").beginArray(); // groups
// write groups into json
for (MaterialGroup g : groups) {
json.beginObject();
json.name("materialIndex").value(g.getMaterialIndex());
json.name("start").value(g.getStart());
json.name("count").value(g.getCount());
json.endObject();
}
json.endArray(); // groups
json.endObject(); // data
json.endObject(); // tile-geometry-object
json.endObject(); // main-object
// save and return
json.flush();
return sw.toString();
} catch (IOException e) {
// since we are using a StringWriter there should never be an IO exception
// thrown
throw new RuntimeException(e);
}
}
public static BufferGeometry fromJson(String jsonString) throws IOException {
Gson gson = new GsonBuilder().create();
JsonReader json = gson.newJsonReader(new StringReader(jsonString));
List<MaterialGroup> groups = new ArrayList<>(10);
Map<String, BufferAttribute> attributes = new HashMap<>();
json.beginObject(); // root
while (!"tileGeometry".equals(json.nextName())) json.skipValue(); // find tileGeometry
json.beginObject(); // tileGeometry
while (json.hasNext()) {
String name1 = json.nextName();
if (name1.equals("data")) {
json.beginObject(); // data
while (json.hasNext()) {
String name2 = json.nextName();
if (name2.equals("attributes")) {
json.beginObject(); // attributes
while (json.hasNext()) {
String name3 = json.nextName();
attributes.put(name3, BufferAttribute.readJson(json));
}
json.endObject(); // attributes
}
else if (name2.equals("groups")) {
json.beginArray(); // groups
while (json.hasNext()) {
MaterialGroup group = new MaterialGroup(0, 0, 0);
json.beginObject(); // group
while (json.hasNext()) {
String name3 = json.nextName();
if (name3.equals("materialIndex")) {
group.setMaterialIndex(json.nextInt());
}
else if (name3.equals("start")) {
group.setStart(json.nextInt());
}
else if (name3.equals("count")) {
group.setCount(json.nextInt());
}
else json.skipValue();
}
json.endObject(); // group
groups.add(group);
}
json.endArray(); // groups
}
else json.skipValue();
}
json.endObject();// data
}
else json.skipValue();
}
json.endObject(); // tileGeometry
while (json.hasNext()) json.skipValue(); // skip remaining values
json.endObject(); // root
groups.sort((g1, g2) -> (int) Math.signum(g1.getStart() - g2.getStart()));
int nextGroup = 0;
for (MaterialGroup g : groups) {
if (g.getStart() != nextGroup)
throw new IllegalArgumentException("Group did not start at correct index! (Got " + g.getStart() + " but expected " + nextGroup + ")");
if (g.getCount() < 0) throw new IllegalArgumentException("Group has a negative count! (" + g.getCount() + ")");
nextGroup += g.getCount();
}
BufferGeometry bufferGeometry = new BufferGeometry();
bufferGeometry.attributes = attributes;
bufferGeometry.groups = groups.toArray(new MaterialGroup[groups.size()]);
return bufferGeometry;
}
}

View File

@ -1,61 +0,0 @@
/*
* 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.threejs;
public class MaterialGroup {
private int materialIndex;
private int start;
private int count;
public MaterialGroup(int materialIndex, int start, int count) {
this.materialIndex = materialIndex;
this.start = start;
this.count = count;
}
public int getMaterialIndex() {
return materialIndex;
}
public int getStart() {
return start;
}
public int getCount() {
return count;
}
public void setMaterialIndex(int materialIndex) {
this.materialIndex = materialIndex;
}
public void setStart(int start) {
this.start = start;
}
public void setCount(int count) {
this.count = count;
}
}

View File

@ -30,7 +30,12 @@
import java.io.InputStream;
import java.io.OutputStream;
public class DelegateInputStream extends InputStream {
/**
* An {@link InputStream} implementation delegating all methods to another InputStream.
* Can be used as a base-class for other InputStream implementations that wrap around an existing stream and only want
* to modify certain methods.
*/
public abstract class DelegateInputStream extends InputStream {
protected final InputStream in;

View File

@ -29,7 +29,12 @@
import java.io.IOException;
import java.io.OutputStream;
public class DelegateOutputStream extends OutputStream {
/**
* An {@link OutputStream} implementation delegating all methods to another OutputStream.
* Can be used as a base-class for other OutputStream implementations that wrap around an existing stream and only want
* to modify certain methods.
*/
public abstract class DelegateOutputStream extends OutputStream {
protected final OutputStream out;

View File

@ -27,6 +27,9 @@
import java.io.IOException;
import java.io.InputStream;
/**
* An {@link InputStream} implementation that performs an additional action right <b>after</b> the base stream got closed.
*/
public class OnCloseInputStream extends DelegateInputStream {
private final AutoCloseable onClose;

View File

@ -27,6 +27,9 @@
import java.io.IOException;
import java.io.OutputStream;
/**
* An {@link OutputStream} implementation that performs an additional action right <b>after</b> the base stream got closed.
*/
public class OnCloseOutputStream extends DelegateOutputStream {
private final AutoCloseable onClose;

View File

@ -32,6 +32,7 @@
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resources.datapack.DataPack;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Vector2iCache;
@ -45,7 +46,6 @@
import lombok.Getter;
import lombok.ToString;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -54,7 +54,6 @@
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
@Getter
@ToString
@ -260,7 +259,7 @@ private Chunk loadChunk(int x, int z) {
public static MCAWorld load(Path worldFolder, Key dimension) throws IOException, InterruptedException {
// load level.dat
Path levelFile = worldFolder.resolve("level.dat");
InputStream levelFileIn = new GZIPInputStream(new BufferedInputStream(Files.newInputStream(levelFile)));
InputStream levelFileIn = Compression.GZIP.decompress(Files.newInputStream(levelFile));
LevelData levelData = MCAUtil.BLUENBT.read(levelFileIn, LevelData.class);
// load datapacks

View File

@ -24,14 +24,13 @@
*/
package de.bluecolored.bluemap.core.world.mca.chunk;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -63,7 +62,7 @@ public MCAChunk load(byte[] data, int offset, int length, Compression compressio
// try last used version
ChunkVersionLoader<?> usedLoader = lastUsedLoader;
MCAChunk chunk;
try (InputStream decompressedIn = new BufferedInputStream(compression.decompress(in))) {
try (InputStream decompressedIn = compression.decompress(in)) {
chunk = usedLoader.load(world, decompressedIn);
}
@ -71,7 +70,7 @@ public MCAChunk load(byte[] data, int offset, int length, Compression compressio
ChunkVersionLoader<?> actualLoader = findBestLoaderForVersion(chunk.getDataVersion());
if (actualLoader != null && usedLoader != actualLoader) {
in.reset(); // reset read position
try (InputStream decompressedIn = new BufferedInputStream(compression.decompress(in))) {
try (InputStream decompressedIn = compression.decompress(in)) {
chunk = actualLoader.load(world, decompressedIn);
}
lastUsedLoader = actualLoader;

View File

@ -25,12 +25,11 @@
package de.bluecolored.bluemap.core.world.mca.region;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import de.bluecolored.bluemap.core.world.mca.chunk.MCAChunk;
import io.airlift.compress.zstd.ZstdInputStream;
import java.io.*;
import java.nio.file.Files;
@ -147,9 +146,8 @@ public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
byte[] chunkDataBuffer = null;
try (
InputStream in = new ZstdInputStream(new ByteArrayInputStream(compressedData));
BufferedInputStream bIn = new BufferedInputStream(in);
DataInputStream dIn = new DataInputStream(bIn)
InputStream in = Compression.ZSTD.decompress(new ByteArrayInputStream(compressedData));
DataInputStream dIn = new DataInputStream(in)
) {
int[] chunkDataLengths = new int[1024];
int[] chunkTimestamps = new int[1024];

View File

@ -25,7 +25,7 @@
package de.bluecolored.bluemap.core.world.mca.region;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region;

View File

@ -43,7 +43,7 @@
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.metrics.Metrics;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.util.FileHelper;
import org.apache.commons.cli.*;
import org.apache.commons.lang3.time.DurationFormatUtils;
@ -72,7 +72,10 @@ public void renderMaps(BlueMapService blueMap, boolean watch, boolean forceRende
@Nullable String mapsToRender) throws ConfigurationException, IOException, InterruptedException {
//metrics report
if (blueMap.getConfig().getCoreConfig().isMetrics()) Metrics.sendReportAsync("cli");
if (blueMap.getConfig().getCoreConfig().isMetrics()) Metrics.sendReportAsync(
"cli",
blueMap.getConfig().getMinecraftVersion().getVersionString()
);
if (blueMap.getConfig().getWebappConfig().isEnabled())
blueMap.createOrUpdateWebApp(forceGenerateWebapp);
@ -201,12 +204,13 @@ public void startWebserver(BlueMapService blueMap, boolean verbose) throws IOExc
// map route
for (var mapConfigEntry : blueMap.getConfig().getMapConfigs().entrySet()) {
Storage storage = blueMap.getOrLoadStorage(mapConfigEntry.getValue().getStorage());
MapStorage storage = blueMap.getOrLoadStorage(mapConfigEntry.getValue().getStorage())
.map(mapConfigEntry.getKey());
routingRequestHandler.register(
"maps/" + Pattern.quote(mapConfigEntry.getKey()) + "/(.*)",
"$1",
new MapRequestHandler(mapConfigEntry.getKey(), storage)
new MapRequestHandler(storage)
);
}
@ -242,9 +246,10 @@ public void startWebserver(BlueMapService blueMap, boolean verbose) throws IOExc
throw new ConfigurationException("BlueMap failed to bind to the configured address.\n" +
"This usually happens when the configured port (" + config.getPort() + ") is already in use by some other program.", ex);
} catch (IOException ex) {
throw new ConfigurationException("BlueMap failed to initialize the webserver.\n" +
"Check your webserver-config if everything is configured correctly.\n" +
"(Make sure you DON'T use the same port for bluemap that you also use for your minecraft server)", ex);
throw new ConfigurationException("""
BlueMap failed to initialize the webserver.
Check your webserver-config if everything is configured correctly.
(Make sure you DON'T use the same port for bluemap that you also use for your minecraft server)""", ex);
}
}

View File

@ -31,7 +31,6 @@
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.world.World;
import net.md_5.bungee.chat.ComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.CommandSender;