Query and update SQL database asynchronously

This commit is contained in:
Eric 2017-02-08 17:02:03 +01:00
parent 248cf08811
commit 7b3dac61ac
13 changed files with 588 additions and 332 deletions

View File

@ -226,7 +226,7 @@ public class ShopChest extends JavaPlugin {
ShopReloadEvent event = new ShopReloadEvent(Bukkit.getConsoleSender()); ShopReloadEvent event = new ShopReloadEvent(Bukkit.getConsoleSender());
Bukkit.getServer().getPluginManager().callEvent(event); Bukkit.getServer().getPluginManager().callEvent(event);
if (!event.isCancelled()) shopUtils.reloadShops(true, false); if (!event.isCancelled()) shopUtils.reloadShops(true, false, null);
} }
}, config.auto_reload_time * 20, config.auto_reload_time * 20); }, config.auto_reload_time * 20, config.auto_reload_time * 20);
} }
@ -288,17 +288,17 @@ public class ShopChest extends JavaPlugin {
@Override @Override
public void onDisable() { public void onDisable() {
debug("Disabling ShopChest..."); debug("Disabling ShopChest...", true);
if (updater != null) { if (updater != null) {
debug("Stopping updater"); debug("Stopping updater", true);
updater.cancel(); updater.cancel();
} }
if (database != null) { if (database != null) {
for (Shop shop : shopUtils.getShops()) { for (Shop shop : shopUtils.getShops()) {
shopUtils.removeShop(shop, false); shopUtils.removeShop(shop, false, true);
debug("Removed shop (#" + shop.getID() + ")"); debug("Removed shop (#" + shop.getID() + ")", true);
} }
database.disconnect(); database.disconnect();
@ -314,6 +314,26 @@ public class ShopChest extends JavaPlugin {
} }
} }
/**
* Print a message to the <i>/plugins/ShopChest/debug.txt</i> file
* @param message Message to print
* @param useCurrentThread Whether the current thread should be used. If set to false, a new synchronized task will be created.
*/
public void debug(final String message, boolean useCurrentThread) {
if (config.enable_debug_log && fw != null) {
if (useCurrentThread) {
debug(message);
} else {
new BukkitRunnable() {
@Override
public void run() {
debug(message);
}
}.runTask(this);
}
}
}
/** /**
* Print a message to the <i>/plugins/ShopChest/debug.txt</i> file * Print a message to the <i>/plugins/ShopChest/debug.txt</i> file
* @param message Message to print * @param message Message to print
@ -332,6 +352,28 @@ public class ShopChest extends JavaPlugin {
} }
} }
/**
* Print a {@link Throwable}'s stacktrace to the <i>/plugins/ShopChest/debug.txt</i> file
* @param throwable {@link Throwable} whose stacktrace will be printed
* @param useCurrentThread Whether the current thread should be used. If set to false, a new synchronized task will be created.
*/
public void debug(final Throwable throwable, boolean useCurrentThread) {
if (config.enable_debug_log && fw != null) {
if (useCurrentThread) {
debug(throwable);
} else {
new BukkitRunnable() {
@Override
public void run() {
PrintWriter pw = new PrintWriter(fw);
throwable.printStackTrace(pw);
pw.flush();
}
}.runTask(this);
}
}
}
/** /**
* Print a {@link Throwable}'s stacktrace to the <i>/plugins/ShopChest/debug.txt</i> file * Print a {@link Throwable}'s stacktrace to the <i>/plugins/ShopChest/debug.txt</i> file
* @param throwable {@link Throwable} whose stacktrace will be printed * @param throwable {@link Throwable} whose stacktrace will be printed
@ -350,9 +392,17 @@ public class ShopChest extends JavaPlugin {
*/ */
private void initializeShops() { private void initializeShops() {
debug("Initializing Shops..."); debug("Initializing Shops...");
int count = shopUtils.reloadShops(false, false); shopUtils.reloadShops(false, false, new Callback(this) {
getLogger().info("Initialized " + count + " Shops"); @Override
debug("Initialized " + count + " Shops"); public void onResult(Object result) {
if (result instanceof Integer) {
int count = (int) result;
getLogger().info("Initialized " + count + " Shops");
debug("Initialized " + count + " Shops");
}
}
});
} }
/** /**

View File

@ -241,7 +241,7 @@ class ShopCommand implements CommandExecutor {
* A given player reloads the shops * A given player reloads the shops
* @param sender The command executor * @param sender The command executor
*/ */
private void reload(CommandSender sender) { private void reload(final CommandSender sender) {
plugin.debug(sender.getName() + " is reloading the shops"); plugin.debug(sender.getName() + " is reloading the shops");
ShopReloadEvent event = new ShopReloadEvent(sender); ShopReloadEvent event = new ShopReloadEvent(sender);
@ -251,9 +251,17 @@ class ShopCommand implements CommandExecutor {
return; return;
} }
int count = shopUtils.reloadShops(true, true); shopUtils.reloadShops(true, true, new Callback(plugin) {
plugin.debug(sender.getName() + " has reloaded " + count + " shops"); @Override
sender.sendMessage(LanguageUtils.getMessage(LocalizedMessage.Message.RELOADED_SHOPS, new LocalizedMessage.ReplacedRegex(Regex.AMOUNT, String.valueOf(count)))); public void onResult(Object result) {
if (result instanceof Integer) {
int count = (int) result;
sender.sendMessage(LanguageUtils.getMessage(LocalizedMessage.Message.RELOADED_SHOPS, new LocalizedMessage.ReplacedRegex(Regex.AMOUNT, String.valueOf(count))));
plugin.debug(sender.getName() + " has reloaded " + count + " shops");
}
}
});
} }
/** /**

View File

@ -0,0 +1,9 @@
package de.epiceric.shopchest.exceptions;
public class ChestNotFoundException extends Exception {
public ChestNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,9 @@
package de.epiceric.shopchest.exceptions;
public class NotEnoughSpaceException extends Exception {
public NotEnoughSpaceException(String message) {
super(message);
}
}

View File

@ -0,0 +1,9 @@
package de.epiceric.shopchest.exceptions;
public class WorldNotFoundException extends Exception {
public WorldNotFoundException(String message) {
super(message);
}
}

View File

@ -16,6 +16,7 @@ import de.epiceric.shopchest.utils.ShopUtils;
import de.epiceric.shopchest.utils.Utils; import de.epiceric.shopchest.utils.Utils;
import de.epiceric.shopchest.worldguard.ShopFlag; import de.epiceric.shopchest.worldguard.ShopFlag;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockFace; import org.bukkit.block.BlockFace;
@ -62,13 +63,9 @@ public class ChestProtectListener implements Listener {
Bukkit.getScheduler().runTaskLater(plugin, new Runnable() { Bukkit.getScheduler().runTaskLater(plugin, new Runnable() {
@Override @Override
public void run() { public void run() {
Shop newShop = null; Location loc = (b.getLocation().equals(l.getLocation()) ? r.getLocation() : l.getLocation());
Shop newShop = new Shop(shop.getID(), plugin, shop.getVendor(), shop.getProduct(), loc, shop.getBuyPrice(), shop.getSellPrice(), shop.getShopType());
if (b.getLocation().equals(l.getLocation())) newShop.create();
newShop = new Shop(shop.getID(), plugin, shop.getVendor(), shop.getProduct(), r.getLocation(), shop.getBuyPrice(), shop.getSellPrice(), shop.getShopType());
else if (b.getLocation().equals(r.getLocation()))
newShop = new Shop(shop.getID(), plugin, shop.getVendor(), shop.getProduct(), l.getLocation(), shop.getBuyPrice(), shop.getSellPrice(), shop.getShopType());
shopUtils.addShop(newShop, true); shopUtils.addShop(newShop, true);
} }
}, 1L); }, 1L);
@ -170,6 +167,7 @@ public class ChestProtectListener implements Listener {
if (b.getRelative(BlockFace.UP).getType() == Material.AIR) { if (b.getRelative(BlockFace.UP).getType() == Material.AIR) {
shopUtils.removeShop(shop, true); shopUtils.removeShop(shop, true);
Shop newShop = new Shop(shop.getID(), ShopChest.getInstance(), shop.getVendor(), shop.getProduct(), shop.getLocation(), shop.getBuyPrice(), shop.getSellPrice(), shop.getShopType()); Shop newShop = new Shop(shop.getID(), ShopChest.getInstance(), shop.getVendor(), shop.getProduct(), shop.getLocation(), shop.getBuyPrice(), shop.getSellPrice(), shop.getShopType());
newShop.create();
shopUtils.addShop(newShop, true); shopUtils.addShop(newShop, true);
plugin.debug(String.format("%s extended %s's shop (#%d)", p.getName(), shop.getVendor().getName(), shop.getID())); plugin.debug(String.format("%s extended %s's shop (#%d)", p.getName(), shop.getVendor().getName(), shop.getID()));
} else { } else {

View File

@ -17,10 +17,7 @@ import de.epiceric.shopchest.nms.Hologram;
import de.epiceric.shopchest.shop.Shop; import de.epiceric.shopchest.shop.Shop;
import de.epiceric.shopchest.shop.Shop.ShopType; import de.epiceric.shopchest.shop.Shop.ShopType;
import de.epiceric.shopchest.sql.Database; import de.epiceric.shopchest.sql.Database;
import de.epiceric.shopchest.utils.ClickType; import de.epiceric.shopchest.utils.*;
import de.epiceric.shopchest.utils.Permissions;
import de.epiceric.shopchest.utils.ShopUtils;
import de.epiceric.shopchest.utils.Utils;
import de.epiceric.shopchest.worldguard.ShopFlag; import de.epiceric.shopchest.worldguard.ShopFlag;
import fr.xephi.authme.AuthMe; import fr.xephi.authme.AuthMe;
import net.milkbowl.vault.economy.Economy; import net.milkbowl.vault.economy.Economy;
@ -447,45 +444,56 @@ public class ShopInteractListener implements Listener {
* @param sellPrice Sell price * @param sellPrice Sell price
* @param shopType Type of the shop * @param shopType Type of the shop
*/ */
private void create(Player executor, Location location, ItemStack product, double buyPrice, double sellPrice, ShopType shopType) { private void create(final Player executor, final Location location, final ItemStack product, final double buyPrice, final double sellPrice, final ShopType shopType) {
plugin.debug(executor.getName() + " is creating new shop..."); plugin.debug(executor.getName() + " is creating new shop...");
int id = database.getNextFreeID(); database.getNextFreeID(new Callback(plugin) {
double creationPrice = (shopType == ShopType.NORMAL) ? config.shop_creation_price_normal : config.shop_creation_price_admin; @Override
public void onResult(Object result) {
if (result instanceof Integer) {
int id = (int) result;
double creationPrice = (shopType == ShopType.NORMAL) ? config.shop_creation_price_normal : config.shop_creation_price_admin;
Shop shop = new Shop(id, plugin, executor, product, location, buyPrice, sellPrice, shopType);
ShopCreateEvent event = new ShopCreateEvent(executor, Shop.createImaginaryShop(executor, product, location, buyPrice, sellPrice,shopType), creationPrice); ShopCreateEvent event = new ShopCreateEvent(executor, shop, creationPrice);
Bukkit.getPluginManager().callEvent(event); Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) { if (event.isCancelled()) {
plugin.debug("Create event cancelled (#" + id + ")"); plugin.debug("Create event cancelled (#" + id + ")");
return; return;
} }
EconomyResponse r = plugin.getEconomy().withdrawPlayer(executor, location.getWorld().getName(), creationPrice);
if (!r.transactionSuccess()) {
plugin.debug("Economy transaction failed: " + r.errorMessage);
executor.sendMessage(LanguageUtils.getMessage(LocalizedMessage.Message.ERROR_OCCURRED, new LocalizedMessage.ReplacedRegex(Regex.ERROR, r.errorMessage)));
return;
}
Shop shop = new Shop(id, plugin, executor, product, location, buyPrice, sellPrice, shopType); EconomyResponse r = plugin.getEconomy().withdrawPlayer(executor, location.getWorld().getName(), creationPrice);
if (!r.transactionSuccess()) {
plugin.debug("Economy transaction failed: " + r.errorMessage);
executor.sendMessage(LanguageUtils.getMessage(LocalizedMessage.Message.ERROR_OCCURRED, new LocalizedMessage.ReplacedRegex(Regex.ERROR, r.errorMessage)));
return;
}
plugin.debug("Shop created (#" + id + ")"); shop.create();
shopUtils.addShop(shop, true);
executor.sendMessage(LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_CREATED));
for (Player p : location.getWorld().getPlayers()) { plugin.debug("Shop created (#" + id + ")");
if (p.getLocation().distanceSquared(location) <= Math.pow(config.maximal_distance, 2)) { shopUtils.addShop(shop, true);
if (shop.getHologram() != null) { executor.sendMessage(LanguageUtils.getMessage(LocalizedMessage.Message.SHOP_CREATED));
shop.getHologram().showPlayer(p);
for (Player p : location.getWorld().getPlayers()) {
if (p.getLocation().distanceSquared(location) <= Math.pow(config.maximal_distance, 2)) {
if (shop.getHologram() != null) {
shop.getHologram().showPlayer(p);
}
}
if (p.getLocation().distanceSquared(location) <= Math.pow(config.maximal_item_distance, 2)) {
if (shop.getItem() != null) {
shop.getItem().setVisible(p, true);
}
}
}
} }
} }
if (p.getLocation().distanceSquared(location) <= Math.pow(config.maximal_item_distance, 2)) {
if (shop.getItem() != null) {
shop.getItem().setVisible(p, true);
}
}
}
@Override
public void onError(Throwable throwable) {}
});
} }
/** /**
@ -708,7 +716,7 @@ public class ShopInteractListener implements Listener {
return; return;
} }
database.logEconomy(executor, newProduct, shop.getVendor(), shop.getShopType(), shop.getLocation(), newPrice, ShopBuySellEvent.Type.BUY); database.logEconomy(executor, newProduct, shop.getVendor(), shop.getShopType(), shop.getLocation(), newPrice, ShopBuySellEvent.Type.BUY, null);
addToInventory(inventory, newProduct); addToInventory(inventory, newProduct);
removeFromInventory(c.getInventory(), newProduct); removeFromInventory(c.getInventory(), newProduct);
@ -742,7 +750,7 @@ public class ShopInteractListener implements Listener {
return; return;
} }
database.logEconomy(executor, newProduct, shop.getVendor(), shop.getShopType(), shop.getLocation(), newPrice, ShopBuySellEvent.Type.BUY); database.logEconomy(executor, newProduct, shop.getVendor(), shop.getShopType(), shop.getLocation(), newPrice, ShopBuySellEvent.Type.BUY, null);
addToInventory(inventory, newProduct); addToInventory(inventory, newProduct);
executor.updateInventory(); executor.updateInventory();
@ -847,7 +855,7 @@ public class ShopInteractListener implements Listener {
return; return;
} }
database.logEconomy(executor, newProduct, shop.getVendor(), shop.getShopType(), shop.getLocation(), newPrice, ShopBuySellEvent.Type.SELL); database.logEconomy(executor, newProduct, shop.getVendor(), shop.getShopType(), shop.getLocation(), newPrice, ShopBuySellEvent.Type.SELL, null);
addToInventory(inventory, newProduct); addToInventory(inventory, newProduct);
removeFromInventory(executor.getInventory(), newProduct); removeFromInventory(executor.getInventory(), newProduct);
@ -882,7 +890,7 @@ public class ShopInteractListener implements Listener {
return; return;
} }
database.logEconomy(executor, newProduct, shop.getVendor(), shop.getShopType(), shop.getLocation(), newPrice, ShopBuySellEvent.Type.SELL); database.logEconomy(executor, newProduct, shop.getVendor(), shop.getShopType(), shop.getLocation(), newPrice, ShopBuySellEvent.Type.SELL, null);
removeFromInventory(executor.getInventory(), newProduct); removeFromInventory(executor.getInventory(), newProduct);
executor.updateInventory(); executor.updateInventory();

View File

@ -150,29 +150,44 @@ public class Hologram {
/** /**
* @param p Player from which the hologram should be hidden * @param p Player from which the hologram should be hidden
*/ */
public void hidePlayer(final Player p) { public void hidePlayer(final Player p, boolean useCurrentThread) {
new BukkitRunnable() { if (useCurrentThread) {
@Override sendDestroyPackets(p);
public void run() { } else {
for (Object o : entityList) { new BukkitRunnable() {
try { @Override
int id = (int) entityArmorStandClass.getMethod("getId").invoke(o); public void run() {
sendDestroyPackets(p);
Object packet = packetPlayOutEntityDestroyClass.getConstructor(int[].class).newInstance((Object) new int[] {id});
Utils.sendPacket(plugin, packet, p);
} catch (NoSuchMethodException | InstantiationException | InvocationTargetException | IllegalAccessException e) {
plugin.getLogger().severe("Could not hide Hologram from player with reflection");
plugin.debug("Could not hide Hologram from player with reflection");
plugin.debug(e);
}
} }
} }.runTaskAsynchronously(plugin);
}.runTaskAsynchronously(plugin); }
visible.remove(p); visible.remove(p);
} }
private void sendDestroyPackets(Player p) {
for (Object o : entityList) {
try {
int id = (int) entityArmorStandClass.getMethod("getId").invoke(o);
Object packet = packetPlayOutEntityDestroyClass.getConstructor(int[].class).newInstance((Object) new int[] {id});
Utils.sendPacket(plugin, packet, p);
} catch (NoSuchMethodException | InstantiationException | InvocationTargetException | IllegalAccessException e) {
plugin.getLogger().severe("Could not hide Hologram from player with reflection");
plugin.debug("Could not hide Hologram from player with reflection");
plugin.debug(e);
}
}
}
/**
* @param p Player from which the hologram should be hidden
*/
public void hidePlayer(final Player p) {
hidePlayer(p, false);
}
/** /**
* @param p Player to check * @param p Player to check
* @return Whether the hologram is visible to the player * @return Whether the hologram is visible to the player

View File

@ -2,6 +2,8 @@ package de.epiceric.shopchest.shop;
import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.ShopChest;
import de.epiceric.shopchest.config.Regex; import de.epiceric.shopchest.config.Regex;
import de.epiceric.shopchest.exceptions.ChestNotFoundException;
import de.epiceric.shopchest.exceptions.NotEnoughSpaceException;
import de.epiceric.shopchest.language.LanguageUtils; import de.epiceric.shopchest.language.LanguageUtils;
import de.epiceric.shopchest.language.LocalizedMessage; import de.epiceric.shopchest.language.LocalizedMessage;
import de.epiceric.shopchest.nms.Hologram; import de.epiceric.shopchest.nms.Hologram;
@ -19,6 +21,7 @@ import org.bukkit.inventory.ItemStack;
public class Shop { public class Shop {
private boolean created;
private int id; private int id;
private ShopChest plugin; private ShopChest plugin;
private OfflinePlayer vendor; private OfflinePlayer vendor;
@ -39,22 +42,6 @@ public class Shop {
this.buyPrice = buyPrice; this.buyPrice = buyPrice;
this.sellPrice = sellPrice; this.sellPrice = sellPrice;
this.shopType = shopType; this.shopType = shopType;
Block b = location.getBlock();
if (b.getType() != Material.CHEST && b.getType() != Material.TRAPPED_CHEST) {
plugin.getShopUtils().removeShop(this, plugin.getShopChestConfig().remove_shop_on_error);
plugin.getLogger().severe("No Chest found at specified Location: " + b.getX() + "; " + b.getY() + "; " + b.getZ());
plugin.debug("No Chest found at specified Location: " + b.getX() + "; " + b.getY() + "; " + b.getZ());
return;
} else if ((b.getRelative(BlockFace.UP).getType() != Material.AIR) && plugin.getShopChestConfig().show_shop_items) {
plugin.getShopUtils().removeShop(this, plugin.getShopChestConfig().remove_shop_on_error);
plugin.getLogger().severe("No space above chest at specified Location: " + b.getX() + "; " + b.getY() + "; " + b.getZ());
plugin.debug("No space above chest at specified Location: " + b.getX() + "; " + b.getY() + "; " + b.getZ());
return;
}
if (hologram == null || !hologram.exists()) createHologram();
if (item == null) createItem();
} }
private Shop(OfflinePlayer vendor, ItemStack product, Location location, double buyPrice, double sellPrice, ShopType shopType) { private Shop(OfflinePlayer vendor, ItemStack product, Location location, double buyPrice, double sellPrice, ShopType shopType) {
@ -67,21 +54,54 @@ public class Shop {
this.shopType = shopType; this.shopType = shopType;
} }
public void create() {
if (created) return;
Block b = location.getBlock();
if (b.getType() != Material.CHEST && b.getType() != Material.TRAPPED_CHEST) {
ChestNotFoundException ex = new ChestNotFoundException("No Chest found at location: " + b.getX() + "; " + b.getY() + "; " + b.getZ());
plugin.getShopUtils().removeShop(this, plugin.getShopChestConfig().remove_shop_on_error);
plugin.getLogger().severe(ex.getMessage());
plugin.debug("Failed to create shop (#" + id + ")");
plugin.debug(ex);
return;
} else if ((b.getRelative(BlockFace.UP).getType() != Material.AIR) && plugin.getShopChestConfig().show_shop_items) {
NotEnoughSpaceException ex = new NotEnoughSpaceException("No space above chest at location: " + b.getX() + "; " + b.getY() + "; " + b.getZ());
plugin.getShopUtils().removeShop(this, plugin.getShopChestConfig().remove_shop_on_error);
plugin.getLogger().severe(ex.getMessage());
plugin.debug("Failed to create shop (#" + id + ")");
plugin.debug(ex);
return;
}
if (hologram == null || !hologram.exists()) createHologram();
if (item == null) createItem();
created = true;
}
/** /**
* Creates the hologram of the shop if it doesn't exist * Removes the hologram of the shop
*/ */
public void removeHologram() { public void removeHologram(boolean useCurrentThread) {
if (hologram != null && hologram.exists()) { if (hologram != null && hologram.exists()) {
plugin.debug("Removing hologram (#" + id + ")"); plugin.debug("Removing hologram (#" + id + ")");
for (Player p : Bukkit.getOnlinePlayers()) { for (Player p : Bukkit.getOnlinePlayers()) {
hologram.hidePlayer(p); hologram.hidePlayer(p, useCurrentThread);
} }
hologram.remove(); hologram.remove();
} }
} }
/**
* Removes the hologram of the shop
*/
public void removeHologram() {
removeHologram(false);
}
/** /**
* Removes the floating item of the shop * Removes the floating item of the shop
*/ */
@ -203,6 +223,13 @@ public class Shop {
hologram = new Hologram(plugin, holoText, holoLocation); hologram = new Hologram(plugin, holoText, holoLocation);
} }
/**
* @return Whether the shop has already been created
*/
public boolean isCreated() {
return created;
}
/** /**
* @return The ID of the shop * @return The ID of the shop
*/ */

