diff --git a/src/main/java/world/bentobox/bentobox/Settings.java b/src/main/java/world/bentobox/bentobox/Settings.java index b9d08c714..bacb65105 100644 --- a/src/main/java/world/bentobox/bentobox/Settings.java +++ b/src/main/java/world/bentobox/bentobox/Settings.java @@ -38,7 +38,7 @@ public class Settings implements ConfigObject { private boolean useEconomy = true; // Database - @ConfigComment("JSON, MYSQL, MARIADB (10.2.3+), MONGODB, and YAML(deprecated).") + @ConfigComment("JSON, MYSQL, MARIADB (10.2.3+), MONGODB, SQLITE and YAML(deprecated).") @ConfigComment("Transition database options are:") @ConfigComment(" YAML2JSON, YAML2MARIADB, YAML2MYSQL") @ConfigComment(" JSON2MARIADB, JSON2MYSQL, MYSQL2JSON") diff --git a/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java b/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java index 350853ef9..701b82606 100644 --- a/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java +++ b/src/main/java/world/bentobox/bentobox/database/DatabaseSetup.java @@ -5,6 +5,7 @@ import world.bentobox.bentobox.database.json.JSONDatabase; import world.bentobox.bentobox.database.mariadb.MariaDBDatabase; import world.bentobox.bentobox.database.mongodb.MongoDBDatabase; import world.bentobox.bentobox.database.mysql.MySQLDatabase; +import world.bentobox.bentobox.database.sqlite.SQLiteDatabase; import world.bentobox.bentobox.database.transition.Json2MariaDBDatabase; import world.bentobox.bentobox.database.transition.Json2MySQLDatabase; import world.bentobox.bentobox.database.transition.MySQL2JsonDatabase; @@ -80,7 +81,12 @@ public interface DatabaseSetup { */ MARIADB(new MariaDBDatabase()), - MONGODB(new MongoDBDatabase()); + MONGODB(new MongoDBDatabase()), + + /** + * @since 1.6.0 + */ + SQLITE(new SQLiteDatabase()); DatabaseSetup database; diff --git a/src/main/java/world/bentobox/bentobox/database/sqlite/SQLiteDatabase.java b/src/main/java/world/bentobox/bentobox/database/sqlite/SQLiteDatabase.java new file mode 100644 index 000000000..e209e5bae --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/sqlite/SQLiteDatabase.java @@ -0,0 +1,17 @@ +package world.bentobox.bentobox.database.sqlite; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.AbstractDatabaseHandler; +import world.bentobox.bentobox.database.DatabaseSetup; + +/** + * @since 1.6.0 + * @author Poslovitch + */ +public class SQLiteDatabase implements DatabaseSetup { + + @Override + public AbstractDatabaseHandler getHandler(Class dataObjectClass) { + return new SQLiteDatabaseHandler<>(BentoBox.getInstance(), dataObjectClass, new SQLiteDatabaseConnector(BentoBox.getInstance())); + } +} diff --git a/src/main/java/world/bentobox/bentobox/database/sqlite/SQLiteDatabaseConnector.java b/src/main/java/world/bentobox/bentobox/database/sqlite/SQLiteDatabaseConnector.java new file mode 100644 index 000000000..b1013ccec --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/sqlite/SQLiteDatabaseConnector.java @@ -0,0 +1,66 @@ +package world.bentobox.bentobox.database.sqlite; + +import org.bukkit.Bukkit; +import org.eclipse.jdt.annotation.NonNull; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.DatabaseConnector; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +/** + * @since 1.6.0 + * @author Poslovitch + */ +public class SQLiteDatabaseConnector implements DatabaseConnector { + + private String connectionUrl; + private Connection connection = null; + private static final String DATABASE_FOLDER_NAME = "database"; + + SQLiteDatabaseConnector(@NonNull BentoBox plugin) { + File dataFolder = new File(plugin.getDataFolder(), DATABASE_FOLDER_NAME); + connectionUrl = "jdbc:sqlite:" + dataFolder.getAbsolutePath() + File.separator + "database.db"; + } + + @Override + public Object createConnection() { + try { + connection = DriverManager.getConnection(connectionUrl); + } catch (SQLException e) { + Bukkit.getLogger().severe("Could not connect to the database! " + e.getMessage()); + } + return connection; + } + + @Override + public void closeConnection() { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + Bukkit.getLogger().severe("Could not close SQLite database connection"); + } + } + } + + @Override + public String getConnectionUrl() { + return connectionUrl; + } + + @Override + @NonNull + public String getUniqueId(String tableName) { + // Not used + return ""; + } + + @Override + public boolean uniqueIdExists(String tableName, String key) { + // Not used + return false; + } +} diff --git a/src/main/java/world/bentobox/bentobox/database/sqlite/SQLiteDatabaseHandler.java b/src/main/java/world/bentobox/bentobox/database/sqlite/SQLiteDatabaseHandler.java new file mode 100644 index 000000000..54f37561a --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/sqlite/SQLiteDatabaseHandler.java @@ -0,0 +1,215 @@ +package world.bentobox.bentobox.database.sqlite; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import org.bukkit.Bukkit; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.DatabaseConnector; +import world.bentobox.bentobox.database.json.AbstractJSONDatabaseHandler; +import world.bentobox.bentobox.database.objects.DataObject; + +import java.beans.IntrospectionException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @since 1.6.0 + * @author Poslovitch + */ +public class SQLiteDatabaseHandler extends AbstractJSONDatabaseHandler { + + /** + * Connection to the database + */ + private Connection connection; + + /** + * Constructor + * + * @param plugin + * @param type The type of the objects that should be created and filled with + * values from the database or inserted into the database + * @param databaseConnector Contains the settings to create a connection to the database + */ + protected SQLiteDatabaseHandler(BentoBox plugin, Class type, DatabaseConnector databaseConnector) { + super(plugin, type, databaseConnector); + connection = (Connection) databaseConnector.createConnection(); + if (connection == null) { + plugin.logError("Are the settings in config.yml correct?"); + Bukkit.getPluginManager().disablePlugin(plugin); + return; + } + // Check if the table exists in the database and if not, create it + createSchema(); + } + + /** + * Creates the table in the database if it doesn't exist already + */ + private void createSchema() { + String sql = "CREATE TABLE IF NOT EXISTS `" + + dataObject.getCanonicalName() + + "` (json JSON, uniqueId VARCHAR(255) NOT NULL PRIMARY KEY)"; + // Prepare and execute the database statements + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.executeUpdate(); + } catch (SQLException e) { + plugin.logError("Problem trying to create schema for data object " + dataObject.getCanonicalName() + " " + e.getMessage()); + } + } + + @Override + public List loadObjects() throws InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, IntrospectionException, NoSuchMethodException { + try (Statement preparedStatement = connection.createStatement()) { + List list = new ArrayList<>(); + + String sb = "SELECT `json` FROM `" + + dataObject.getCanonicalName() + + "`"; + try (ResultSet resultSet = preparedStatement.executeQuery(sb)) { + // Load all the results + Gson gson = getGson(); + while (resultSet.next()) { + String json = resultSet.getString("json"); + if (json != null) { + try { + T gsonResult = gson.fromJson(json, dataObject); + if (gsonResult != null) { + list.add(gsonResult); + } + } catch (JsonSyntaxException ex) { + plugin.logError("Could not load object " + ex.getMessage()); + plugin.logError(json); + } + } + } + } catch (Exception e) { + plugin.logError("Could not load object " + e.getMessage()); + } + return list; + } catch (SQLException e) { + plugin.logError("Could not load objects " + e.getMessage()); + } + return Collections.emptyList(); + } + + @Nullable + @Override + public T loadObject(@NonNull String uniqueId) throws InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, IntrospectionException, NoSuchMethodException { + String sb = "SELECT `json` FROM `" + dataObject.getCanonicalName() + "` WHERE uniqueId = ? LIMIT 1"; + try (PreparedStatement preparedStatement = connection.prepareStatement(sb)) { + // UniqueId needs to be placed in quotes + preparedStatement.setString(1, "\"" + uniqueId + "\""); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + if (resultSet.next()) { + // If there is a result, we only want/need the first one + Gson gson = getGson(); + return gson.fromJson(resultSet.getString("json"), dataObject); + } + } catch (Exception e) { + plugin.logError("Could not load object " + uniqueId + " " + e.getMessage()); + } + } catch (SQLException e) { + plugin.logError("Could not load object " + uniqueId + " " + e.getMessage()); + } + return null; + } + + @Override + public void saveObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException { + // Null check + if (instance == null) { + plugin.logError("MySQL database request to store a null. "); + return; + } + if (!(instance instanceof DataObject)) { + plugin.logError("This class is not a DataObject: " + instance.getClass().getName()); + return; + } + String sb = "INSERT INTO " + + "`" + + dataObject.getCanonicalName() + + "` (json, uniqueId) VALUES (?, ?) ON CONFLICT(uniqueId) DO UPDATE SET json = ?"; + + Gson gson = getGson(); + String toStore = gson.toJson(instance); + + try (PreparedStatement preparedStatement = connection.prepareStatement(sb)) { + preparedStatement.setString(1, toStore); + preparedStatement.setString(2, ((DataObject)instance).getUniqueId()); + preparedStatement.setString(3, toStore); + preparedStatement.execute(); + } catch (SQLException e) { + plugin.logError("Could not save object " + instance.getClass().getName() + " " + e.getMessage()); + } + } + + @Override + public void deleteObject(T instance) throws IllegalAccessException, InvocationTargetException, IntrospectionException { + // Null check + if (instance == null) { + plugin.logError("SQLite database request to delete a null."); + return; + } + if (!(instance instanceof DataObject)) { + plugin.logError("This class is not a DataObject: " + instance.getClass().getName()); + return; + } + try { + Method getUniqueId = dataObject.getMethod("getUniqueId"); + deleteID((String) getUniqueId.invoke(instance)); + } catch (Exception e) { + plugin.logError("Could not delete object " + instance.getClass().getName() + " " + e.getMessage()); + } + } + + @Override + public boolean objectExists(String uniqueId) { + // Create the query to see if this key exists + String query = "SELECT EXISTS (SELECT 1 FROM `" + + dataObject.getCanonicalName() + + "` WHERE `uniqueId` = ?)"; + + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + // UniqueId needs to be placed in quotes + preparedStatement.setString(1, "\"" + uniqueId + "\""); + try (ResultSet resultSet = preparedStatement.executeQuery()) { + if (resultSet.next()) { + return resultSet.getBoolean(1); + } + } + } catch (SQLException e) { + plugin.logError("Could not check if key exists in database! " + uniqueId + " " + e.getMessage()); + } + return false; + } + + @Override + public void close() { + databaseConnector.closeConnection(); + } + + @Override + public void deleteID(String uniqueId) { + String sb = "DELETE FROM `" + + dataObject.getCanonicalName() + + "` WHERE uniqueId = ?"; + try (PreparedStatement preparedStatement = connection.prepareStatement(sb)) { + // UniqueId needs to be placed in quotes + preparedStatement.setString(1, "\"" + uniqueId + "\""); + preparedStatement.execute(); + } catch (Exception e) { + plugin.logError("Could not delete object " + dataObject.getCanonicalName() + " " + uniqueId + " " + e.getMessage()); + } + } +} diff --git a/src/main/java/world/bentobox/bentobox/database/sqlite/package-info.java b/src/main/java/world/bentobox/bentobox/database/sqlite/package-info.java new file mode 100644 index 000000000..2f69659f8 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/sqlite/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains SQLite database managers. + * @since 1.6.0 + */ +package world.bentobox.bentobox.database.sqlite; \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a16d43096..4efaa1341 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -11,7 +11,7 @@ general: # If there is no economy plugin present anyway, money will be automatically disabled. use-economy: true database: - # JSON, MYSQL, MARIADB (10.2.3+), MONGODB, and YAML(deprecated). + # JSON, MYSQL, MARIADB (10.2.3+), MONGODB, SQLITE and YAML(deprecated). # Transition database options are: # YAML2JSON, YAML2MARIADB, YAML2MYSQL # JSON2MARIADB, JSON2MYSQL, MYSQL2JSON diff --git a/src/test/java/world/bentobox/bentobox/database/mariadb/MariaDBDatabaseHandlerTest.java b/src/test/java/world/bentobox/bentobox/database/mariadb/MariaDBDatabaseHandlerTest.java index 0f3e1c36f..b9b598d8c 100644 --- a/src/test/java/world/bentobox/bentobox/database/mariadb/MariaDBDatabaseHandlerTest.java +++ b/src/test/java/world/bentobox/bentobox/database/mariadb/MariaDBDatabaseHandlerTest.java @@ -401,22 +401,9 @@ public class MariaDBDatabaseHandlerTest { @Test public void testClose() throws SQLException { handler.close(); - verify(connection).close(); + verify(dbConn).closeConnection(); } - /** - * Test method for {@link world.bentobox.bentobox.database.mariadb.MariaDBDatabaseHandler#close()}. - * @throws SQLException - */ - @Ignore("it doesn't recognize the #close() ran in the database connector") - @Test - public void testCloseError() throws SQLException { - Mockito.doThrow(new SQLException("error")).when(connection).close(); - handler.close(); - verify(plugin).logError(eq("Could not close database for some reason")); - } - - /** * Test method for {@link world.bentobox.bentobox.database.mariadb.MariaDBDatabaseHandler#deleteID(java.lang.String)}. * @throws SQLException diff --git a/src/test/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseConnectorTest.java b/src/test/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseConnectorTest.java new file mode 100644 index 000000000..7d62c540d --- /dev/null +++ b/src/test/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseConnectorTest.java @@ -0,0 +1,158 @@ +/** + * + */ +package world.bentobox.bentobox.database.mysql; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.logging.Logger; + +import org.bukkit.Bukkit; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import world.bentobox.bentobox.database.DatabaseConnectionSettingsImpl; + +/** + * @author tastybento + * + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest( { Bukkit.class, DriverManager.class }) +public class MySQLDatabaseConnectorTest { + + @Mock + private DatabaseConnectionSettingsImpl dbSettings; + @Mock + private Connection connection; + @Mock + private Logger logger; + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + when(dbSettings.getDatabaseName()).thenReturn("bentobox"); + when(dbSettings.getHost()).thenReturn("localhost"); + when(dbSettings.getPort()).thenReturn(1234); + when(dbSettings.getUsername()).thenReturn("username"); + when(dbSettings.getPassword()).thenReturn("password"); + + mockStatic(DriverManager.class); + when(DriverManager.getConnection( + "jdbc:mysql://localhost:1234/bentobox?autoReconnect=true&useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8", + "username", + "password" + )).thenReturn(connection); + + // Logger + PowerMockito.mockStatic(Bukkit.class); + when(Bukkit.getLogger()).thenReturn(logger); + } + + /** + * @throws java.lang.Exception + */ + @After + public void tearDown() throws Exception { + } + + /** + * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseConnector#MySQLDatabaseConnector(world.bentobox.bentobox.database.DatabaseConnectionSettingsImpl)}. + */ + @Test + public void testMySQLDatabaseConnector() { + new MySQLDatabaseConnector(dbSettings); + verify(dbSettings).getDatabaseName(); + verify(dbSettings).getHost(); + verify(dbSettings).getPort(); + } + + /** + * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseConnector#createConnection()}. + */ + @Ignore("This is apparently very hard to do!") + @Test + public void testCreateConnection() { + MySQLDatabaseConnector dc = new MySQLDatabaseConnector(dbSettings); + assertEquals(connection, dc.createConnection()); + } + + /** + * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseConnector#createConnection()}. + * @throws SQLException + */ + @Test + public void testCreateConnectionError() throws SQLException { + PowerMockito.doThrow(new SQLException("error")).when(DriverManager.class); + DriverManager.getConnection(any(), any(), any()); + MySQLDatabaseConnector dc = new MySQLDatabaseConnector(dbSettings); + dc.createConnection(); + verify(logger).severe("Could not connect to the database! No suitable driver found for jdbc:mysql://localhost:1234/bentobox?autoReconnect=true&useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8"); + } + + /** + * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseConnector#getConnectionUrl()}. + */ + @Test + public void testGetConnectionUrl() { + MySQLDatabaseConnector dc = new MySQLDatabaseConnector(dbSettings); + assertEquals("jdbc:mysql://localhost:1234/bentobox" + + "?autoReconnect=true&useSSL=false&allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8", dc.getConnectionUrl()); + } + + /** + * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseConnector#getUniqueId(java.lang.String)}. + */ + @Test + public void testGetUniqueId() { + assertTrue(new MySQLDatabaseConnector(dbSettings).getUniqueId("any").isEmpty()); + + } + + /** + * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseConnector#uniqueIdExists(java.lang.String, java.lang.String)}. + */ + @Test + public void testUniqueIdExists() { + assertFalse(new MySQLDatabaseConnector(dbSettings).uniqueIdExists("", "")); + } + + /** + * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseConnector#closeConnection()}. + */ + @Test + public void testCloseConnection() { + MySQLDatabaseConnector dc = new MySQLDatabaseConnector(dbSettings); + dc.createConnection(); + dc.closeConnection(); + } + + /** + * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseConnector#closeConnection()}. + */ + @Test + public void testCloseConnectionError() throws SQLException { + MySQLDatabaseConnector dc = new MySQLDatabaseConnector(dbSettings); + dc.createConnection(); + dc.closeConnection(); + } + +} diff --git a/src/test/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseHandlerTest.java b/src/test/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseHandlerTest.java index b8ffac220..4550a3891 100644 --- a/src/test/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseHandlerTest.java +++ b/src/test/java/world/bentobox/bentobox/database/mysql/MySQLDatabaseHandlerTest.java @@ -398,22 +398,9 @@ public class MySQLDatabaseHandlerTest { @Test public void testClose() throws SQLException { handler.close(); - verify(connection).close(); + verify(dbConn).closeConnection(); } - /** - * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseHandler#close()}. - * @throws SQLException - */ - @Ignore("it doesn't recognize the #close() ran in the database connector") - @Test - public void testCloseError() throws SQLException { - Mockito.doThrow(new SQLException("error")).when(connection).close(); - handler.close(); - verify(plugin).logError(eq("Could not close database for some reason")); - } - - /** * Test method for {@link world.bentobox.bentobox.database.mysql.MySQLDatabaseHandler#deleteID(java.lang.String)}. * @throws SQLException