diff --git a/build.gradle b/build.gradle index 20c8190..9eb2d6b 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,8 @@ repositories { } ext { + junitVersion = '5.5.2' + libs = [ bukkit: [lib: 'org.bukkit:bukkit:1.12.2-R0.1-SNAPSHOT', exclude: [ 'com.google.code.gson:gson', @@ -27,7 +29,17 @@ ext { ]], commons_text: 'org.apache.commons:commons-text:1.9', lombok: 'org.projectlombok:lombok:1.18.12', - reflection_object: 'ru.dmitriymx:reflection-object:1.0-BETA' + reflection_object: 'ru.dmitriymx:reflection-object:1.0-BETA', + mysql: 'mysql:mysql-connector-java:8.0.22', + test: [ + junit5: [ + "org.junit.jupiter:junit-jupiter-api:$junitVersion", + "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + ], + mock: ['org.mockito:mockito-core:1.10.19'], + h2db: 'com.h2database:h2:1.4.200' + ] + ] } @@ -40,11 +52,30 @@ def compileOnly2(library) { } } +def testImplementation2(library) { + dependencies.testImplementation library.lib, { + library.exclude.each { String excludeLibStr -> + String[] excludeLib = excludeLibStr.split(':') + exclude group: excludeLib[0], module: excludeLib[1] + } + } +} + dependencies { compileOnly libs.lombok annotationProcessor libs.lombok compileOnly2 libs.bukkit + compileOnly libs.mysql implementation libs.commons_text implementation libs.reflection_object + + testImplementation libs.test.junit5 + testImplementation libs.test.mock + testImplementation2 libs.bukkit + testImplementation libs.test.h2db +} + +test { + useJUnitPlatform() } \ No newline at end of file diff --git a/src/main/java/ghast/GhastTools.java b/src/main/java/ghast/GhastTools.java index a374852..9ce291e 100644 --- a/src/main/java/ghast/GhastTools.java +++ b/src/main/java/ghast/GhastTools.java @@ -1,5 +1,6 @@ package ghast; +import ghast.assets.AssetsManager; import lombok.experimental.UtilityClass; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.Plugin; diff --git a/src/main/java/ghast/AssetsException.java b/src/main/java/ghast/assets/AssetsException.java similarity index 90% rename from src/main/java/ghast/AssetsException.java rename to src/main/java/ghast/assets/AssetsException.java index 472da21..ba31e5b 100644 --- a/src/main/java/ghast/AssetsException.java +++ b/src/main/java/ghast/assets/AssetsException.java @@ -1,4 +1,4 @@ -package ghast; +package ghast.assets; public class AssetsException extends RuntimeException { diff --git a/src/main/java/ghast/AssetsManager.java b/src/main/java/ghast/assets/AssetsManager.java similarity index 99% rename from src/main/java/ghast/AssetsManager.java rename to src/main/java/ghast/assets/AssetsManager.java index 9ef5589..b8774cb 100644 --- a/src/main/java/ghast/AssetsManager.java +++ b/src/main/java/ghast/assets/AssetsManager.java @@ -1,5 +1,6 @@ -package ghast; +package ghast.assets; +import ghast.GhastTools; import lombok.experimental.UtilityClass; import org.bukkit.plugin.Plugin; diff --git a/src/main/java/ghast/database/CannotGetJdbcConnectionException.java b/src/main/java/ghast/database/CannotGetJdbcConnectionException.java new file mode 100644 index 0000000..1d02152 --- /dev/null +++ b/src/main/java/ghast/database/CannotGetJdbcConnectionException.java @@ -0,0 +1,10 @@ +package ghast.database; + +import java.sql.SQLException; + +public class CannotGetJdbcConnectionException extends DataAccessException { + + public CannotGetJdbcConnectionException(String msg, SQLException ex) { + super(msg, ex); + } +} diff --git a/src/main/java/ghast/database/DataAccessException.java b/src/main/java/ghast/database/DataAccessException.java new file mode 100644 index 0000000..5c686b5 --- /dev/null +++ b/src/main/java/ghast/database/DataAccessException.java @@ -0,0 +1,28 @@ +package ghast.database; + +import lombok.Getter; + +@SuppressWarnings("java:S1165") +@Getter +public class DataAccessException extends RuntimeException { + + private String sql; + + public DataAccessException(String msg) { + super(msg); + } + + public DataAccessException(String msg, String sql) { + this(msg); + this.sql = sql; + } + + public DataAccessException(String msg, Throwable cause) { + super(msg, cause); + } + + public DataAccessException(String msg, String sql, Throwable cause) { + this(msg, cause); + this.sql = sql; + } +} diff --git a/src/main/java/ghast/database/EmptyResultDataAccessException.java b/src/main/java/ghast/database/EmptyResultDataAccessException.java new file mode 100644 index 0000000..0d42ee1 --- /dev/null +++ b/src/main/java/ghast/database/EmptyResultDataAccessException.java @@ -0,0 +1,8 @@ +package ghast.database; + +public class EmptyResultDataAccessException extends IncorrectResultSizeDataAccessException { + + public EmptyResultDataAccessException(int expectedSize) { + super(expectedSize); + } +} diff --git a/src/main/java/ghast/database/IncorrectResultSizeDataAccessException.java b/src/main/java/ghast/database/IncorrectResultSizeDataAccessException.java new file mode 100644 index 0000000..5acd10d --- /dev/null +++ b/src/main/java/ghast/database/IncorrectResultSizeDataAccessException.java @@ -0,0 +1,8 @@ +package ghast.database; + +public class IncorrectResultSizeDataAccessException extends DataAccessException { + + public IncorrectResultSizeDataAccessException(int expectedSize) { + super("Incorrect result size: expected " + expectedSize); + } +} diff --git a/src/main/java/ghast/database/JdbcOperations.java b/src/main/java/ghast/database/JdbcOperations.java new file mode 100644 index 0000000..89d12ca --- /dev/null +++ b/src/main/java/ghast/database/JdbcOperations.java @@ -0,0 +1,23 @@ +package ghast.database; + +import java.util.List; +import java.util.Map; + +public interface JdbcOperations { + + void execute(String sql) throws DataAccessException; + + T query(String sql, ResultSetExtractor rse) throws DataAccessException; + + List query(String sql, RowMapper rowMapper) throws DataAccessException; + + T queryForObject(String sql, RowMapper rowMapper) throws DataAccessException; + + Map queryForMap(String sql) throws DataAccessException; + + List> queryForList(String sql) throws DataAccessException; + + int update(String sql) throws DataAccessException; + + int delete(String sql) throws DataAccessException; +} diff --git a/src/main/java/ghast/database/JdbcTemplate.java b/src/main/java/ghast/database/JdbcTemplate.java new file mode 100644 index 0000000..c9e629a --- /dev/null +++ b/src/main/java/ghast/database/JdbcTemplate.java @@ -0,0 +1,237 @@ +package ghast.database; + +import ghast.XLog; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@NoArgsConstructor +@Getter +@Setter +public class JdbcTemplate implements JdbcOperations { + + private DataSource dataSource; + + public JdbcTemplate(DataSource dataSource) { + setDataSource(dataSource); + } + + @Override + public void execute(String sql) throws DataAccessException { + XLog.debug("Execute SQL: {0}", sql); + + Connection connection = openConnection(); + Statement statement = null; + try { + statement = connection.createStatement(); + statement.execute(sql); + } catch (SQLException e) { + throw new DataAccessException("Error execute SQL", sql, e); + } finally { + closeStatement(statement); + closeConnection(connection); + } + } + + @Override + public T query(String sql, ResultSetExtractor rse) throws DataAccessException { + XLog.debug("Execute SQL: {0}", sql); + + Connection connection = openConnection(); + Statement statement = null; + ResultSet resultSet = null; + try { + statement = connection.createStatement(); + resultSet = statement.executeQuery(sql); + return rse.extractData(resultSet); + } catch (SQLException e) { + throw new DataAccessException("Error execute SQL", sql, e); + } finally { + closeResultSet(resultSet); + closeStatement(statement); + closeConnection(connection); + } + } + + @Override + public List query(String sql, final RowMapper rowMapper) throws DataAccessException { + return query(sql, rs -> { + List resultList = new ArrayList<>(); + int rowNum = 0; + while (rs.next()) { + resultList.add(rowMapper.mapRow(rs, rowNum++)); + } + return resultList; + }); + } + + @Override + public T queryForObject(String sql, RowMapper rowMapper) throws DataAccessException { + return query(sql, rs -> { + if (rs.next()) { + T resultObj = rowMapper.mapRow(rs, 0); + + if (rs.next()) { + throw new IncorrectResultSizeDataAccessException(1); + } + + return resultObj; + } else { + throw new EmptyResultDataAccessException(1); + } + }); + } + + @Override + public Map queryForMap(String sql) throws DataAccessException { + return queryForObject(sql, (rs, rowNum) -> { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + Map resultMap = new LinkedHashMap<>(columnCount); + + for (int i = 1; i <= columnCount; i++) { + String key = lookupColumnName(metaData, i); + Object value = getResultSetRawValue(rs, i); + resultMap.put(key, value); + } + + return resultMap; + }); + } + + @Override + public List> queryForList(String sql) throws DataAccessException { + return query(sql, (rs, rowNum) -> { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + Map resultMap = new LinkedHashMap<>(columnCount); + + for (int i = 1; i <= columnCount; i++) { + String key = lookupColumnName(metaData, i); + Object value = getResultSetRawValue(rs, i); + resultMap.put(key, value); + } + + return resultMap; + }); + } + + @Override + public int update(String sql) throws DataAccessException { + XLog.debug("Execute SQL: {0}", sql); + + Connection connection = openConnection(); + Statement statement = null; + try { + statement = connection.createStatement(); + int rows = statement.executeUpdate(sql); + XLog.debug("Affected {0} rows", rows); + return rows; + } catch (SQLException e) { + XLog.error("Error execute SQL: {0}", e.getMessage(), e); + return 0; + } finally { + closeStatement(statement); + closeConnection(connection); + } + } + + @Override + public int delete(String sql) throws DataAccessException { + return update(sql); + } + + private Connection openConnection() { + try { + return getDataSource().getConnection(); + } + catch (SQLException ex) { + throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex); + } + } + + private String lookupColumnName(ResultSetMetaData resultSetMetaData, int columnIndex) throws SQLException { + String name = resultSetMetaData.getColumnLabel(columnIndex); + if (name == null || name.isEmpty()) { + name = resultSetMetaData.getColumnName(columnIndex); + } + return name; + } + + private Object getResultSetRawValue(ResultSet resultSet, int index) throws SQLException { + Object obj = resultSet.getObject(index); + String className = null; + if (obj != null) { + className = obj.getClass().getName(); + } + + if (obj instanceof Blob) { + Blob blob = (Blob) obj; + obj = blob.getBytes(1, (int) blob.length()); + } else if (obj instanceof Clob) { + Clob clob = (Clob) obj; + obj = clob.getSubString(1, (int) clob.length()); + } else if ("oracle.sql.TIMESTAMP".equals(className) || "oracle.sql.TIMESTAMPTZ".equals(className)) { + obj = resultSet.getTimestamp(index); + } else if (className != null && className.startsWith("oracle.sql.DATE")) { + String metaDataClassName = resultSet.getMetaData().getColumnClassName(index); + if ("java.sql.Timestamp".equals(metaDataClassName) || "oracle.sql.TIMESTAMP".equals(metaDataClassName)) { + obj = resultSet.getTimestamp(index); + } + else { + obj = resultSet.getDate(index); + } + } else if (obj instanceof Date) { + if ("java.sql.Timestamp".equals(resultSet.getMetaData().getColumnClassName(index))) { + obj = resultSet.getTimestamp(index); + } + } + + return obj; + } + + private void closeResultSet(ResultSet resultSet) { + if (resultSet != null) { + try { + resultSet.close(); + } catch (SQLException e) { + XLog.debug("Could not close JDBC ResultSet", e); + } catch (Exception e) { + XLog.debug("Unexpected exception on closing JDBC ResultSet", e); + } + } + } + + private void closeStatement(Statement statement) { + if (statement != null) { + try { + statement.close(); + } catch (SQLException e) { + XLog.debug("Could not close JDBC Statement", e); + } catch (Exception e) { + XLog.debug("Unexpected exception on closing JDBC Statement", e); + } + } + } + + private void closeConnection(Connection con) { + if (con == null) { + return; + } + + try { + con.close(); + } catch (SQLException e) { + XLog.debug("Could not close JDBC Connection", e); + } catch (Exception e) { + XLog.debug("Unexpected exception on closing JDBC Connection", e); + } + } +} diff --git a/src/main/java/ghast/database/ResultSetExtractor.java b/src/main/java/ghast/database/ResultSetExtractor.java new file mode 100644 index 0000000..d08e1a7 --- /dev/null +++ b/src/main/java/ghast/database/ResultSetExtractor.java @@ -0,0 +1,9 @@ +package ghast.database; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public interface ResultSetExtractor { + + T extractData(ResultSet rs) throws SQLException, DataAccessException; +} diff --git a/src/main/java/ghast/database/RowMapper.java b/src/main/java/ghast/database/RowMapper.java new file mode 100644 index 0000000..d8cd575 --- /dev/null +++ b/src/main/java/ghast/database/RowMapper.java @@ -0,0 +1,9 @@ +package ghast.database; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public interface RowMapper { + + T mapRow(ResultSet rs, int rowNum) throws SQLException; +} diff --git a/src/main/java/ghast/scheduler/BukkitScheduleTask.java b/src/main/java/ghast/scheduler/BukkitScheduleTask.java index a7349ca..de4a82b 100644 --- a/src/main/java/ghast/scheduler/BukkitScheduleTask.java +++ b/src/main/java/ghast/scheduler/BukkitScheduleTask.java @@ -3,18 +3,31 @@ package ghast.scheduler; import lombok.RequiredArgsConstructor; import org.bukkit.scheduler.BukkitTask; +import java.util.function.Supplier; + @RequiredArgsConstructor public class BukkitScheduleTask implements ScheduleTask { - private final BukkitTask bukkitTask; + private final Supplier generator; + private BukkitTask bukkitTask; + + @Override + public void start() { + if (isCanceled()) { + bukkitTask = generator.get(); + } + } @Override public boolean isCanceled() { - return bukkitTask.isCancelled(); + return bukkitTask == null || bukkitTask.isCancelled(); } @Override public void cancel() { - bukkitTask.cancel(); + if (bukkitTask != null) { + bukkitTask.cancel(); + bukkitTask = null; + } } } diff --git a/src/main/java/ghast/scheduler/JavaScheduleTask.java b/src/main/java/ghast/scheduler/JavaScheduleTask.java index 03ba3ff..994d21d 100644 --- a/src/main/java/ghast/scheduler/JavaScheduleTask.java +++ b/src/main/java/ghast/scheduler/JavaScheduleTask.java @@ -3,19 +3,31 @@ package ghast.scheduler; import lombok.RequiredArgsConstructor; import java.util.concurrent.Future; +import java.util.function.Supplier; @RequiredArgsConstructor public class JavaScheduleTask implements ScheduleTask { - private final Future future; + private final Supplier> generator; + private Future future; + + @Override + public void start() { + if (future == null || future.isDone()) { + future = generator.get(); + } + } @Override public boolean isCanceled() { - return future.isCancelled(); + return future == null || future.isCancelled(); } @Override public void cancel() { - future.cancel(true); + if (future != null) { + future.cancel(true); + future = null; + } } } diff --git a/src/main/java/ghast/scheduler/ScheduleManager.java b/src/main/java/ghast/scheduler/ScheduleManager.java index 75b2c16..f927259 100644 --- a/src/main/java/ghast/scheduler/ScheduleManager.java +++ b/src/main/java/ghast/scheduler/ScheduleManager.java @@ -7,7 +7,6 @@ import lombok.NoArgsConstructor; import lombok.experimental.UtilityClass; import org.bukkit.Bukkit; import org.bukkit.scheduler.BukkitScheduler; -import org.bukkit.scheduler.BukkitTask; import java.util.concurrent.*; @@ -47,7 +46,7 @@ public class ScheduleManager { return this; } - public ScheduleTask execute(Runnable runnable) { + public ScheduleTask create(Runnable runnable) { if (useBukkitScheduler) { return createBukkitSchedule(runnable); } else { @@ -55,23 +54,27 @@ public class ScheduleManager { } } + public ScheduleTask execute(Runnable runnable) { + ScheduleTask scheduleTask = create(runnable); + scheduleTask.start(); + return scheduleTask; + } + private ScheduleTask createBukkitSchedule(Runnable runnable) { BukkitScheduler bukkitScheduler = Bukkit.getScheduler(); BukkitScheduleTask resultTask; if (this.afterMs == null && this.everyMs == null) { - BukkitTask bukkitTask = bukkitScheduler.runTask(GhastTools.getPlugin(), runnable); - resultTask = new BukkitScheduleTask(bukkitTask); + resultTask = new BukkitScheduleTask(() -> bukkitScheduler.runTask(GhastTools.getPlugin(), runnable)); } else if (this.everyMs != null) { long everyTicks = this.everyMs / MS_PER_ONE_TICK; long afterTicks = this.afterMs != null ? this.afterMs / MS_PER_ONE_TICK : 0; - BukkitTask bukkitTask = bukkitScheduler.runTaskTimer(GhastTools.getPlugin(), runnable, afterTicks, - everyTicks); - resultTask = new BukkitScheduleTask(bukkitTask); + resultTask = new BukkitScheduleTask(() -> + bukkitScheduler.runTaskTimer(GhastTools.getPlugin(), runnable, afterTicks, everyTicks)); } else { long ticks = this.afterMs / MS_PER_ONE_TICK; - BukkitTask bukkitTask = bukkitScheduler.runTaskLater(GhastTools.getPlugin(), runnable, ticks); - resultTask = new BukkitScheduleTask(bukkitTask); + resultTask = new BukkitScheduleTask(() -> + bukkitScheduler.runTaskLater(GhastTools.getPlugin(), runnable, ticks)); } return resultTask; @@ -83,24 +86,23 @@ public class ScheduleManager { if (this.afterMs == null && this.everyMs == null) { executorService = Executors.newSingleThreadExecutor(THREAD_FACTORY); - Future future = executorService.submit(runnable); - resultTask = new JavaScheduleTask(future); + resultTask = new JavaScheduleTask(() -> executorService.submit(runnable)); } else if (this.everyMs != null) { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1, THREAD_FACTORY); - ScheduledFuture scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(runnable, - this.afterMs != null ? this.afterMs : 0, - everyMs, TimeUnit.MILLISECONDS); - resultTask = new JavaScheduleTask(scheduledFuture); + resultTask = new JavaScheduleTask(() -> + scheduledExecutorService.scheduleAtFixedRate(runnable, + this.afterMs != null ? this.afterMs : 0, + everyMs, TimeUnit.MILLISECONDS)); + executorService = scheduledExecutorService; } else { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1, THREAD_FACTORY); - ScheduledFuture schedule = scheduledExecutorService - .schedule(runnable, afterMs, TimeUnit.MILLISECONDS); - resultTask = new JavaScheduleTask(schedule); + resultTask = new JavaScheduleTask(() -> + scheduledExecutorService.schedule(runnable, afterMs, TimeUnit.MILLISECONDS)); executorService = scheduledExecutorService; } diff --git a/src/main/java/ghast/scheduler/ScheduleTask.java b/src/main/java/ghast/scheduler/ScheduleTask.java index 51ef029..6a45c28 100644 --- a/src/main/java/ghast/scheduler/ScheduleTask.java +++ b/src/main/java/ghast/scheduler/ScheduleTask.java @@ -2,6 +2,8 @@ package ghast.scheduler; public interface ScheduleTask { + void start(); + boolean isCanceled(); void cancel(); diff --git a/src/test/java/ghast/database/JdbcTemplateTest.java b/src/test/java/ghast/database/JdbcTemplateTest.java new file mode 100644 index 0000000..c5ff0d2 --- /dev/null +++ b/src/test/java/ghast/database/JdbcTemplateTest.java @@ -0,0 +1,245 @@ +package ghast.database; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import ghast.GhastTools; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.bukkit.plugin.Plugin; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.*; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JdbcTemplateTest { + + static final String JDBC_USER = "sa"; + static final String JDBC_PASSWORD = ""; + static final String JDBC_DB_NAME = "in_mem_db"; + static final String JDBC_URL = "jdbc:h2:mem:" + JDBC_DB_NAME + ";DB_CLOSE_DELAY=-1"; + + static final String TABLE_NAME = "TEST_TABLE"; + static final String COLUMN_ID = "ID"; + static final String COLUMN_NAME = "C_NAME"; + static final String COLUMN_VALUE = "C_VALUE"; + + static final Object[][] DATA = new Object[][]{ + { "Player 1", 100 }, { "Player 2", 250 }, + { "Player 3", 0 }, { "Player 4", 780 } + }; + + static DataSource dataSource; + JdbcTemplate jdbcTemplate; + + @BeforeAll + static void beforeAll() { + Logger logger = Logger.getLogger(JdbcTemplateTest.class.getName()); + + Plugin mockPlugin = mock(Plugin.class); + when(mockPlugin.getLogger()).thenReturn(logger); + + GhastTools.setPlugin(mockPlugin); + + JdbcDataSource jdbcDataSource = new JdbcDataSource(); + jdbcDataSource.setUser(JDBC_USER); + jdbcDataSource.setPassword(JDBC_PASSWORD); + jdbcDataSource.setURL(JDBC_URL); + + dataSource = jdbcDataSource; + } + + @BeforeEach + void before() { + jdbcTemplate = new JdbcTemplate(dataSource); + createTable(); + + String sql_head = MessageFormat.format("INSERT INTO {0} ({1}, {2}) VALUES ", + TABLE_NAME, COLUMN_NAME, COLUMN_VALUE); + StringJoiner sql_sj = new StringJoiner(", "); + for (Object[] datum : DATA) { + sql_sj.add(MessageFormat.format("( ''{0}'', {1} )", datum[0], datum[1])); + } + + jdbcTemplate.execute(sql_head + sql_sj.toString()); + } + + @AfterEach + void after() { + dropTable(); + } + + @Test + void testQuery_Single() { + String sql = MessageFormat.format("SELECT {2} FROM {0} WHERE {1} LIKE ''{3}''", + TABLE_NAME, COLUMN_NAME, COLUMN_VALUE, DATA[0][0]); + + Integer value = jdbcTemplate.query(sql, rs -> { + if (rs.next()) { + return rs.getInt(1); + } else { + return null; + } + }); + + assertEquals(DATA[0][1], value); + } + + @Test + void testQuery_List() { + String sql = MessageFormat.format("SELECT {2} FROM {0} WHERE {1} LIKE ''{3}'' OR {1} LIKE ''{4}''", + TABLE_NAME, COLUMN_NAME, COLUMN_VALUE, DATA[0][0], DATA[1][0]); + + List listValues = jdbcTemplate.query(sql, (rs, rowNum) -> rs.getInt(1)); + + assertIterableEquals(Lists.newArrayList(DATA[0][1], DATA[1][1]), listValues); + } + + @Test + void testQueryForObject() { + class Player { + String name; + int value; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Player)) return false; + Player player = (Player) o; + return new EqualsBuilder().append(value, player.value).append(name, player.name).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(name).append(value).toHashCode(); + } + } + + String sql = MessageFormat.format("SELECT {1}, {2} FROM {0} WHERE {1} LIKE ''{3}''", + TABLE_NAME, COLUMN_NAME, COLUMN_VALUE, DATA[0][0]); + + Player actualPlayer = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> { + Player player0 = new Player(); + player0.name = rs.getString(COLUMN_NAME); + player0.value = rs.getInt(COLUMN_VALUE); + + return player0; + }); + + Player expectedPlayer = new Player(); + expectedPlayer.name = (String) DATA[0][0]; + expectedPlayer.value = (int) DATA[0][1]; + + assertEquals(expectedPlayer, actualPlayer); + } + + @Test + void testQueryForMap() { + String sql = MessageFormat.format("SELECT {1}, {2} FROM {0} WHERE {1} LIKE ''{3}''", + TABLE_NAME, COLUMN_NAME, COLUMN_VALUE, DATA[0][0]); + + Map actualMap = jdbcTemplate.queryForMap(sql); + + Map expectedMap = ImmutableMap.of( + COLUMN_NAME, DATA[0][0], + COLUMN_VALUE, DATA[0][1]); + + assertIterableEquals(expectedMap.entrySet(), actualMap.entrySet()); + } + + @Test + void testQueryForList() { + String sql = MessageFormat.format("SELECT {1}, {2} FROM {0}", + TABLE_NAME, COLUMN_NAME, COLUMN_VALUE); + + List> actualMapList = jdbcTemplate.queryForList(sql); + + List> expectedMapList = Stream.of(DATA) + .map(datum -> ImmutableMap.of(COLUMN_NAME, datum[0], COLUMN_VALUE, datum[1])) + .collect(Collectors.toList()); + + assertIterableEquals(expectedMapList, actualMapList); + + } + + @Test + void testUpdate() { + String newName = "Player X"; + String sql = MessageFormat.format("UPDATE {0} SET {1} = ''{3}'' WHERE {1} LIKE ''{2}''", + TABLE_NAME, COLUMN_NAME, DATA[0][0], newName); + + int rows = jdbcTemplate.update(sql); + + assertEquals(1, rows); + + sql = MessageFormat.format("SELECT {2} FROM {0} WHERE {1} LIKE ''{3}''", + TABLE_NAME, COLUMN_NAME, COLUMN_VALUE, newName); + + Integer value = jdbcTemplate.query(sql, rs -> { + if (rs.next()) { + return rs.getInt(1); + } else { + return null; + } + }); + + assertEquals(DATA[0][1], value); + } + + private void createTable() { + jdbcTemplate.execute("CREATE TABLE " + TABLE_NAME + " (" + + COLUMN_ID + " bigint auto_increment," + + COLUMN_NAME + " varchar(16)," + + COLUMN_VALUE + " integer)"); + } + + private void dropTable() { + jdbcTemplate.execute("DROP TABLE IF EXISTS " + TABLE_NAME); + } + + @Nested + class JdbcTemplateTest_ExecuteTestCase { + + @BeforeEach + void before() { + jdbcTemplate = new JdbcTemplate(dataSource); + dropTable(); + } + + @AfterEach + void after() { + dropTable(); + } + + @Test + void test() throws ClassNotFoundException, IllegalAccessException, InstantiationException, SQLException { + createTable(); + + //region Check result + Class.forName("org.h2.Driver").newInstance(); + Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD); + ResultSet resultSet = connection.getMetaData().getTables(JDBC_DB_NAME.toUpperCase(), "PUBLIC", + TABLE_NAME.toUpperCase(), new String[]{"TABLE"}); + + assertTrue(resultSet.next()); + + resultSet.close(); + connection.close(); + //endregion + } + } +} \ No newline at end of file