View File

@ -2,9 +2,11 @@ package de.epiceric.shopchest.sql;
import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.ShopChest;
import de.epiceric.shopchest.event.ShopBuySellEvent; import de.epiceric.shopchest.event.ShopBuySellEvent;
import de.epiceric.shopchest.exceptions.WorldNotFoundException;
import de.epiceric.shopchest.language.LanguageUtils; import de.epiceric.shopchest.language.LanguageUtils;
import de.epiceric.shopchest.shop.Shop; import de.epiceric.shopchest.shop.Shop;
import de.epiceric.shopchest.shop.Shop.ShopType; import de.epiceric.shopchest.shop.Shop.ShopType;
import de.epiceric.shopchest.utils.Callback;
import de.epiceric.shopchest.utils.Utils; import de.epiceric.shopchest.utils.Utils;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
@ -12,6 +14,7 @@ import org.bukkit.OfflinePlayer;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitRunnable;
import java.sql.*; import java.sql.*;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -36,117 +39,151 @@ public abstract class Database {
* (Re-)Connects to the the database and initializes it. <br> * (Re-)Connects to the the database and initializes it. <br>
* Creates the table (if doesn't exist) and tests the connection * Creates the table (if doesn't exist) and tests the connection
*/ */
public void connect() { public void connect(final Callback callback) {
try { new BukkitRunnable() {
disconnect(); @Override
public void run() {
try {
disconnect();
plugin.debug("Connecting to database..."); plugin.debug("Connecting to database...");
connection = getConnection(); connection = getConnection();
String queryCreateTableShopList = String queryCreateTableShopList =
"CREATE TABLE IF NOT EXISTS shop_list (" + "CREATE TABLE IF NOT EXISTS shop_list (" +
"`id` int(11) NOT NULL," + "`id` int(11) NOT NULL," +
"`vendor` tinytext NOT NULL," + "`vendor` tinytext NOT NULL," +
"`product` text NOT NULL," + "`product` text NOT NULL," +
"`world` tinytext NOT NULL," + "`world` tinytext NOT NULL," +
"`x` int(11) NOT NULL," + "`x` int(11) NOT NULL," +
"`y` int(11) NOT NULL," + "`y` int(11) NOT NULL," +
"`z` int(11) NOT NULL," + "`z` int(11) NOT NULL," +
"`buyprice` float(32) NOT NULL," + "`buyprice` float(32) NOT NULL," +
"`sellprice` float(32) NOT NULL," + "`sellprice` float(32) NOT NULL," +
"`shoptype` tinytext NOT NULL," + "`shoptype` tinytext NOT NULL," +
"PRIMARY KEY (`id`)" + "PRIMARY KEY (`id`)" +
");"; ");";
String queryCreateTableShopLog = String queryCreateTableShopLog =
"CREATE TABLE IF NOT EXISTS shop_log (" + "CREATE TABLE IF NOT EXISTS shop_log (" +
"`id` INTEGER PRIMARY KEY " + (this instanceof SQLite ? "AUTOINCREMENT" : "AUTO_INCREMENT") + "," + "`id` INTEGER PRIMARY KEY " + (Database.this instanceof SQLite ? "AUTOINCREMENT" : "AUTO_INCREMENT") + "," +
"`timestamp` TINYTEXT NOT NULL," + "`timestamp` TINYTEXT NOT NULL," +
"`executor` TINYTEXT NOT NULL," + "`executor` TINYTEXT NOT NULL," +
"`product` TINYTEXT NOT NULL," + "`product` TINYTEXT NOT NULL," +
"`vendor` TINYTEXT NOT NULL," + "`vendor` TINYTEXT NOT NULL," +
"`world` TINYTEXT NOT NULL," + "`world` TINYTEXT NOT NULL," +
"`x` INTEGER NOT NULL," + "`x` INTEGER NOT NULL," +
"`y` INTEGER NOT NULL," + "`y` INTEGER NOT NULL," +
"`z` INTEGER NOT NULL," + "`z` INTEGER NOT NULL," +
"`price` FLOAT NOT NULL," + "`price` FLOAT NOT NULL," +
"`type` TINYTEXT NOT NULL" + "`type` TINYTEXT NOT NULL" +
");"; ");";
// Create table "shop_list" // Create table "shop_list"
Statement s = connection.createStatement(); Statement s = connection.createStatement();
s.executeUpdate(queryCreateTableShopList); s.executeUpdate(queryCreateTableShopList);
s.close(); s.close();
// Create table "shop_log" // Create table "shop_log"
Statement s2 = connection.createStatement(); Statement s2 = connection.createStatement();
s2.executeUpdate(queryCreateTableShopLog); s2.executeUpdate(queryCreateTableShopLog);
s2.close(); s2.close();
// Count entries in table "shop_list" // Count entries in table "shop_list"
PreparedStatement ps = connection.prepareStatement("SELECT * FROM shop_list"); PreparedStatement ps = connection.prepareStatement("SELECT * FROM shop_list");
ResultSet rs = ps.executeQuery(); ResultSet rs = ps.executeQuery();
int count = 0; int count = 0;
while (rs.next()) { while (rs.next()) {
if (rs.getString("vendor") != null) count++; if (rs.getString("vendor") != null) count++;
}
plugin.debug("Initialized database with " + count + " entries");
close(ps, rs);
if (callback != null) callback.callSyncResult(count);
} catch (SQLException ex) {
if (callback != null) callback.callSyncError(ex);
plugin.getLogger().severe("Failed to connect to database");
plugin.debug("Failed to connect to database");
plugin.debug(ex);
}
} }
plugin.debug("Initialized database with " + count + " entries"); }.runTaskAsynchronously(plugin);
close(ps, rs);
} catch (SQLException ex) {
plugin.getLogger().severe("Failed to connect to database");
plugin.debug("Failed to connect to database");
plugin.debug(ex);
}
} }
/** /**
* @return Lowest possible ID which is not used (> 0) * @return Lowest possible ID which is not used (> 0)
*/ */
public int getNextFreeID() { public void getNextFreeID(final Callback callback) {
int highestId = getHighestID(); getHighestID(new Callback(plugin) {
for (int i = 1; i <= highestId + 1; i++) { @Override
if (!isShop(i)) { public void onResult(Object result) {
plugin.debug("Next free id: " + i); if (result instanceof Integer) {
return i; int highestId = (int) result;
}
}
return 1; for (int i = 1; i <= highestId + 1; i++) {
final int id = i;
isShop(i, new Callback(plugin) {
@Override
public void onResult(Object result) {
if (result instanceof Boolean) {
boolean isShop = (boolean) result;
if (!isShop) {
if (callback != null) callback.callSyncResult(id);
}
}
}
@Override
public void onError(Throwable throwable) {
if (callback != null) callback.callSyncError(throwable);
}
});
}
}
}
@Override
public void onError(Throwable throwable) {
if (callback != null) callback.callSyncError(throwable);
}
});
} }
/** /**
* @return Highest ID which is used * @return Highest ID which is used
*/ */
public int getHighestID() { public void getHighestID(final Callback callback) {
PreparedStatement ps = null; new BukkitRunnable() {
ResultSet rs = null; @Override
public void run() {
PreparedStatement ps = null;
ResultSet rs = null;
int highestID = 0; int highestID = 0;
try { try {
ps = connection.prepareStatement("SELECT * FROM shop_list;"); ps = connection.prepareStatement("SELECT * FROM shop_list;");
rs = ps.executeQuery(); rs = ps.executeQuery();
while (rs.next()) { while (rs.next()) {
if (rs.getInt("id") > highestID) { if (rs.getInt("id") > highestID) {
highestID = rs.getInt("id"); highestID = rs.getInt("id");
}
}
plugin.debug("Highest used ID: " + highestID);
if (callback != null) callback.callSyncResult(highestID);
} catch (SQLException ex) {
if (callback != null) callback.callSyncError(ex);
plugin.debug("Failed to get highest used ID");
plugin.getLogger().severe("Failed to access database");
} finally {
close(ps, rs);
} }
} }
}.runTaskAsynchronously(plugin);
plugin.debug("Highest used ID: " + highestID);
return highestID;
} catch (SQLException ex) {
plugin.debug("Failed to get highest used ID");
plugin.getLogger().severe("Failed to access database");
} finally {
close(ps, rs);
}
return 0;
} }
/** /**
@ -154,135 +191,172 @@ public abstract class Database {
* *
* @param shop Shop to remove * @param shop Shop to remove
*/ */
public void removeShop(Shop shop) { public void removeShop(final Shop shop, final Callback callback) {
PreparedStatement ps = null; new BukkitRunnable() {
@Override
try { public void run() {
ps = connection.prepareStatement("DELETE FROM shop_list WHERE id = " + shop.getID() + ";"); PreparedStatement ps = null;
plugin.debug("Removing shop from database (#" + shop.getID() + ")");
ps.executeUpdate();
} catch (SQLException ex) {
plugin.getLogger().severe("Failed to access database");
plugin.debug("Failed to remove shop from database (#" + shop.getID() + ")");
plugin.debug(ex);
} finally {
close(ps, null);
}
try {
ps = connection.prepareStatement("DELETE FROM shop_list WHERE id = " + shop.getID() + ";");
plugin.debug("Removing shop from database (#" + shop.getID() + ")");
ps.executeUpdate();
if (callback != null) callback.callSyncResult(null);
} catch (SQLException ex) {
if (callback != null) callback.callSyncError(ex);
plugin.getLogger().severe("Failed to access database");
plugin.debug("Failed to remove shop from database (#" + shop.getID() + ")");
plugin.debug(ex);
} finally {
close(ps, null);
}
}
}.runTaskAsynchronously(plugin);
} }
/** /**
* @param id ID of the shop * @param id ID of the shop
* @return Whether a shop with the given ID exists * @return Whether a shop with the given ID exists
*/ */
public boolean isShop(int id) { public void isShop(final int id, final Callback callback) {
PreparedStatement ps = null; new BukkitRunnable() {
ResultSet rs = null; @Override
public void run() {
PreparedStatement ps = null;
ResultSet rs = null;
try { try {
ps = connection.prepareStatement("SELECT * FROM shop_list WHERE id = " + id + ";"); ps = connection.prepareStatement("SELECT * FROM shop_list WHERE id = " + id + ";");
rs = ps.executeQuery(); rs = ps.executeQuery();
while (rs.next()) { while (rs.next()) {
if (rs.getInt("id") == id) { if (rs.getInt("id") == id) {
return true; if (callback != null) callback.callSyncResult(true);
return;
}
}
if (callback != null) callback.callSyncResult(false);
} catch (SQLException ex) {
if (callback != null) callback.callSyncError(ex);
plugin.getLogger().severe("Failed to access database");
plugin.debug("Failed to check if shop with ID exists (#" + id + ")");
plugin.debug(ex);
} finally {
close(ps, rs);
} }
} }
} catch (SQLException ex) { }.runTaskAsynchronously(plugin);
plugin.getLogger().severe("Failed to access database");
plugin.debug("Failed to check if shop with ID exists (#" + id + ")");
plugin.debug(ex);
} finally {
close(ps, rs);
}
return false;
} }
/** /**
* @param id ID of the shop * @param id ID of the shop
* @return Shop with the given ID * @return Shop with the given ID
*/ */
public Shop getShop(int id) { public void getShop(final int id, final Callback callback) {
PreparedStatement ps = null; new BukkitRunnable() {
ResultSet rs = null; @Override
public void run() {
PreparedStatement ps = null;
ResultSet rs = null;
try { try {
ps = connection.prepareStatement("SELECT * FROM shop_list WHERE id = " + id + ";"); ps = connection.prepareStatement("SELECT * FROM shop_list WHERE id = " + id + ";");
rs = ps.executeQuery(); rs = ps.executeQuery();
while (rs.next()) { while (rs.next()) {
if (rs.getInt("id") == id) { if (rs.getInt("id") == id) {
plugin.debug("Getting Shop... (#" + id + ")"); plugin.debug("Getting Shop... (#" + id + ")");
World world = Bukkit.getWorld(rs.getString("world")); String worldName = rs.getString("world");
int x = rs.getInt("x"); World world = Bukkit.getWorld(worldName);
int y = rs.getInt("y"); int x = rs.getInt("x");
int z = rs.getInt("z"); int y = rs.getInt("y");
int z = rs.getInt("z");
Location location = new Location(world, x, y, z); if (world == null) {
WorldNotFoundException ex = new WorldNotFoundException("Could not find world with name \"" + worldName + "\"");
callback.callSyncError(ex);
plugin.getLogger().warning(ex.getMessage());
plugin.debug("Failed to get shop (#" + id + ")");
plugin.debug(ex);
return;
}
Shop shop = plugin.getShopUtils().getShop(location); Location location = new Location(world, x, y, z);
if (shop != null) {
plugin.debug("Shop already exists, returning existing one (#" + id + ").");
return shop;
} else {
plugin.debug("Creating new shop... (#" + id + ")");
OfflinePlayer vendor = Bukkit.getOfflinePlayer(UUID.fromString(rs.getString("vendor"))); Shop shop = plugin.getShopUtils().getShop(location);
ItemStack product = Utils.decode(rs.getString("product")); if (shop != null) {
double buyPrice = rs.getDouble("buyprice"); plugin.debug("Shop already exists, returning existing one (#" + id + ").");
double sellPrice = rs.getDouble("sellprice"); if (callback != null) callback.callSyncResult(shop);
ShopType shopType = ShopType.valueOf(rs.getString("shoptype")); } else {
plugin.debug("Creating new shop... (#" + id + ")");
return new Shop(id, plugin, vendor, product, location, buyPrice, sellPrice, shopType); OfflinePlayer vendor = Bukkit.getOfflinePlayer(UUID.fromString(rs.getString("vendor")));
ItemStack product = Utils.decode(rs.getString("product"));
double buyPrice = rs.getDouble("buyprice");
double sellPrice = rs.getDouble("sellprice");
ShopType shopType = ShopType.valueOf(rs.getString("shoptype"));
if (callback != null) callback.callSyncResult(new Shop(id, plugin, vendor, product, location, buyPrice, sellPrice, shopType));
}
return;
}
} }
plugin.debug("Shop with ID not found, returning null. (#" + id + ")");
} catch (SQLException ex) {
if (callback != null) callback.callSyncError(ex);
plugin.getLogger().severe("Failed to access database");
plugin.debug("Failed to get shop (#" + id + ")");
plugin.debug(ex);
} finally {
close(ps, rs);
} }
if (callback != null) callback.callSyncResult(null);
} }
}.runTaskAsynchronously(plugin);
plugin.debug("Shop with ID not found, returning null. (#" + id + ")");
} catch (SQLException ex) {
plugin.getLogger().severe("Failed to access database");
plugin.debug("Failed to get shop (#" + id + ")");
plugin.debug(ex);
} finally {
close(ps, rs);
}
return null;
} }
/** /**
* Adds a shop to the database * Adds a shop to the database
* @param shop Shop to add * @param shop Shop to add
*/ */
public void addShop(Shop shop) { public void addShop(final Shop shop, final Callback callback) {
PreparedStatement ps = null; new BukkitRunnable() {
@Override
public void run() {
PreparedStatement ps = null;
try { try {
ps = connection.prepareStatement("REPLACE INTO shop_list (id,vendor,product,world,x,y,z,buyprice,sellprice,shoptype) VALUES(?,?,?,?,?,?,?,?,?,?)"); ps = connection.prepareStatement("REPLACE INTO shop_list (id,vendor,product,world,x,y,z,buyprice,sellprice,shoptype) VALUES(?,?,?,?,?,?,?,?,?,?)");
ps.setInt(1, shop.getID()); ps.setInt(1, shop.getID());
ps.setString(2, shop.getVendor().getUniqueId().toString()); ps.setString(2, shop.getVendor().getUniqueId().toString());
ps.setString(3, Utils.encode(shop.getProduct())); ps.setString(3, Utils.encode(shop.getProduct()));
ps.setString(4, shop.getLocation().getWorld().getName()); ps.setString(4, shop.getLocation().getWorld().getName());
ps.setInt(5, shop.getLocation().getBlockX()); ps.setInt(5, shop.getLocation().getBlockX());
ps.setInt(6, shop.getLocation().getBlockY()); ps.setInt(6, shop.getLocation().getBlockY());
ps.setInt(7, shop.getLocation().getBlockZ()); ps.setInt(7, shop.getLocation().getBlockZ());
ps.setDouble(8, shop.getBuyPrice()); ps.setDouble(8, shop.getBuyPrice());
ps.setDouble(9, shop.getSellPrice()); ps.setDouble(9, shop.getSellPrice());
ps.setString(10, shop.getShopType().toString()); ps.setString(10, shop.getShopType().toString());
ps.executeUpdate();
plugin.debug("Adding shop to database (#" + shop.getID() + ")"); if (callback != null) callback.callSyncResult(null);
plugin.debug("Adding shop to database (#" + shop.getID() + ")");
ps.executeUpdate(); } catch (SQLException ex) {
} catch (SQLException ex) { if (callback != null) callback.callSyncError(ex);
plugin.getLogger().severe("Failed to access database"); plugin.getLogger().severe("Failed to access database");
plugin.debug("Failed to add shop to database (#" + shop.getID() + ")"); plugin.debug("Failed to add shop to database (#" + shop.getID() + ")");
plugin.debug(ex); plugin.debug(ex);
} finally { } finally {
close(ps, null); close(ps, null);
} }
}
}.runTaskAsynchronously(plugin);
} }
/** /**
@ -294,14 +368,11 @@ public abstract class Database {
* @param price Price (buyprice or sellprice, depends on {@code type}) * @param price Price (buyprice or sellprice, depends on {@code type})
* @param type Whether the player bought or sold something * @param type Whether the player bought or sold something
*/ */
public void logEconomy(final Player executor, final ItemStack product, final OfflinePlayer vendor, final ShopType shopType, final Location location, final double price, final ShopBuySellEvent.Type type) { public void logEconomy(final Player executor, final ItemStack product, final OfflinePlayer vendor, final ShopType shopType, final Location location, final double price, final ShopBuySellEvent.Type type, final Callback callback) {
new BukkitRunnable() {
Bukkit.getScheduler().runTaskAsynchronously(plugin, new Runnable() {
@Override @Override
public void run() { public void run() {
PreparedStatement ps = null; PreparedStatement ps = null;
boolean debugLogEnabled = plugin.getShopChestConfig().enable_debug_log;
try { try {
ps = connection.prepareStatement("INSERT INTO shop_log (timestamp,executor,product,vendor,world,x,y,z,price,type) VALUES(?,?,?,?,?,?,?,?,?,?)"); ps = connection.prepareStatement("INSERT INTO shop_log (timestamp,executor,product,vendor,world,x,y,z,price,type) VALUES(?,?,?,?,?,?,?,?,?,?)");
@ -316,33 +387,20 @@ public abstract class Database {
ps.setInt(8, location.getBlockZ()); ps.setInt(8, location.getBlockZ());
ps.setDouble(9, price); ps.setDouble(9, price);
ps.setString(10, type.toString()); ps.setString(10, type.toString());
ps.executeUpdate(); ps.executeUpdate();
if (debugLogEnabled) { if (callback != null) callback.callSyncResult(null);
Bukkit.getScheduler().runTask(plugin, new Runnable() { plugin.debug("Logged economy transaction to database");
@Override
public void run() {
plugin.debug("Logged economy transaction to database");
}
});
}
} catch (final SQLException ex) { } catch (final SQLException ex) {
if (callback != null) callback.callSyncError(ex);
plugin.getLogger().severe("Failed to access database"); plugin.getLogger().severe("Failed to access database");
if (debugLogEnabled) { plugin.debug("Failed to log economy transaction to database");
Bukkit.getScheduler().runTask(plugin, new Runnable() { plugin.debug(ex);
@Override
public void run() {
plugin.debug("Failed to log economy transaction to database");
plugin.debug(ex);
}
});
}
} finally { } finally {
close(ps, null); close(ps, null);
} }
} }
}); }.runTaskAsynchronously(plugin);
} }
/** /**

View File

@ -1,6 +1,7 @@
package de.epiceric.shopchest.sql; package de.epiceric.shopchest.sql;
import de.epiceric.shopchest.ShopChest; import de.epiceric.shopchest.ShopChest;
import org.bukkit.scheduler.BukkitRunnable;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
@ -38,13 +39,18 @@ public class MySQL extends Database {
} }
public void ping() { public void ping() {
try (PreparedStatement ps = connection.prepareStatement("/* ping */ SELECT 1")) { new BukkitRunnable() {
plugin.debug("Pinging to MySQL server..."); @Override
ps.executeQuery(); public void run() {
} catch (SQLException ex) { try (PreparedStatement ps = connection.prepareStatement("/* ping */ SELECT 1")) {
plugin.getLogger().severe("Failed to ping to MySQL server. Trying to reconnect..."); plugin.debug("Pinging to MySQL server...");
plugin.debug("Failed to ping to MySQL server. Trying to reconnect..."); ps.executeQuery();
connect(); } catch (SQLException ex) {
} plugin.getLogger().severe("Failed to ping to MySQL server. Trying to reconnect...");
plugin.debug("Failed to ping to MySQL server. Trying to reconnect...");
connect(null);
}
}
}.runTaskAsynchronously(plugin);
} }
} }

