diff --git a/api/src/main/java/de/epiceric/shopchest/api/ShopManager.java b/api/src/main/java/de/epiceric/shopchest/api/ShopManager.java index 21c1f4b..56272c6 100644 --- a/api/src/main/java/de/epiceric/shopchest/api/ShopManager.java +++ b/api/src/main/java/de/epiceric/shopchest/api/ShopManager.java @@ -2,6 +2,7 @@ package de.epiceric.shopchest.api; import java.util.Collection; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import org.bukkit.Location; @@ -77,12 +78,11 @@ public interface ShopManager { * Can be either chest if it's on a double chest * @param buyPrice the price a player can buy the product for. * @param sellPrice the price a player can sell the product for. - * @param callback the callback returning the created shop on success - * @param errorCallback the callback returning the error if one occurred + * @return a completable future returning the new shop * @since 1.13 * @see ShopManager#addAdminShop(ShopProduct, Location, double, double, Consumer, Consumer) */ - void addShop(OfflinePlayer vendor, ShopProduct product, Location location, double buyPrice, double sellPrice, Consumer callback, Consumer errorCallback); + CompletableFuture addShop(OfflinePlayer vendor, ShopProduct product, Location location, double buyPrice, double sellPrice); /** * Creates an admin shop and adds it to the database @@ -96,32 +96,29 @@ public interface ShopManager { * Can be either chest if it's on a double chest * @param buyPrice the price a player can buy the product for. * @param sellPrice the price a player can sell the product for. - * @param callback the callback returning the created shop on success - * @param errorCallback the callback returning the error if one occurred + * @return a completable future returning the new shop * @since 1.13 * @see ShopManager#addShop(OfflinePlayer, ShopProduct, Location, double, double, Consumer, Consumer) */ - void addAdminShop(ShopProduct product, Location location, double buyPrice, double sellPrice, Consumer callback, Consumer errorCallback); + CompletableFuture addAdminShop(ShopProduct product, Location location, double buyPrice, double sellPrice); /** * Removes a shop from the database * * @param shop the shop to remove - * @param callback the callback returning nothing on success - * @param errorCallback the callback returning the error if one occurred + * @return a completable future returning nothing * @since 1.13 */ - void removeShop(Shop shop, Runnable callback, Consumer errorCallback); + CompletableFuture removeShop(Shop shop); /** * Removes all shops and reloads the shops in currently loaded chunks *

* This does not trigger the {@link ShopReloadEvent}. * - * @param callback the callback returning the amount of shops on success - * @param errorCallback the callback returning the error if one occurred + * @return a completable future returning the loaded shops * @since 1.13 */ - void reloadShops(Consumer callback, Consumer errorCallback); + CompletableFuture> reloadShops(); } \ No newline at end of file diff --git a/core/src/main/java/de/epiceric/shopchest/ShopChestImpl.java b/core/src/main/java/de/epiceric/shopchest/ShopChestImpl.java index 4c3ce29..9f7ca10 100644 --- a/core/src/main/java/de/epiceric/shopchest/ShopChestImpl.java +++ b/core/src/main/java/de/epiceric/shopchest/ShopChestImpl.java @@ -6,15 +6,12 @@ import java.lang.reflect.Field; import java.net.URL; import java.net.URLConnection; import java.util.Map; -import java.util.stream.Stream; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.bstats.bukkit.Metrics; -import org.bukkit.Chunk; -import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.command.CommandMap; import org.bukkit.command.SimpleCommandMap; @@ -26,7 +23,6 @@ import de.epiceric.shopchest.api.ShopManager; import de.epiceric.shopchest.api.command.ShopCommand; import de.epiceric.shopchest.api.config.Config; import de.epiceric.shopchest.api.database.DatabaseType; -import de.epiceric.shopchest.api.event.ShopLoadedEvent; import de.epiceric.shopchest.api.player.ShopPlayer; import de.epiceric.shopchest.command.ShopCommandImpl; import de.epiceric.shopchest.config.ConfigManager; @@ -182,31 +178,21 @@ public class ShopChestImpl extends ShopChest { database = new MySQL(this); } - ((ShopManagerImpl) getShopManager()).loadShopAmounts( - shopAmounts -> { - Logger.info("Loaded shop amounts from the database"); - }, - error -> { - Logger.severe("Failed to load shops amounts from the database"); - Logger.severe("Shop limits will not be working correctly"); - Logger.severe(error); - } - ); - - Chunk[] chunks = getServer().getWorlds().stream().map(World::getLoadedChunks) - .flatMap(Stream::of).toArray(Chunk[]::new); - - ((ShopManagerImpl) getShopManager()).loadShops(chunks, - shops -> { - getServer().getPluginManager().callEvent(new ShopLoadedEvent(shops)); - Logger.info("Loaded {0} shops from the database", shops.size()); - }, - error -> { - Logger.severe("Failed to load shops from the database"); - Logger.severe(error); - getServer().getPluginManager().disablePlugin(this); - } - ); + getDatabase().connect() + .thenCompose(amount -> ((ShopManagerImpl) getShopManager()).loadShopAmounts()) + .thenCompose(shopAmounts -> { + Logger.info("Loaded shop amounts from the database"); + return getShopManager().reloadShops(); + }) + .thenAccept(shops -> { + Logger.info("Loaded {0} shops from the database", shops.size()); + }) + .exceptionally(ex -> { + Logger.severe("Failed to load shops from the database"); + Logger.severe(ex); + getServer().getPluginManager().disablePlugin(this); + return null; + }); } private void unloadDatabase() { diff --git a/core/src/main/java/de/epiceric/shopchest/ShopManagerImpl.java b/core/src/main/java/de/epiceric/shopchest/ShopManagerImpl.java index 5e66692..1ed454a 100644 --- a/core/src/main/java/de/epiceric/shopchest/ShopManagerImpl.java +++ b/core/src/main/java/de/epiceric/shopchest/ShopManagerImpl.java @@ -9,7 +9,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.Map.Entry; -import java.util.function.Consumer; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -69,9 +69,11 @@ public class ShopManagerImpl implements ShopManager { * Destoys all shops and clears the cache */ public void clearShops() { - shopsInWorld.values().stream().flatMap(map -> map.values().stream()) - .forEach(shop -> ((ShopImpl) shop).destroy()); - shopsInWorld.clear(); + synchronized (shopsInWorld) { + shopsInWorld.values().stream().flatMap(map -> map.values().stream()) + .forEach(shop -> ((ShopImpl) shop).destroy()); + shopsInWorld.clear(); + } } /** @@ -79,56 +81,52 @@ public class ShopManagerImpl implements ShopManager { *

* This will fire a {@link ShopLoadedEvent}. * - * @param chunks a collection + * @param chunks the chunks whose shops will be loaded + * @return a completable future returning the loaded shops */ - public void loadShops(Chunk[] chunks, Consumer> callback, Consumer errorCallback) { - plugin.getDatabase().connect( - amount -> { - plugin.getDatabase().getShops(chunks, - shops -> { - for (Iterator it = shops.iterator(); it.hasNext();) { - Shop shop = it.next(); + public CompletableFuture> loadShops(Chunk[] chunks) { + return plugin.getDatabase().getShops(chunks).thenApply(shops -> { + for (Iterator it = shops.iterator(); it.hasNext();) { + Shop shop = it.next(); - if (getShop(shop.getLocation()).isPresent()) { - // A shop is already loaded at the location, which should be the same. - it.remove(); - continue; - } + if (getShop(shop.getLocation()).isPresent()) { + // A shop is already loaded at the location, which should be the same. + it.remove(); + continue; + } - String worldName = shop.getWorld().getName(); - if (!shopsInWorld.containsKey(worldName)) { - shopsInWorld.put(worldName, new HashMap<>()); - } + synchronized (shopsInWorld) { + String worldName = shop.getWorld().getName(); + if (!shopsInWorld.containsKey(worldName)) { + shopsInWorld.put(worldName, new HashMap<>()); + } - ((ShopImpl) shop).create(); + ((ShopImpl) shop).create(); - shopsInWorld.get(worldName).put(toBlockLocation(shop.getLocation()), shop); - ((ShopImpl) shop).getOtherLocation().ifPresent(otherLoc -> - shopsInWorld.get(worldName).put(toBlockLocation(otherLoc), shop)); - } - - callback.accept(Collections.unmodifiableCollection(shops)); - plugin.getServer().getPluginManager().callEvent(new ShopLoadedEvent(shops)); - }, - errorCallback - ); - }, - errorCallback - ); + shopsInWorld.get(worldName).put(toBlockLocation(shop.getLocation()), shop); + ((ShopImpl) shop).getOtherLocation().ifPresent(otherLoc -> + shopsInWorld.get(worldName).put(toBlockLocation(otherLoc), shop)); + } + } + + Collection unmodifiableShops = Collections.unmodifiableCollection(shops); + plugin.getServer().getPluginManager().callEvent(new ShopLoadedEvent(unmodifiableShops)); + return unmodifiableShops; + }); } /** * Loads all players' shop amounts from the database + * + * @return a completable future returning nothing */ - public void loadShopAmounts(Consumer> callback, Consumer errorCallback) { - plugin.getDatabase().getShopAmounts( - shopAmounts -> { + public CompletableFuture loadShopAmounts() { + return plugin.getDatabase().getShopAmounts().thenAccept(shopAmounts -> { + synchronized (shopAmounts) { this.shopAmounts.clear(); shopAmounts.forEach((uuid, amount) -> this.shopAmounts.put(uuid, new Counter(amount))); - callback.accept(shopAmounts); - }, - errorCallback - ); + } + }); } /** @@ -139,15 +137,19 @@ public class ShopManagerImpl implements ShopManager { * @see ShopPlayer#getShopAmount() */ public int getShopAmount(OfflinePlayer player) { - return shopAmounts.getOrDefault(player.getUniqueId(), new Counter()).get(); + synchronized (shopAmounts) { + return shopAmounts.getOrDefault(player.getUniqueId(), new Counter()).get(); + } } /* API Implementation */ @Override public Collection getShops() { - return shopsInWorld.values().stream().flatMap((map) -> map.values().stream()).distinct() - .collect(Collectors.toList()); + synchronized (shopsInWorld) { + return shopsInWorld.values().stream().flatMap((map) -> map.values().stream()).distinct() + .collect(Collectors.toList()); + } } @Override @@ -161,8 +163,10 @@ public class ShopManagerImpl implements ShopManager { return Optional.empty(); } - return Optional.ofNullable(shopsInWorld.get(location.getWorld().getName())) - .map(map -> map.get(toBlockLocation(location))); + synchronized (shopsInWorld) { + return Optional.ofNullable(shopsInWorld.get(location.getWorld().getName())) + .map(map -> map.get(toBlockLocation(location))); + } } @Override @@ -174,72 +178,79 @@ public class ShopManagerImpl implements ShopManager { @Override public Collection getShops(World world) { - if (!shopsInWorld.containsKey(world.getName())) { - return new ArrayList<>(); - } + synchronized (shopsInWorld) { + if (!shopsInWorld.containsKey(world.getName())) { + return new ArrayList<>(); + } - return shopsInWorld.get(world.getName()).values().stream().distinct().collect(Collectors.toList()); + return shopsInWorld.get(world.getName()).values().stream().distinct().collect(Collectors.toList()); + } } @Override - public void addShop(OfflinePlayer vendor, ShopProduct product, Location location, double buyPrice, double sellPrice, Consumer callback, Consumer errorCallback) { - Shop shop = new ShopImpl(vendor, product, location, buyPrice, sellPrice); - plugin.getDatabase().addShop(shop, - id -> { - ((ShopImpl) shop).create(); + public CompletableFuture addShop(OfflinePlayer vendor, ShopProduct product, Location location, double buyPrice, double sellPrice) { + ShopImpl shop = new ShopImpl(vendor, product, location, buyPrice, sellPrice); + return plugin.getDatabase().addShop(shop).thenApply(id -> { + shop.create(); + shop.setId(id); + synchronized (shopsInWorld) { String worldName = location.getWorld().getName(); if (!shopsInWorld.containsKey(worldName)) { shopsInWorld.put(worldName, new HashMap<>()); } shopsInWorld.get(worldName).put(toBlockLocation(location), shop); - ((ShopImpl) shop).getOtherLocation().ifPresent(otherLoc -> + shop.getOtherLocation().ifPresent(otherLoc -> shopsInWorld.get(worldName).put(toBlockLocation(otherLoc), shop)); + } - if (vendor != null) { + if (vendor != null) { + synchronized (shopAmounts) { shopAmounts.compute(vendor.getUniqueId(), (uuid, counter) -> { return counter == null ? new Counter(1) : counter.increment(); }); } + } - callback.accept(shop); - }, - errorCallback - ); + return shop; + }); } @Override - public void addAdminShop(ShopProduct product, Location location, double buyPrice, double sellPrice, Consumer callback, Consumer errorCallback) { - addShop(null, product, location, buyPrice, sellPrice, callback, errorCallback); + public CompletableFuture addAdminShop(ShopProduct product, Location location, double buyPrice, double sellPrice) { + return addShop(null, product, location, buyPrice, sellPrice); } @Override - public void removeShop(Shop shop, Runnable callback, Consumer errorCallback) { - ((ShopImpl) shop).destroy(); - shopsInWorld.get(shop.getWorld().getName()).remove(shop.getLocation()); - - getRemainingShopLocation(shop.getId(), shop.getWorld()).ifPresent(otherLoc -> - shopsInWorld.get(shop.getWorld().getName()).remove(otherLoc)); + public CompletableFuture removeShop(Shop shop) { + return plugin.getDatabase().removeShop(shop).thenRun(() -> { + ((ShopImpl) shop).destroy(); + synchronized (shopsInWorld) { + shopsInWorld.get(shop.getWorld().getName()).remove(shop.getLocation()); - shop.getVendor().ifPresent(vendor -> { - shopAmounts.compute(vendor.getUniqueId(), (uuid, counter) -> { - return counter == null ? new Counter() : counter.decrement(); + getRemainingShopLocation(shop.getId(), shop.getWorld()).ifPresent(otherLoc -> + shopsInWorld.get(shop.getWorld().getName()).remove(otherLoc)); + } + + shop.getVendor().ifPresent(vendor -> { + synchronized (shopAmounts) { + shopAmounts.compute(vendor.getUniqueId(), (uuid, counter) -> { + return counter == null ? new Counter() : counter.decrement(); + }); + } }); }); - - plugin.getDatabase().removeShop(shop, callback, errorCallback); } @Override - public void reloadShops(Consumer callback, Consumer errorCallback) { + public CompletableFuture> reloadShops() { + clearShops(); + Chunk[] chunks = plugin.getServer().getWorlds().stream().map(World::getLoadedChunks) .flatMap(Stream::of).toArray(Chunk[]::new); - loadShops(chunks, - shops -> callback.accept(shops.size()), - errorCallback - ); + return loadShops(chunks); } } \ No newline at end of file diff --git a/core/src/main/java/de/epiceric/shopchest/database/Database.java b/core/src/main/java/de/epiceric/shopchest/database/Database.java index 0fdaf7e..2d08450 100644 --- a/core/src/main/java/de/epiceric/shopchest/database/Database.java +++ b/core/src/main/java/de/epiceric/shopchest/database/Database.java @@ -29,14 +29,13 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Base64; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.function.Consumer; +import java.util.concurrent.CompletableFuture; import com.zaxxer.hikari.HikariDataSource; @@ -58,36 +57,6 @@ public abstract class Database { this.plugin = plugin; } - protected void callSyncResult(Runnable callback) { - if (callback == null) return; - - if (plugin.getServer().isPrimaryThread()) { - callback.run(); - } else { - plugin.getServer().getScheduler().runTask(plugin, () -> callback.run()); - } - } - - protected void callSyncResult(Consumer callback, T result) { - if (callback == null) return; - - if (plugin.getServer().isPrimaryThread()) { - callback.accept(result); - } else { - plugin.getServer().getScheduler().runTask(plugin, () -> callback.accept(result)); - } - } - - protected void callSyncError(Consumer callback, Throwable error) { - if (callback == null) return; - - if (plugin.getServer().isPrimaryThread()) { - callback.accept(error); - } else { - plugin.getServer().getScheduler().runTask(plugin, () -> callback.accept(error)); - } - } - abstract HikariDataSource getDataSource(); abstract String getQueryCreateTableShops(); @@ -284,15 +253,16 @@ public abstract class Database { } /** - *

(Re-)Connects to the the database and initializes it.

+ * Connects or reconnects to the the database and initializes it + *

+ * All tables are created if necessary. If the database structure has to be updated, + * that is done as well. * - * All tables are created if necessary and if the database - * structure has to be updated, that is done as well. - * - * @param callback Callback that - if succeeded - returns the amount of shops - * that were found (as {@code int}) + * @return a completable future that returns the amount of shops found in the database */ - public void connect(Consumer callback, Consumer errorCallback) { + public CompletableFuture connect() { + CompletableFuture result = new CompletableFuture<>(); + if (!Config.DATABASE_TABLE_PREFIX.get().matches("^([a-zA-Z0-9\\-\\_]+)?$")) { // Only letters, numbers dashes and underscores are allowed Logger.severe("Database table prefix contains illegal letters, using 'shopchest_' prefix."); @@ -312,14 +282,14 @@ public abstract class Database { try { dataSource = getDataSource(); } catch (Exception e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); return; } if (dataSource == null) { Exception e = new IllegalStateException("Data source is null"); Logger.severe("Failed to get data source: {0}", e.getMessage()); - callSyncError(errorCallback, e); + result.completeExceptionally(e); return; } @@ -360,76 +330,83 @@ public abstract class Database { if (rs.next()) { int count = rs.getInt(1); initialized = true; - callSyncResult(callback, count); + result.complete(count); } else { throw new SQLException("Count result set has no entries"); } } } catch (SQLException e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); Logger.severe("Failed to initialize or connect to database"); Logger.severe(e); } }); + + return result; } /** - * Remove a shop from the database + * Removes the given shop from the database * - * @param shop Shop to remove - * @param callback Callback that - if succeeded - returns {@code null} + * @param shop the shop to remove + * @return a completable future returning nothing */ - public void removeShop(Shop shop, Runnable callback, Consumer errorCallback) { + public CompletableFuture removeShop(Shop shop) { + CompletableFuture result = new CompletableFuture<>(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { try (Connection con = dataSource.getConnection(); PreparedStatement ps = con.prepareStatement("DELETE FROM " + tableShops + " WHERE id = ?")) { ps.setInt(1, shop.getId()); ps.executeUpdate(); - callSyncResult(callback); + result.complete(null); } catch (SQLException e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); Logger.severe("Failed to remove shop from database"); Logger.severe(e); } }); + + return result; } /** - * Get shop amounts for each player + * Gets shop amounts for each player * - * @param callback Callback that returns a map of each player's shop amount + * @return a completable future returning a map of each player's shop amounts */ - public void getShopAmounts(Consumer> callback, Consumer errorCallback) { + public CompletableFuture> getShopAmounts() { + CompletableFuture> result = new CompletableFuture<>(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { try (Connection con = dataSource.getConnection(); Statement s = con.createStatement()) { ResultSet rs = s.executeQuery("SELECT vendor, COUNT(*) AS count FROM " + tableShops + " WHERE shoptype = 'NORMAL' GROUP BY vendor"); - Map result = new HashMap<>(); + Map shopAmounts = new HashMap<>(); while (rs.next()) { UUID uuid = UUID.fromString(rs.getString("vendor")); - result.put(uuid, rs.getInt("count")); + shopAmounts.put(uuid, rs.getInt("count")); } - callSyncResult(callback, result); + result.complete(shopAmounts); } catch (SQLException e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); Logger.severe("Failed to get shop amounts from database"); Logger.severe(e); - } + } }); + + return result; } /** - * Get all shops from the database + * Gets the shops in the given chunks from the database * - * @param showConsoleMessages Whether console messages (errors or warnings) - * should be shown - * @param callback Callback that - if succeeded - returns a read-only - * collection of all shops (as - * {@code Collection}) + * @param chunks the chunks whose shops will be retrieved + * @return a completable future returning the shops */ - public void getShops(Chunk[] chunks, Consumer> callback, Consumer errorCallback) { + public CompletableFuture> getShops(Chunk[] chunks) { // Split chunks into packages containing each {splitSize} chunks at max int splitSize = 80; int parts = (int) Math.ceil(chunks.length / (double) splitSize); @@ -441,6 +418,8 @@ public abstract class Database { splitChunks[i] = tmp; } + CompletableFuture> result = new CompletableFuture<>(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { List shops = new ArrayList<>(); @@ -517,7 +496,7 @@ public abstract class Database { shops.add(new ShopImpl(id, vendor, product, location, buyPrice, sellPrice)); } } catch (SQLException e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); Logger.severe("Failed to get shops from database"); Logger.severe(e); @@ -525,21 +504,24 @@ public abstract class Database { } } - callSyncResult(callback, Collections.unmodifiableCollection(shops)); + result.complete(shops); }); + + return result; } /** - * Adds a shop to the database + * Adds the given shop to the database * - * @param shop Shop to add - * @param callback Callback that - if succeeded - returns the ID the shop was - * given (as {@code int}) + * @param shop the shop to add + * @return a completable future returning the added shop's ID */ - public void addShop(Shop shop, Consumer callback, Consumer errorCallback) { + public CompletableFuture addShop(Shop shop) { final String queryNoId = "REPLACE INTO " + tableShops + " (vendor,product,amount,world,x,y,z,buyprice,sellprice,shoptype) VALUES(?,?,?,?,?,?,?,?,?,?)"; final String queryWithId = "REPLACE INTO " + tableShops + " (id,vendor,product,amount,world,x,y,z,buyprice,sellprice,shoptype) VALUES(?,?,?,?,?,?,?,?,?,?,?)"; + CompletableFuture result = new CompletableFuture<>(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { String query = shop.getId() != -1 ? queryWithId : queryNoId; @@ -572,24 +554,26 @@ public abstract class Database { ((ShopImpl) shop).setId(shopId); } - callSyncResult(callback, shop.getId()); + result.complete(shop.getId()); } catch (SQLException e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); Logger.severe("Failed to add shop to database"); Logger.severe(e); } }); + + return result; } /** - * Updates a shop in the database + * Updates the given shop in the database * * @param shop shop to update - * @param callback callback that runs on success - * @param errorCallback callback that returns an error if one occurred + * @return a completable future that returns nothing */ - public void updateShop(Shop shop, Runnable callback, Consumer errorCallback) { + public CompletableFuture updateShop(Shop shop) { final String query = "REPLACE INTO " + tableShops + " (id,vendor,product,amount,world,x,y,z,buyprice,sellprice,shoptype) VALUES(?,?,?,?,?,?,?,?,?,?,?)"; + CompletableFuture result = new CompletableFuture<>(); plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { try (Connection con = dataSource.getConnection(); @@ -607,34 +591,37 @@ public abstract class Database { ps.setString(11, shop.isAdminShop() ? "ADMIN" : "NORMAL"); ps.executeUpdate(); - callSyncResult(callback); + result.complete(null); } catch (SQLException e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); Logger.severe("Failed to update shop in database"); Logger.severe(e); } }); + + return result; } /** - * Log an economy transaction to the database + * Logs an economy transaction to the database * - * @param executor Player who bought/sold something - * @param shop The {@link Shop} the player bought from or sold to - * @param product The {@link ItemStack} that was bought/sold - * @param price The price the product was bought or sold for - * @param type Whether the executor bought or sold - * @param callback Callback that - if succeeded - returns {@code null} + * @param executor the player who bought/sold something + * @param shop the shop the player bought from or sold to + * @param product the item that was bought/sold + * @param price the price the product was bought or sold for + * @param type whether the executor bought or sold + * @return a completable future returning nothing */ - public void logEconomy(Player executor, Shop shop, ShopProduct product, double price, Type type, Runnable callback, Consumer errorCallback) { + public CompletableFuture logEconomy(Player executor, Shop shop, ShopProduct product, double price, Type type) { if (!Config.ECONOMY_LOG_ENABLE.get()) { - callSyncResult(callback); - return; + return CompletableFuture.completedFuture(null); } final String query = "INSERT INTO " + tableLogs + " (shop_id,timestamp,time,player_name,player_uuid,product_name,product,amount," + "vendor_name,vendor_uuid,admin,world,x,y,z,price,type) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + CompletableFuture result = new CompletableFuture<>(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { try (Connection con = dataSource.getConnection(); PreparedStatement ps = con.prepareStatement(query)) { @@ -660,23 +647,29 @@ public abstract class Database { ps.setString(17, type.toString()); ps.executeUpdate(); - callSyncResult(callback); + result.complete(null); } catch (SQLException e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); Logger.severe("Failed to log economy transaction to database"); Logger.severe(e); } }); + + return result; } /** * Cleans up the economy log to reduce file size + * + * @return a completable future returning nothing */ - public void cleanUpEconomy() { + public CompletableFuture cleanUpEconomy() { if (!Config.ECONOMY_LOG_CLEANUP.get()) { - return; + return CompletableFuture.completedFuture(null); } + CompletableFuture result = new CompletableFuture<>(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { int cleanupDays = Config.ECONOMY_LOG_CLEANUP_DAYS.get(); long time = System.currentTimeMillis() - cleanupDays * 86400000L; @@ -689,22 +682,27 @@ public abstract class Database { s.executeUpdate(queryCleanUpLog); s2.executeUpdate(queryCleanUpPlayers); Logger.info("Cleaned up economy log entries older than {0} days", cleanupDays); + result.complete(null); } catch (SQLException e) { + result.completeExceptionally(e); Logger.severe("Failed to clean up economy log"); Logger.severe(e); } }); + + return result; } /** - * Get the revenue a player got while he was offline + * Gets the revenue a player got while he was offline * - * @param player Player whose revenue to get - * @param logoutTime Time in milliseconds when he logged out the last time - * @param callback Callback that - if succeeded - returns the revenue the - * player made while offline (as {@code double}) + * @param player the player whose revenue to get + * @param logoutTime the system time in milliseconds when he logged out the last time + * @return a completable future returning the revenue */ - public void getRevenue(Player player, long logoutTime, Consumer callback, Consumer errorCallback) { + public CompletableFuture getRevenue(Player player, long logoutTime) { + CompletableFuture result = new CompletableFuture<>(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { double revenue = 0; @@ -727,58 +725,66 @@ public abstract class Database { } } - callSyncResult(callback, revenue); + result.complete(revenue); } catch (SQLException e) { - callSyncError(errorCallback, e); + result.complete(null); Logger.severe("Failed to get revenue of {0} from database", player.getName()); Logger.severe(e); } }); + + return result; } /** - * Log a logout to the database + * Logs the given player's logout to the database * - * @param player Player who logged out - * @param callback Callback that - if succeeded - returns {@code null} + * @param player the player who logged out + * @return a completable future returning nothing */ - public void logLogout(Player player, Runnable callback, Consumer errorCallback) { + public CompletableFuture logLogout(Player player) { + CompletableFuture result = new CompletableFuture<>(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { try (Connection con = dataSource.getConnection(); PreparedStatement ps = con.prepareStatement("REPLACE INTO " + tableLogouts + " (player,time) VALUES(?,?)")) { ps.setString(1, player.getUniqueId().toString()); ps.setLong(2, System.currentTimeMillis()); ps.executeUpdate(); - callSyncResult(callback); + result.complete(null); } catch (SQLException e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); Logger.severe("Failed to log last logout to database"); Logger.severe(e); } }); + + return result; } /** - * Get the last logout of a player + * Gets the system time in milliseconds when the given player logged out last * - * @param player Player who logged out - * @param callback Callback that - if succeeded - returns the time in - * milliseconds the player logged out (as {@code long}) - * or {@code -1} if the player has not logged out yet. + * @param player the player who logged out + * @return a completable future returning the time or -1 if no recent logout has been found */ - public void getLastLogout(Player player, Consumer callback, Consumer errorCallback) { + public CompletableFuture getLastLogout(Player player) { + CompletableFuture result = new CompletableFuture<>(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { try (Connection con = dataSource.getConnection(); PreparedStatement ps = con.prepareStatement("SELECT * FROM " + tableLogouts + " WHERE player = ?")) { ps.setString(1, player.getUniqueId().toString()); ResultSet rs = ps.executeQuery(); - callSyncResult(callback, rs.next() ? rs.getLong("time") : -1L); + result.complete(rs.next() ? rs.getLong("time") : -1L); } catch (SQLException e) { - callSyncError(errorCallback, e); + result.completeExceptionally(e); Logger.severe("Failed to get last logout from database"); Logger.severe(e); } }); + + return result; } /** diff --git a/core/src/main/java/de/epiceric/shopchest/database/MySQL.java b/core/src/main/java/de/epiceric/shopchest/database/MySQL.java index 1f702ec..9739943 100644 --- a/core/src/main/java/de/epiceric/shopchest/database/MySQL.java +++ b/core/src/main/java/de/epiceric/shopchest/database/MySQL.java @@ -43,7 +43,7 @@ public class MySQL extends Database { s.execute("/* ping */ SELECT 1"); } catch (SQLException ex) { Logger.severe("Failed to ping MySQL server. Trying to reconnect..."); - connect(null, null); + connect(); } } }.runTaskAsynchronously(plugin); diff --git a/core/src/main/java/de/epiceric/shopchest/listener/ChunkLoadListener.java b/core/src/main/java/de/epiceric/shopchest/listener/ChunkLoadListener.java index 9cac181..23cd5c7 100644 --- a/core/src/main/java/de/epiceric/shopchest/listener/ChunkLoadListener.java +++ b/core/src/main/java/de/epiceric/shopchest/listener/ChunkLoadListener.java @@ -35,13 +35,14 @@ public class ChunkLoadListener implements Listener { if (newLoadedChunks.isEmpty()) { plugin.getServer().getScheduler().runTaskLater(plugin, () -> { int chunkCount = newLoadedChunks.size(); - ((ShopManagerImpl) plugin.getShopManager()).loadShops(newLoadedChunks.toArray(new Chunk[chunkCount]), - shops -> {}, - error -> { - Logger.severe("Failed to load shops in newly loaded chunks"); - Logger.severe(error); - } - ); + + ((ShopManagerImpl) plugin.getShopManager()).loadShops(newLoadedChunks.toArray(new Chunk[chunkCount])) + .exceptionally(ex -> { + Logger.severe("Failed to load shops in newly loaded chunks"); + Logger.severe(ex); + return null; + }); + newLoadedChunks.clear(); }, 10L); } diff --git a/core/src/main/java/de/epiceric/shopchest/listener/internal/monitor/ShopInteractMonitorListener.java b/core/src/main/java/de/epiceric/shopchest/listener/internal/monitor/ShopInteractMonitorListener.java index 3e11468..dce03a7 100644 --- a/core/src/main/java/de/epiceric/shopchest/listener/internal/monitor/ShopInteractMonitorListener.java +++ b/core/src/main/java/de/epiceric/shopchest/listener/internal/monitor/ShopInteractMonitorListener.java @@ -46,15 +46,23 @@ public class ShopInteractMonitorListener implements Listener { public void onShopCreate(ShopCreateEvent e) { Shop shop = e.getShop(); if (shop.isAdminShop()) { - plugin.getShopManager().addAdminShop(shop.getProduct(), shop.getLocation(), shop.getBuyPrice(), shop.getSellPrice(), - newShop -> e.getPlayer().sendMessage("§aAdmin shop has been added with ID {0}.", newShop.getId()), // TODO: i18n - error -> e.getPlayer().sendMessage("§cFailed to add admin shop: {0}", error.getMessage()) - ); + plugin.getShopManager().addAdminShop(shop.getProduct(), shop.getLocation(), shop.getBuyPrice(), shop.getSellPrice()) + .thenAccept(newShop -> e.getPlayer().sendMessage("§aAdmin shop has been added with ID {0}.", newShop.getId())) + .exceptionally(ex -> { // TODO: i18n + Logger.severe("Failed to add admin shop"); + Logger.severe(ex); + e.getPlayer().sendMessage("§cFailed to add admin shop: {0}", ex.getMessage()); + return null; + }); } else { - plugin.getShopManager().addShop(shop.getVendor().get(), shop.getProduct(), shop.getLocation(), shop.getBuyPrice(), shop.getSellPrice(), - newShop -> e.getPlayer().sendMessage("§aShop has been added with ID {0}.", newShop.getId()), // TODO: i18n - error -> e.getPlayer().sendMessage("§cFailed to add shop: {0}", error.getMessage()) - ); + plugin.getShopManager().addShop(shop.getVendor().get(), shop.getProduct(), shop.getLocation(), shop.getBuyPrice(), shop.getSellPrice()) + .thenAccept(newShop -> e.getPlayer().sendMessage("§aShop has been added with ID {0}.", newShop.getId())) + .exceptionally(ex -> { // TODO: i18n + Logger.severe("Failed to add shop"); + Logger.severe(ex); + e.getPlayer().sendMessage("§cFailed to add shop: {0}", ex.getMessage()); + return null; + }); } } @@ -65,18 +73,26 @@ public class ShopInteractMonitorListener implements Listener { shop.setSellPrice(e.getSellPrice()); shop.setProduct(new ShopProductImpl(e.getItemStack(), e.getAmount())); - ((ShopChestImpl) plugin).getDatabase().updateShop(shop, - () -> e.getPlayer().sendMessage("§aShop has been edited."), // TODO: i18n - error -> e.getPlayer().sendMessage("§cFailed to save edit: {0}", error.getMessage()) - ); + ((ShopChestImpl) plugin).getDatabase().updateShop(shop) + .thenRun(() -> e.getPlayer().sendMessage("§aShop has been edited.")) // TODO: i18n + .exceptionally(ex -> { + Logger.severe("Failed to save shop edit"); + Logger.severe(ex); + e.getPlayer().sendMessage("§cFailed to save edit: {0}", ex.getMessage()); + return null; + }); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onShopRemove(ShopRemoveEvent e) { - plugin.getShopManager().removeShop(e.getShop(), - () -> e.getPlayer().sendMessage("§aShop has been removed."), // TODO: i18n - error -> e.getPlayer().sendMessage("§cFailed to remove shop: {0}", error.getMessage()) - ); + ((ShopChestImpl) plugin).getDatabase().removeShop(e.getShop()) + .thenRun(() -> e.getPlayer().sendMessage("§aShop has been removed.")) // TODO: i18n + .exceptionally(ex -> { + Logger.severe("Failed to remove shop"); + Logger.severe(ex); + e.getPlayer().sendMessage("§cFailed to remove shop: {0}", ex.getMessage()); + return null; + }); } private String getProductJson(ShopProduct product) {