View File

@ -0,0 +1,34 @@
package de.epiceric.shopchest.utils;
import de.epiceric.shopchest.ShopChest;
import org.bukkit.scheduler.BukkitRunnable;
public abstract class Callback {
private ShopChest plugin;
public Callback(ShopChest plugin) {
this.plugin = plugin;
}
public void onResult(Object result) {}
public void onError(Throwable throwable) {}
public void callSyncResult(final Object result) {
new BukkitRunnable() {
@Override
public void run() {
onResult(result);
}
}.runTask(plugin);
}
public void callSyncError(final Throwable throwable) {
new BukkitRunnable() {
@Override
public void run() {
onError(throwable);
}
}.runTask(plugin);
}
}

View File

@ -86,7 +86,7 @@ public class ShopUtils {
} }
if (addToDatabase) if (addToDatabase)
plugin.getShopDatabase().addShop(shop); plugin.getShopDatabase().addShop(shop, null);
} }
@ -95,7 +95,7 @@ public class ShopUtils {
* @param shop Shop to remove * @param shop Shop to remove
* @param removeFromDatabase Whether the shop should also be removed from the database * @param removeFromDatabase Whether the shop should also be removed from the database
*/ */
public void removeShop(Shop shop, boolean removeFromDatabase) { public void removeShop(Shop shop, boolean removeFromDatabase, boolean useCurrentThread) {
plugin.debug("Removing shop (#" + shop.getID() + ")"); plugin.debug("Removing shop (#" + shop.getID() + ")");
InventoryHolder ih = shop.getInventoryHolder(); InventoryHolder ih = shop.getInventoryHolder();
@ -112,10 +112,14 @@ public class ShopUtils {
} }
shop.removeItem(); shop.removeItem();
shop.removeHologram(); shop.removeHologram(useCurrentThread);
if (removeFromDatabase) if (removeFromDatabase)
plugin.getShopDatabase().removeShop(shop); plugin.getShopDatabase().removeShop(shop, null);
}
public void removeShop(Shop shop, boolean removeFromDatabase) {
removeShop(shop, removeFromDatabase, false);
} }
/** /**
@ -189,40 +193,61 @@ public class ShopUtils {
* @param showConsoleMessages Whether messages about the language file should be shown in the console * @param showConsoleMessages Whether messages about the language file should be shown in the console
* @return Amount of shops, which were reloaded * @return Amount of shops, which were reloaded
*/ */
public int reloadShops(boolean reloadConfig, boolean showConsoleMessages) { public void reloadShops(boolean reloadConfig, boolean showConsoleMessages, final Callback callback) {
plugin.debug("Reloading shops..."); plugin.debug("Reloading shops...");
plugin.getShopDatabase().connect();
if (reloadConfig) { if (reloadConfig) {
plugin.getShopChestConfig().reload(false, true, showConsoleMessages); plugin.getShopChestConfig().reload(false, true, showConsoleMessages);
plugin.getUpdater().setMaxDelta(plugin.getShopChestConfig().update_quality.getTime()); plugin.getUpdater().setMaxDelta(plugin.getShopChestConfig().update_quality.getTime());
} }
for (Shop shop : getShops()) { plugin.getShopDatabase().connect(new Callback(plugin) {
removeShop(shop, false); @Override
plugin.debug("Removed shop (#" + shop.getID() + ")"); public void onResult(Object result) {
}
int highestId = plugin.getShopDatabase().getHighestID(); for (Shop shop : getShops()) {
removeShop(shop, false);
plugin.debug("Removed shop (#" + shop.getID() + ")");
}
int count = 0; plugin.getShopDatabase().getHighestID(new Callback(plugin) {
for (int id = 1; id <= highestId; id++) { @Override
public void onResult(Object result) {
if (result instanceof Integer) {
int highestId = (int) result;
int count = 0;
for (int i = 1; i <= highestId; i++) {
final int id = i;
plugin.debug("Trying to add shop. (#" + id + ")");
plugin.getShopDatabase().getShop(id, new Callback(plugin) {
@Override
public void onResult(Object result) {
if (result instanceof Shop) {
Shop shop = (Shop) result;
shop.create();
addShop(shop, false);
}
}
@Override
public void onError(Throwable throwable) {
plugin.debug("Error while adding shop (#" + id + "):");
plugin.debug(throwable);
}
});
count++;
}
if (callback != null) callback.callSyncResult(count);
}
}
});
try {
plugin.debug("Trying to add shop. (#" + id + ")");
Shop shop = plugin.getShopDatabase().getShop(id);
addShop(shop, false);
} catch (Exception e) {
plugin.debug("Error while adding shop (#" + id + "):");
plugin.debug(e);
continue;
} }
});
count++;
}
return count;
} }
/** /**