diff --git a/DatabaseConnector.mpr b/DatabaseConnector.mpr index 1ad67e9..9e043eb 100644 Binary files a/DatabaseConnector.mpr and b/DatabaseConnector.mpr differ diff --git a/javasource/databaseconnector/actions/ExecuteQuery.java b/javasource/databaseconnector/actions/ExecuteQuery.java index 13e3201..d3cf9ca 100644 --- a/javasource/databaseconnector/actions/ExecuteQuery.java +++ b/javasource/databaseconnector/actions/ExecuteQuery.java @@ -16,6 +16,7 @@ import com.mendix.logging.ILogNode; import com.mendix.systemwideinterfaces.core.IContext; import com.mendix.systemwideinterfaces.core.IMendixObject; +import com.mendix.systemwideinterfaces.core.meta.IMetaObject; import com.mendix.webui.CustomJavaAction; import databaseconnector.impl.JdbcConnector; @@ -76,11 +77,11 @@ public ExecuteQuery(IContext context, String jdbcUrl, String userName, String pa public java.util.List executeAction() throws Exception { // BEGIN USER CODE - String entityName = resultObject.getMetaObject().getName(); + IMetaObject metaObject = resultObject.getMetaObject(); Stream resultStream = connector.executeQuery( - this.jdbcUrl, this.userName, this.password, entityName, this.sql, this.getContext()); + this.jdbcUrl, this.userName, this.password, metaObject, this.sql, this.getContext()); List resultList = resultStream.collect(Collectors.toList()); - logNode.info(String.format("List: %d", resultList.size())); + logNode.trace(String.format("Result list count: %d", resultList.size())); return resultList; // END USER CODE diff --git a/javasource/databaseconnector/actions/ExecuteQueryToJson.java b/javasource/databaseconnector/actions/ExecuteQueryToJson.java deleted file mode 100644 index 53d7488..0000000 --- a/javasource/databaseconnector/actions/ExecuteQueryToJson.java +++ /dev/null @@ -1,91 +0,0 @@ -// This file was generated by Mendix Modeler. -// -// WARNING: Only the following code will be retained when actions are regenerated: -// - the import list -// - the code between BEGIN USER CODE and END USER CODE -// - the code between BEGIN EXTRA CODE and END EXTRA CODE -// Other code you write will be lost the next time you deploy the project. -// Special characters, e.g., é, ö, à, etc. are supported in comments. - -package databaseconnector.actions; - -import com.mendix.core.Core; -import com.mendix.logging.ILogNode; -import com.mendix.systemwideinterfaces.core.IContext; -import com.mendix.webui.CustomJavaAction; -import databaseconnector.impl.JdbcConnector; - -/** - *

- * This Java action provides a consistent environment for Mendix projects to perform an arbitrary SELECT SQL query on relational external databases. - * JDBC (Java Database Connectivity) API, a standard Java API, is used when this Java action attempts - * to connect with a Relational Database for which a JDBC driver exists. - * The JDBC drivers for the databases you want to connect to, must be placed inside the userlib directory of a project. - *

- * - * Do not use this Java action for INSERT, UPDATE, DELETE or DDL queries. - * This action returns a list of Mendix objects based on the JDBC result set. - * The jdbcUrl argument must specify a database URL address that points to your relational database and is dependent - * upon the particular database and JDBC driver. It will always begin with "jdbc:" protocol, but the rest is up to particular vendor. - * For example 'jdbc:mysql://hostname/databaseName' jdbcUrl format can be used for MySQL databases. - * Note: Proper security must be applied as this action can allow SQL Injection in your Mendix application. - * - * @see JdbcConnector - * @since Mendix World 2016 - * @param jdbcUrl - * A database URL address that points to your database. - * - * @param userName - * The user name for logging into the database, relative to the jdbcUrl argument. - * - * @param password - * The password for logging into the database, relative to the jdbcUrl argument. - * - * @param sql - * The SELECT query to be performed, relative to the database type. - * - * @param resultObject - * An instance of the resulting object. This instance is used only for defining the type of object to be returned. - * - * @return > - * SELECT Query result as a list of objects. - */ -public class ExecuteQueryToJson extends CustomJavaAction -{ - private String jdbcUrl; - private String userName; - private String password; - private String sql; - - public ExecuteQueryToJson(IContext context, String jdbcUrl, String userName, String password, String sql) - { - super(context); - this.jdbcUrl = jdbcUrl; - this.userName = userName; - this.password = password; - this.sql = sql; - } - - @Override - public String executeAction() throws Exception - { - // BEGIN USER CODE - return connector.executeQueryToJson(this.jdbcUrl, this.userName, this.password, this.sql, this.getContext()); - // END USER CODE - } - - /** - * Returns a string representation of this action - */ - @Override - public String toString() - { - return "ExecuteQueryToJson"; - } - - // BEGIN EXTRA CODE - private final ILogNode logNode = Core.getLogger(this.getClass().getName()); - - private final JdbcConnector connector = new JdbcConnector(logNode); - // END EXTRA CODE -} diff --git a/javasource/databaseconnector/impl/ColumnInfo.java b/javasource/databaseconnector/impl/ColumnInfo.java index 7b9542b..33c2a5f 100644 --- a/javasource/databaseconnector/impl/ColumnInfo.java +++ b/javasource/databaseconnector/impl/ColumnInfo.java @@ -1,12 +1,16 @@ package databaseconnector.impl; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive.PrimitiveType; + public class ColumnInfo { - private int index; - private String name; + private final int index; + private final String name; + private final PrimitiveType type; - public ColumnInfo(int index, String name) { + public ColumnInfo(final int index, final String name, final PrimitiveType type) { this.index = index; this.name = name; + this.type = type; } public int getIndex() { @@ -16,4 +20,8 @@ public int getIndex() { public String getName() { return name; } + + public PrimitiveType getType() { + return type; + } } diff --git a/javasource/databaseconnector/impl/JdbcConnectionManager.java b/javasource/databaseconnector/impl/JdbcConnectionManager.java index 3378ee1..0c4406a 100644 --- a/javasource/databaseconnector/impl/JdbcConnectionManager.java +++ b/javasource/databaseconnector/impl/JdbcConnectionManager.java @@ -43,10 +43,10 @@ public Connection getConnection(final String jdbcUrl, final String userName, fin final Integer connPoolKey = toConnPoolKey(jdbcUrl, userName); final HikariDataSource dataSource = connectionPool.computeIfAbsent(connPoolKey, k -> { - logNode.info(String.format("Creating data source in connection pool for [url=%s, user=%s]", jdbcUrl, userName)); + logNode.trace(String.format("Creating data source in connection pool for [url=%s, user=%s]", jdbcUrl, userName)); return createHikariDataSource(jdbcUrl, userName, password); }); - logNode.info(String.format("Getting connection from data source in connection pool for [url=%s, user=%s]", jdbcUrl, userName)); + logNode.trace(String.format("Getting connection from data source in connection pool for [url=%s, user=%s]", jdbcUrl, userName)); return dataSource.getConnection(); } @@ -61,7 +61,7 @@ private synchronized void initializeDrivers() { ServiceLoader loader = ServiceLoader.load(Driver.class); Stream driverNames = StreamSupport.stream(loader.spliterator(), false).map(a -> a.getClass().getName()); String logMessage = driverNames.collect(Collectors.joining(", ", "Found JDBC Drivers: ", "")); - logNode.info(logMessage); + logNode.trace(logMessage); hasDriversInitialized = true; } } diff --git a/javasource/databaseconnector/impl/JdbcConnector.java b/javasource/databaseconnector/impl/JdbcConnector.java index 571d87e..9b138a9 100644 --- a/javasource/databaseconnector/impl/JdbcConnector.java +++ b/javasource/databaseconnector/impl/JdbcConnector.java @@ -1,74 +1,93 @@ package databaseconnector.impl; +import java.io.ByteArrayInputStream; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.function.Supplier; import java.util.stream.Stream; -import org.json.JSONArray; -import org.json.JSONObject; - +import com.mendix.core.objectmanagement.member.MendixHashString; import com.mendix.logging.ILogNode; import com.mendix.systemwideinterfaces.core.IContext; import com.mendix.systemwideinterfaces.core.IMendixObject; +import com.mendix.systemwideinterfaces.core.meta.IMetaObject; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive.PrimitiveType; import databaseconnector.interfaces.ConnectionManager; import databaseconnector.interfaces.ObjectInstantiator; +/** + * JdbcConnector implements the execute query (and execute statement) functionality, and returns a {@link Stream} of {@link IMendixObject}s. + */ public class JdbcConnector { private final ILogNode logNode; - private ObjectInstantiator objectInstantiator; - private ConnectionManager connectionManager; + private final ObjectInstantiator objectInstantiator; + private final ConnectionManager connectionManager; - public JdbcConnector(final ILogNode logNode, ObjectInstantiator objectInstantiator, ConnectionManager connectionManager) { + public JdbcConnector(final ILogNode logNode, final ObjectInstantiator objectInstantiator, final ConnectionManager connectionManager) { this.logNode = logNode; this.objectInstantiator = objectInstantiator; this.connectionManager = connectionManager; } - public JdbcConnector(ILogNode logNode) { + public JdbcConnector(final ILogNode logNode) { this(logNode, new ObjectInstantiatorImpl(), ConnectionManagerSingleton.getInstance()); } - public Stream executeQuery(String jdbcUrl, String userName, String password, String entityName, String sql, - IContext context) throws SQLException { - Function, IMendixObject> toMendixObject = columns -> { + public Stream executeQuery(final String jdbcUrl, final String userName, final String password, final IMetaObject metaObject, final String sql, + final IContext context) throws SQLException { + String entityName = metaObject.getName(); + + Function>, IMendixObject> toMendixObject = columns -> { IMendixObject obj = objectInstantiator.instantiate(context, entityName); - columns.forEach((n, v) -> obj.setValue(context, n, v)); - logNode.info("obj: " + obj); + + BiConsumer> setMemberValue = (name, value) -> { + PrimitiveType type = metaObject.getMetaPrimitive(name).getType(); + // convert to suitable value (different for Binary type) + Function toSuitableValue = toSuitableValue(type); + // for Boolean type, convert null to false + Supplier defaultValue = () -> type == PrimitiveType.Boolean ? Boolean.FALSE : null; + // apply two functions declared above + Object convertedValue = value.map(toSuitableValue).orElseGet(defaultValue); + // update object with converted value + if (type == PrimitiveType.HashString) + ((MendixHashString) obj.getMember(context, name)).setInitialHash((String) convertedValue); + else + obj.setValue(context, name, convertedValue); + }; + + columns.forEach(setMemberValue); + logNode.trace("Instantiated object: " + obj); return obj; }; - Stream> stream = executeQuery(jdbcUrl, userName, password, sql); - - return stream.map(toMendixObject); + return executeQuery(jdbcUrl, userName, password, metaObject, sql).map(toMendixObject); } - public String executeQueryToJson(String jdbcUrl, String userName, String password, String sql, IContext context) throws SQLException { - Stream> stream = executeQuery(jdbcUrl, userName, password, sql); - Stream jsonObjects = stream.map(JSONObject::new); - - return new JSONArray(jsonObjects.collect(Collectors.toList())).toString(); + private Function toSuitableValue(final PrimitiveType type) { + return v -> type == PrimitiveType.Binary ? new ByteArrayInputStream((byte[]) v) : v; } - public Stream> executeQuery(String jdbcUrl, String userName, String password, String sql) throws SQLException { - logNode.info(String.format("executeQuery: %s, %s, %s", jdbcUrl, userName, sql)); + private Stream>> executeQuery(final String jdbcUrl, final String userName, final String password, final IMetaObject metaObject, final String sql) throws SQLException { + logNode.trace(String.format("executeQuery: %s, %s, %s", jdbcUrl, userName, sql)); try (Connection connection = connectionManager.getConnection(jdbcUrl, userName, password); PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { - ResultSetReader resultSetReader = new ResultSetReader(resultSet); + ResultSetReader resultSetReader = new ResultSetReader(resultSet, metaObject); return resultSetReader.readAll().stream(); } } - public long executeStatement(String jdbcUrl, String userName, String password, String sql) throws SQLException { - logNode.info(String.format("executeStatement: %s, %s, %s", jdbcUrl, userName, sql)); + public long executeStatement(final String jdbcUrl, final String userName, final String password, final String sql) throws SQLException { + logNode.trace(String.format("executeStatement: %s, %s, %s", jdbcUrl, userName, sql)); try (Connection connection = connectionManager.getConnection(jdbcUrl, userName, password); PreparedStatement preparedStatement = connection.prepareStatement(sql)) { diff --git a/javasource/databaseconnector/impl/ObjectInstantiatorImpl.java b/javasource/databaseconnector/impl/ObjectInstantiatorImpl.java index 5c7cbe8..df2fb1a 100644 --- a/javasource/databaseconnector/impl/ObjectInstantiatorImpl.java +++ b/javasource/databaseconnector/impl/ObjectInstantiatorImpl.java @@ -9,7 +9,7 @@ class ObjectInstantiatorImpl implements ObjectInstantiator { @Override - public IMendixObject instantiate(IContext context, String entityName) { + public IMendixObject instantiate(final IContext context, final String entityName) { return Core.instantiate(context, entityName); } } \ No newline at end of file diff --git a/javasource/databaseconnector/impl/ResultSetIterator.java b/javasource/databaseconnector/impl/ResultSetIterator.java index 60d595e..496995a 100644 --- a/javasource/databaseconnector/impl/ResultSetIterator.java +++ b/javasource/databaseconnector/impl/ResultSetIterator.java @@ -5,24 +5,30 @@ import java.sql.SQLException; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.Spliterator; import java.util.Spliterators; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import com.mendix.systemwideinterfaces.core.meta.IMetaObject; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive; + /** * ResultSetIterator implements {@link Iterator} interface. It wraps {@link ResultSet} into a stream for more convenient usage. Along * with that, it provides information about columns of the given result set. */ public class ResultSetIterator implements Iterator { - private final ResultSet resultSet; private final List columnInfos; + private final IMetaObject metaObject; - public ResultSetIterator(final ResultSet resultSet) { + public ResultSetIterator(final ResultSet resultSet, final IMetaObject metaObject) { this.resultSet = resultSet; + this.metaObject = metaObject; this.columnInfos = createColumnInfos(resultSet); } @@ -36,12 +42,24 @@ private List createColumnInfos(final ResultSet resultSet) { } } - private ColumnInfo getColumnInfo(int index) { + private ColumnInfo getColumnInfo(final int index) { + final String columnName; + try { - return new ColumnInfo(index, resultSet.getMetaData().getColumnName(index)); + columnName = resultSet.getMetaData().getColumnName(index); } catch (Exception e) { throw new RuntimeException(e); } + + final Predicate caseInsensitiveName = (IMetaPrimitive mp) -> mp.getName().equalsIgnoreCase(columnName); + final Optional primitive = metaObject.getMetaPrimitives().stream().filter(caseInsensitiveName).findFirst(); + + final IMetaPrimitive.PrimitiveType type = primitive.orElseThrow(() -> { + final String msg = "The entity type '%s' does not contain the primitive '%s' as specified in the query."; + throw new RuntimeException(String.format(msg, metaObject.getName(), columnName)); + }).getType(); + + return new ColumnInfo(index, columnName, type); } @Override diff --git a/javasource/databaseconnector/impl/ResultSetReader.java b/javasource/databaseconnector/impl/ResultSetReader.java index 23ccf95..762e587 100644 --- a/javasource/databaseconnector/impl/ResultSetReader.java +++ b/javasource/databaseconnector/impl/ResultSetReader.java @@ -1,12 +1,20 @@ package databaseconnector.impl; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Date; import java.util.List; import java.util.Map; - +import java.util.Optional; +import java.util.TimeZone; import java.util.function.Function; -import java.util.stream.Collectors; + +import com.mendix.systemwideinterfaces.core.meta.IMetaObject; /** * ResultSetReader converts a given instance of {@link ResultSet} into a list of instances of Map, with key for column name @@ -14,9 +22,10 @@ */ public class ResultSetReader { private final ResultSetIterator rsIter; + private final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - public ResultSetReader(final ResultSet resultSet) { - this.rsIter = new ResultSetIterator(resultSet); + public ResultSetReader(final ResultSet resultSet, final IMetaObject metaObject) { + this.rsIter = new ResultSetIterator(resultSet, metaObject); } /** @@ -25,23 +34,59 @@ public ResultSetReader(final ResultSet resultSet) { * @return list of records, where records are represented as map * @throws SQLException */ - public List> readAll() throws SQLException { + public List>> readAll() throws SQLException { // Force the stream to read the whole ResultSet, so that the connection can be closed. - return rsIter.stream().map(rs -> getRowResult(rs)).collect(Collectors.toList()); + return rsIter.stream().map(this::getRowResult).collect(toList()); } - private Map getRowResult(final ResultSet rs) { - return rsIter.getColumnInfos().collect(Collectors.toMap(ColumnInfo::getName, curryGetColumnResult(rs))); + /** + * The Optional type for value is used because Collectors.toMap does not accept null for a value. + */ + private Map> getRowResult(final ResultSet rs) { + return rsIter.getColumnInfos().collect(toMap(ColumnInfo::getName, curryGetColumnResult(rs))); } - private Function curryGetColumnResult(final ResultSet rs) { + private Function> curryGetColumnResult(final ResultSet rs) { return ci -> getColumnResult(rs, ci); } - private Object getColumnResult(final ResultSet rs, final ColumnInfo columnInfo) { + @SuppressWarnings("deprecation") + private Optional getColumnResult(final ResultSet rs, final ColumnInfo columnInfo) { try { - final Object columnValue = rs.getObject(columnInfo.getIndex()); - return columnValue; + final int columnIndex = columnInfo.getIndex(); + Object columnValue = null; + switch (columnInfo.getType()) { + case Integer: + columnValue = rs.getInt(columnIndex); + break; + case AutoNumber: + case Long: + columnValue = rs.getLong(columnIndex); + break; + case DateTime: + Timestamp timeStamp = rs.getTimestamp(columnIndex, calendar); + columnValue = (timeStamp != null) ? new Date(timeStamp.getTime()) : null; + break; + case Boolean: + columnValue = rs.getBoolean(columnIndex); + break; + case Decimal: + columnValue = rs.getBigDecimal(columnIndex); + break; + case Float: + case Currency: + columnValue = rs.getDouble(columnIndex); + break; + case HashString: + case Enum: + case String: + columnValue = rs.getString(columnIndex); + break; + case Binary: + columnValue = rs.getBytes(columnIndex); + break; + } + return rs.wasNull() ? Optional.empty() : Optional.ofNullable(columnValue); } catch (SQLException e) { throw new RuntimeException(e); } diff --git a/javasource/databaseconnector/interfaces/ConnectionManager.java b/javasource/databaseconnector/interfaces/ConnectionManager.java index 00353d6..70b5bda 100644 --- a/javasource/databaseconnector/interfaces/ConnectionManager.java +++ b/javasource/databaseconnector/interfaces/ConnectionManager.java @@ -4,5 +4,5 @@ import java.sql.SQLException; public interface ConnectionManager { - Connection getConnection(String jdbcUrl, String userName, String password) throws SQLException; + Connection getConnection(final String jdbcUrl, final String userName, final String password) throws SQLException; } diff --git a/javasource/databaseconnector/interfaces/ObjectInstantiator.java b/javasource/databaseconnector/interfaces/ObjectInstantiator.java index 57ffef7..e760813 100644 --- a/javasource/databaseconnector/interfaces/ObjectInstantiator.java +++ b/javasource/databaseconnector/interfaces/ObjectInstantiator.java @@ -4,5 +4,5 @@ import com.mendix.systemwideinterfaces.core.IMendixObject; public interface ObjectInstantiator { - IMendixObject instantiate(IContext context, String entityName); + IMendixObject instantiate(final IContext context, final String entityName); } diff --git a/javasource/databaseconnectortest/actions/AssertEqualsListEntityValues.java b/javasource/databaseconnectortest/actions/AssertEqualsListEntityValues.java new file mode 100644 index 0000000..8a24cbc --- /dev/null +++ b/javasource/databaseconnectortest/actions/AssertEqualsListEntityValues.java @@ -0,0 +1,133 @@ +// This file was generated by Mendix Modeler. +// +// WARNING: Only the following code will be retained when actions are regenerated: +// - the import list +// - the code between BEGIN USER CODE and END USER CODE +// - the code between BEGIN EXTRA CODE and END EXTRA CODE +// Other code you write will be lost the next time you deploy the project. +// Special characters, e.g., é, ö, à, etc. are supported in comments. + +package databaseconnectortest.actions; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.io.IOUtils; +import com.mendix.systemwideinterfaces.core.IContext; +import com.mendix.systemwideinterfaces.core.IMendixObject; +import com.mendix.systemwideinterfaces.core.IMendixObjectMember; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive.PrimitiveType; +import com.mendix.webui.CustomJavaAction; +import unittesting.proxies.microflows.Microflows; + +public class AssertEqualsListEntityValues extends CustomJavaAction +{ + private java.util.List Expected; + private java.util.List Actual; + + public AssertEqualsListEntityValues(IContext context, java.util.List Expected, java.util.List Actual) + { + super(context); + this.Expected = Expected; + this.Actual = Actual; + } + + @Override + public Boolean executeAction() throws Exception + { + // BEGIN USER CODE + Consumer assertMessage = a -> + Microflows.assertTrue2(getContext(), false, "The values of some entities in both lists do not match.\n" + a); + Optional notEqualMessage = compare(Expected, Actual); + notEqualMessage.ifPresent(assertMessage); + + return !notEqualMessage.isPresent(); + // END USER CODE + } + + /** + * Returns a string representation of this action + */ + @Override + public String toString() + { + return "AssertEqualsListEntityValues"; + } + + // BEGIN EXTRA CODE + private Optional compare(List expected, List actual) { + if (expected.size() != actual.size()) + return Optional.of(format("The list of expected items contains %s items," + + " the list of actual items %s", expected.size(), actual.size())); + + IntStream range = IntStream.range(0, expected.size()); + IntFunction> compare = a -> compare(a + 1, expected.get(a), actual.get(a)); + String message = range.mapToObj(compare).flatMap(messagesFilter).collect(joining("," + System.lineSeparator())); + + return isEmpty(message) ? Optional.empty() : Optional.of(message); + } + + private Optional compare(int objectNr, IMendixObject expected, IMendixObject actual) { + Function, Optional> compare = expectedMember -> { + PrimitiveType primitiveType = expected.getMetaObject().getMetaPrimitive(expectedMember.getName()).getType(); + IMendixObjectMember actualMember = actual.getMember(getContext(), expectedMember.getName()); + return compare(primitiveType, expectedMember, actualMember); + }; + Stream> potentialMessages = expected.getPrimitives(getContext()).stream().map(compare); + String message = potentialMessages.flatMap(messagesFilter).collect(joining(", ")); + + return isEmpty(message) ? Optional.empty() : Optional.of(format("Row %s: ", objectNr) + message); + } + + private Optional compare(PrimitiveType primitiveType, IMendixObjectMember expected, IMendixObjectMember actual) { + Object expectedValue = toComparableValue(expected.getValue(getContext())); + Object actualValue = toComparableValue(actual.getValue(getContext())); + boolean isEqual; + + if (expectedValue != null && actualValue != null) { + switch (primitiveType) { + case Binary: + isEqual = Arrays.equals((byte[]) expectedValue, (byte[]) actualValue); + break; + case Decimal: + isEqual = ((BigDecimal) expectedValue).compareTo((BigDecimal) actualValue) == 0; + break; + default: + isEqual = expectedValue.equals(actualValue); + } + } else + isEqual = expectedValue == null && actualValue == null; + + return isEqual ? Optional.empty() : Optional.of(format("%s (%s != %s)", expected.getName(), expectedValue, actualValue)); + } + + /** + * Convert the value to a comparable value, like InputStream to byte array. + */ + private Object toComparableValue(Object value) { + if (value instanceof InputStream) try { + return IOUtils.toByteArray((InputStream) value); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (NullPointerException e) { + // this will occur in some kind of InputStream where an underlying input stream is null. + return null; + } + + return value; + } + + private Function, Stream> messagesFilter = a -> a.map(Stream::of).orElseGet(Stream::empty); + // END EXTRA CODE +} diff --git a/javasource/databaseconnectortest/actions/HasEqualListEntityValues.java b/javasource/databaseconnectortest/actions/HasEqualListEntityValues.java deleted file mode 100644 index 86826ad..0000000 --- a/javasource/databaseconnectortest/actions/HasEqualListEntityValues.java +++ /dev/null @@ -1,59 +0,0 @@ -// This file was generated by Mendix Modeler. -// -// WARNING: Only the following code will be retained when actions are regenerated: -// - the import list -// - the code between BEGIN USER CODE and END USER CODE -// - the code between BEGIN EXTRA CODE and END EXTRA CODE -// Other code you write will be lost the next time you deploy the project. -// Special characters, e.g., é, ö, à, etc. are supported in comments. - -package databaseconnectortest.actions; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import com.mendix.systemwideinterfaces.core.IContext; -import com.mendix.systemwideinterfaces.core.IMendixObject; -import com.mendix.systemwideinterfaces.core.IMendixObjectMember; -import com.mendix.webui.CustomJavaAction; - -public class HasEqualListEntityValues extends CustomJavaAction -{ - private java.util.List Expected; - private java.util.List Actual; - - public HasEqualListEntityValues(IContext context, java.util.List Expected, java.util.List Actual) - { - super(context); - this.Expected = Expected; - this.Actual = Actual; - } - - @Override - public Boolean executeAction() throws Exception - { - // BEGIN USER CODE - return getMemberArray(Expected).equals(getMemberArray(Actual)); - // END USER CODE - } - - /** - * Returns a string representation of this action - */ - @Override - public String toString() - { - return "HasEqualListEntityValues"; - } - - // BEGIN EXTRA CODE - private List getMemberArray(List mendixObjects) { - return mendixObjects.stream().map(this::getMembers).collect(Collectors.toList()); - } - - private Map> getMembers(IMendixObject mendixObject) { - return mendixObject.getMembers(getContext()); - } - - // END EXTRA CODE -} diff --git a/javasource/databaseconnectortest/actions/SetBinaryTextValue.java b/javasource/databaseconnectortest/actions/SetBinaryTextValue.java new file mode 100644 index 0000000..58ca76e --- /dev/null +++ b/javasource/databaseconnectortest/actions/SetBinaryTextValue.java @@ -0,0 +1,53 @@ +// This file was generated by Mendix Modeler. +// +// WARNING: Only the following code will be retained when actions are regenerated: +// - the import list +// - the code between BEGIN USER CODE and END USER CODE +// - the code between BEGIN EXTRA CODE and END EXTRA CODE +// Other code you write will be lost the next time you deploy the project. +// Special characters, e.g., é, ö, à, etc. are supported in comments. + +package databaseconnectortest.actions; + +import com.mendix.core.objectmanagement.member.MendixBinary; +import com.mendix.systemwideinterfaces.core.IContext; +import com.mendix.systemwideinterfaces.core.IMendixObject; +import com.mendix.webui.CustomJavaAction; + +public class SetBinaryTextValue extends CustomJavaAction +{ + private IMendixObject instance; + private String member; + private String value; + + public SetBinaryTextValue(IContext context, IMendixObject instance, String member, String value) + { + super(context); + this.instance = instance; + this.member = member; + this.value = value; + } + + @Override + public Boolean executeAction() throws Exception + { + // BEGIN USER CODE + MendixBinary binaryMember = (MendixBinary) instance.getMember(getContext(), member); + binaryMember.setValue(getContext(), value.getBytes()); + + return true; + // END USER CODE + } + + /** + * Returns a string representation of this action + */ + @Override + public String toString() + { + return "SetBinaryTextValue"; + } + + // BEGIN EXTRA CODE + // END EXTRA CODE +} diff --git a/javasource/databaseconnectortest/actions/SetHashedValue.java b/javasource/databaseconnectortest/actions/SetHashedValue.java new file mode 100644 index 0000000..4e73764 --- /dev/null +++ b/javasource/databaseconnectortest/actions/SetHashedValue.java @@ -0,0 +1,53 @@ +// This file was generated by Mendix Modeler. +// +// WARNING: Only the following code will be retained when actions are regenerated: +// - the import list +// - the code between BEGIN USER CODE and END USER CODE +// - the code between BEGIN EXTRA CODE and END EXTRA CODE +// Other code you write will be lost the next time you deploy the project. +// Special characters, e.g., é, ö, à, etc. are supported in comments. + +package databaseconnectortest.actions; + +import com.mendix.core.objectmanagement.member.MendixHashString; +import com.mendix.systemwideinterfaces.core.IContext; +import com.mendix.systemwideinterfaces.core.IMendixObject; +import com.mendix.webui.CustomJavaAction; + +public class SetHashedValue extends CustomJavaAction +{ + private IMendixObject instance; + private String member; + private String hashedValue; + + public SetHashedValue(IContext context, IMendixObject instance, String member, String hashedValue) + { + super(context); + this.instance = instance; + this.member = member; + this.hashedValue = hashedValue; + } + + @Override + public Boolean executeAction() throws Exception + { + // BEGIN USER CODE + MendixHashString hashStringMember = (MendixHashString) instance.getMember(getContext(), member); + hashStringMember.setInitialHash(hashedValue); + + return true; + // END USER CODE + } + + /** + * Returns a string representation of this action + */ + @Override + public String toString() + { + return "SetHashedValue"; + } + + // BEGIN EXTRA CODE + // END EXTRA CODE +} diff --git a/javasource/databaseconnectortest/actions/SetNullValues.java b/javasource/databaseconnectortest/actions/SetNullValues.java new file mode 100644 index 0000000..c61687f --- /dev/null +++ b/javasource/databaseconnectortest/actions/SetNullValues.java @@ -0,0 +1,54 @@ +// This file was generated by Mendix Modeler. +// +// WARNING: Only the following code will be retained when actions are regenerated: +// - the import list +// - the code between BEGIN USER CODE and END USER CODE +// - the code between BEGIN EXTRA CODE and END EXTRA CODE +// Other code you write will be lost the next time you deploy the project. +// Special characters, e.g., é, ö, à, etc. are supported in comments. + +package databaseconnectortest.actions; + +import java.util.function.Predicate; +import com.mendix.systemwideinterfaces.core.IContext; +import com.mendix.systemwideinterfaces.core.IMendixObject; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive.PrimitiveType; +import com.mendix.webui.CustomJavaAction; + +public class SetNullValues extends CustomJavaAction +{ + private IMendixObject instance; + + public SetNullValues(IContext context, IMendixObject instance) + { + super(context); + this.instance = instance; + } + + @Override + public Boolean executeAction() throws Exception + { + // BEGIN USER CODE + instance.getMetaObject().getMetaPrimitives().stream() + .filter(isNotBoolean).filter(isNotHashString) + .forEach(p -> instance.setValue(getContext(), p.getName(), null)); + + return true; + // END USER CODE + } + + /** + * Returns a string representation of this action + */ + @Override + public String toString() + { + return "SetNullValues"; + } + + // BEGIN EXTRA CODE + private Predicate isNotBoolean = a -> a.getType() != PrimitiveType.Boolean; + private Predicate isNotHashString = a -> a.getType() != PrimitiveType.HashString; + // END EXTRA CODE +} diff --git a/javasource/databaseconnectortest/test/ConnectionManagerTest.java b/javasource/databaseconnectortest/test/ConnectionManagerTest.java index 3240311..9f61e15 100644 --- a/javasource/databaseconnectortest/test/ConnectionManagerTest.java +++ b/javasource/databaseconnectortest/test/ConnectionManagerTest.java @@ -4,15 +4,16 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.times; -import com.mendix.logging.ILogNode; +import java.sql.Connection; +import java.sql.SQLException; -import databaseconnector.impl.JdbcConnectionManager; -import databaseconnector.interfaces.ConnectionManager; import org.junit.Test; import org.mockito.Mockito; -import java.sql.Connection; -import java.sql.SQLException; +import com.mendix.logging.ILogNode; + +import databaseconnector.impl.JdbcConnectionManager; +import databaseconnector.interfaces.ConnectionManager; public class ConnectionManagerTest { private static final String jdbcUrl = "jdbc:hsqldb:mem:testcase;shutdown=true"; @@ -23,19 +24,28 @@ private ConnectionManager newConnManager(final ILogNode logNode) { return new JdbcConnectionManager(logNode); } +// @Before +// public void setUp() { +// ILogNode logNode = Mockito.mock(ILogNode.class); +// ILogManager logManager = Mockito.mock(ILogManager.class); +// Mockito.when(logManager.getLogNode(Matchers.anyString())).thenReturn(logNode); +// StaticLoggerBinder.init(logManager); +// } + @Test public void testGetConnection() throws SQLException { ILogNode logger = Mockito.mock(ILogNode.class); ConnectionManager manager = newConnManager(logger); Connection conn1 = manager.getConnection(jdbcUrl, userName, password); - Mockito.verify(logger, times(3)).info(anyString()); // One log message is printed in JdbcConnectionManager.initializeDrivers. + Mockito.verify(logger, times(3)).trace(anyString()); // One log message is printed in JdbcConnectionManager.initializeDrivers. conn1.close(); Connection conn2 = manager.getConnection(jdbcUrl, userName, password); - Mockito.verify(logger, times(4)).info(anyString()); + Mockito.verify(logger, times(4)).trace(anyString()); conn2.close(); assertNotEquals(conn1, conn2); } + } diff --git a/javasource/databaseconnectortest/test/JdbcConnectorTest.java b/javasource/databaseconnectortest/test/JdbcConnectorTest.java index d95e4be..60a10be 100644 --- a/javasource/databaseconnectortest/test/JdbcConnectorTest.java +++ b/javasource/databaseconnectortest/test/JdbcConnectorTest.java @@ -16,18 +16,27 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.stream.Stream; import org.junit.Rule; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import com.mendix.logging.ILogNode; import com.mendix.systemwideinterfaces.core.IContext; import com.mendix.systemwideinterfaces.core.IMendixObject; +import com.mendix.systemwideinterfaces.core.meta.IMetaObject; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive; +import static com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive.PrimitiveType.String; +import static com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive.PrimitiveType.Boolean; import databaseconnector.impl.JdbcConnector; import databaseconnector.interfaces.ConnectionManager; @@ -53,6 +62,28 @@ public class JdbcConnectorTest { @InjectMocks private JdbcConnector jdbcConnector; + private IMetaObject mockIMetaObject(SimpleEntry... entries) { + IMetaObject metaObject = mock(IMetaObject.class); + when(metaObject.getName()).thenReturn(entityName); + + final Collection primitives = new ArrayList<>(); + + Arrays.asList(entries).forEach(entry -> { + IMetaPrimitive metaPrimitive = mock(IMetaPrimitive.class); + when(metaPrimitive.getName()).thenReturn(entry.getKey()); + when(metaPrimitive.getType()).thenReturn(entry.getValue()); + when(metaObject.getMetaPrimitive(entry.getKey())).thenReturn(metaPrimitive); + primitives.add(metaPrimitive); + }); + + Mockito.>when(metaObject.getMetaPrimitives()).thenReturn(primitives); + return metaObject; + } + + private SimpleEntry entry(String name, IMetaPrimitive.PrimitiveType type) { + return new SimpleEntry<>(name, type); + } + @Test public void testStatementCreationException() throws SQLException { Exception testException = new SQLException("Test Exception Text"); @@ -60,7 +91,7 @@ public class JdbcConnectorTest { when(connection.prepareStatement(anyString())).thenThrow(testException); try { - jdbcConnector.executeQuery(jdbcUrl, userName, password, entityName, sqlQuery, context); + jdbcConnector.executeQuery(jdbcUrl, userName, password, mockIMetaObject(), sqlQuery, context); fail("An exception should occur!"); } catch(SQLException sqlException) {} @@ -78,7 +109,7 @@ public class JdbcConnectorTest { when(resultSet.next()).thenReturn(true, false); when(objectInstantiator.instantiate(anyObject(), anyString())).thenThrow(testException); - Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, entityName, sqlQuery, context); + Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, mockIMetaObject(), sqlQuery, context); try { result.count(); fail("An exception should occur!"); @@ -96,7 +127,7 @@ public class JdbcConnectorTest { when(preparedStatement.executeQuery()).thenReturn(resultSet); when(resultSet.getMetaData()).thenReturn(resultSetMetaData); - Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, entityName, sqlQuery, context); + Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, mockIMetaObject(), sqlQuery, context); assertEquals(0, result.count()); @@ -115,17 +146,18 @@ public class JdbcConnectorTest { when(resultSetMetaData.getColumnName(anyInt())).thenReturn("a", "b"); when(resultSetMetaData.getColumnCount()).thenReturn(2); when(resultSet.getMetaData()).thenReturn(resultSetMetaData); - when(resultSet.getObject(anyInt())).thenReturn(true); + when(resultSet.getBoolean(anyInt())).thenReturn(true); when(resultSet.next()).thenReturn(true, true, true, true, false); - Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, entityName, sqlQuery, context); + IMetaObject metaObject = mockIMetaObject(entry("a", Boolean), entry("b", Boolean)); + Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, metaObject, sqlQuery, context); assertEquals(4, result.count()); verify(objectInstantiator, times(4)).instantiate(context, entityName); verify(connectionManager).getConnection(jdbcUrl, userName, password); verify(connection).prepareStatement(sqlQuery); - verify(resultSet).getMetaData(); + verify(resultSet, times(3)).getMetaData(); verify(resultSetMetaData).getColumnCount(); verify(resultSet, times(5)).next(); } @@ -150,10 +182,11 @@ public class JdbcConnectorTest { when(resultSetMetaData.getColumnName(2)).thenReturn(columnName2); when(resultSet.getMetaData()).thenReturn(resultSetMetaData); when(resultSet.next()).thenReturn(true, true, false); - when(resultSet.getObject(1)).thenReturn(row1Value1, row2Value1); - when(resultSet.getObject(2)).thenReturn(row1Value2, row2Value2); + when(resultSet.getString(1)).thenReturn(row1Value1, row2Value1); + when(resultSet.getString(2)).thenReturn(row1Value2, row2Value2); - Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, entityName, sqlQuery, context); + IMetaObject metaObject = mockIMetaObject(entry(columnName1, String), entry(columnName2, String)); + Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, metaObject, sqlQuery, context); assertEquals(2, result.count()); verify(resultObject1).setValue(context, columnName1, row1Value1); @@ -164,6 +197,31 @@ public class JdbcConnectorTest { verify(resultSet, times(3)).next(); } + @Test public void testResultForBoolean() throws SQLException { + IMendixObject resultObject = mock(IMendixObject.class); + + when(connectionManager.getConnection(anyString(), anyString(), anyString())).thenReturn(connection); + when(connection.prepareStatement(anyString())).thenReturn(preparedStatement); + when(preparedStatement.executeQuery()).thenReturn(resultSet); + when(objectInstantiator.instantiate(anyObject(), anyString())).thenReturn(resultObject); + + when(resultSetMetaData.getColumnCount()).thenReturn(1); + when(resultSetMetaData.getColumnName(1)).thenReturn("Boolean"); + when(resultSet.getMetaData()).thenReturn(resultSetMetaData); + when(resultSet.next()).thenReturn(true, false); + + // As Mockito does not allow to return null, we should not mock getting Boolean + // when(resultSet.getBoolean(1)).thenReturn(null); + + IMetaObject metaObject = mockIMetaObject(entry("Boolean", Boolean), entry("String", String)); + Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, metaObject, sqlQuery, context); + assertEquals(1, result.count()); + + verify(resultObject).setValue(context, "Boolean", false); + verify(objectInstantiator, times(1)).instantiate(context, entityName); + verify(resultSet, times(2)).next(); + } + @Test public void testNoResults() throws Exception { IMendixObject resultObject = mock(IMendixObject.class); @@ -172,10 +230,13 @@ public class JdbcConnectorTest { when(preparedStatement.executeQuery()).thenReturn(resultSet); when(objectInstantiator.instantiate(anyObject(), anyString())).thenReturn(resultObject); when(resultSetMetaData.getColumnCount()).thenReturn(2); + when(resultSetMetaData.getColumnName(1)).thenReturn("a"); + when(resultSetMetaData.getColumnName(2)).thenReturn("b"); when(resultSet.getMetaData()).thenReturn(resultSetMetaData); when(resultSet.next()).thenReturn(false); - Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, entityName, sqlQuery, context); + IMetaObject metaObject = mockIMetaObject(entry("a", String), entry("b", Boolean)); + Stream result = jdbcConnector.executeQuery(jdbcUrl, userName, password, metaObject, sqlQuery, context); assertEquals(0, result.count()); diff --git a/javasource/databaseconnectortest/test/ResultSetIteratorTest.java b/javasource/databaseconnectortest/test/ResultSetIteratorTest.java index 068bb7f..834ba9c 100644 --- a/javasource/databaseconnectortest/test/ResultSetIteratorTest.java +++ b/javasource/databaseconnectortest/test/ResultSetIteratorTest.java @@ -1,27 +1,53 @@ package databaseconnectortest.test; -import databaseconnector.impl.ColumnInfo; -import databaseconnector.impl.ResultSetIterator; -import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import org.junit.Test; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import com.mendix.systemwideinterfaces.core.meta.IMetaObject; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive.PrimitiveType; + +import databaseconnector.impl.ColumnInfo; +import databaseconnector.impl.ResultSetIterator; +import org.mockito.Mockito; public class ResultSetIteratorTest { + private final IMetaObject metaObject = mockMetaObject(); + + private IMetaObject mockMetaObject() { + final IMetaObject metaObject = mock(IMetaObject.class); + final Collection stringPrimitives = Arrays.asList( + mockMetaPrimitive("a", PrimitiveType.String), + mockMetaPrimitive("b", PrimitiveType.String), + mockMetaPrimitive("c", PrimitiveType.String) + ); + Mockito.>when(metaObject.getMetaPrimitives()).thenReturn(stringPrimitives); + return metaObject; + } + + private IMetaPrimitive mockMetaPrimitive(String name, PrimitiveType type) { + final IMetaPrimitive primitive = mock(IMetaPrimitive.class); + when(primitive.getType()).thenReturn(type); + when(primitive.getName()).thenReturn(name); + return primitive; + } + private ResultSet mockResultSet() throws SQLException { final ResultSet rs = mock(ResultSet.class); final ResultSetMetaData md = mock(ResultSetMetaData.class); @@ -36,7 +62,7 @@ private ResultSet mockResultSet() throws SQLException { @Test public void testColumnInfos() throws SQLException { final ResultSet rs = mockResultSet(); - final ResultSetIterator rsi = new ResultSetIterator(rs); + final ResultSetIterator rsi = new ResultSetIterator(rs, metaObject); final Stream infos = rsi.getColumnInfos(); final List actual = infos.map(ci -> ci.getIndex() + ":" + ci.getName()).collect(Collectors.toList()); final List expected = Arrays.asList("1:a", "2:b", "3:c"); @@ -47,7 +73,7 @@ public void testColumnInfos() throws SQLException { public void testHasNext_Empty() throws SQLException { final ResultSet rs = mockResultSet(); when(rs.next()).thenReturn(false); - final ResultSetIterator rsi = new ResultSetIterator(rs); + final ResultSetIterator rsi = new ResultSetIterator(rs, metaObject); assertFalse(rsi.hasNext()); } @@ -55,7 +81,7 @@ public void testHasNext_Empty() throws SQLException { public void testStream_Empty() throws SQLException { final ResultSet rs = mockResultSet(); when(rs.next()).thenReturn(false); - final ResultSetIterator rsi = new ResultSetIterator(rs); + final ResultSetIterator rsi = new ResultSetIterator(rs, metaObject); assertEquals(0, rsi.stream().count()); } @@ -63,7 +89,7 @@ public void testStream_Empty() throws SQLException { public void testStream_OneRow() throws SQLException { final ResultSet rs = mockResultSet(); when(rs.next()).thenReturn(true, false); - final ResultSetIterator rsi = new ResultSetIterator(rs); + final ResultSetIterator rsi = new ResultSetIterator(rs, metaObject); assertEquals(1, rsi.stream().count()); verify(rs, times(2)).next(); } @@ -72,7 +98,7 @@ public void testStream_OneRow() throws SQLException { public void testStream_TwoRows() throws SQLException { final ResultSet rs = mockResultSet(); when(rs.next()).thenReturn(true, true, false); - final ResultSetIterator rsi = new ResultSetIterator(rs); + final ResultSetIterator rsi = new ResultSetIterator(rs, metaObject); assertEquals(2, rsi.stream().count()); verify(rs, times(3)).next(); } diff --git a/javasource/databaseconnectortest/test/ResultSetReaderTest.java b/javasource/databaseconnectortest/test/ResultSetReaderTest.java index 36b309f..bed1130 100644 --- a/javasource/databaseconnectortest/test/ResultSetReaderTest.java +++ b/javasource/databaseconnectortest/test/ResultSetReaderTest.java @@ -1,30 +1,111 @@ package databaseconnectortest.test; - import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import databaseconnector.impl.ResultSetReader; -import org.junit.Test; - +import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Optional; -import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.mockito.Mockito; + +import com.mendix.systemwideinterfaces.core.meta.IMetaObject; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive; +import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive.PrimitiveType; + +import databaseconnector.impl.ResultSetReader; public class ResultSetReaderTest { + private final IMetaPrimitive integerPrimitive = mockMetaPrimitive(PrimitiveType.Integer); + private final IMetaPrimitive autoNumberPrimitive = mockMetaPrimitive(PrimitiveType.AutoNumber); + private final IMetaPrimitive longPrimitive = mockMetaPrimitive(PrimitiveType.Long); + private final IMetaPrimitive booleanPrimitive = mockMetaPrimitive(PrimitiveType.Boolean); + private final IMetaPrimitive decimalPrimitive = mockMetaPrimitive(PrimitiveType.Decimal); + @SuppressWarnings("deprecation") + private final IMetaPrimitive floatPrimitive = mockMetaPrimitive(PrimitiveType.Float); + @SuppressWarnings("deprecation") + private final IMetaPrimitive currencyPrimitive = mockMetaPrimitive(PrimitiveType.Currency); + private final IMetaPrimitive hashStringPrimitive = mockMetaPrimitive(PrimitiveType.HashString); + private final IMetaPrimitive enumPrimitive = mockMetaPrimitive(PrimitiveType.Enum); + private final IMetaPrimitive stringPrimitive = mockMetaPrimitive(PrimitiveType.String); + private final IMetaPrimitive binaryPrimitive = mockMetaPrimitive(PrimitiveType.Binary); + private final IMetaPrimitive dateTimePrimitive = mockMetaPrimitive(PrimitiveType.DateTime); + + private final IMetaObject metaObjectWithAllPrimitives = mockMetaObjectWithAllPrimitives(); + private final IMetaObject metaObjectWithThreePrimitives = mockMetaObjectWithThreePrimitives(); + private final IMetaObject metaObjectWithStringPrimitive = mockMetaObjectWithStringPrimitive(); + + private IMetaObject mockMetaObjectWithAllPrimitives() { + final IMetaObject metaObject = mock(IMetaObject.class); + final Collection allPrimitives = Arrays.asList( + integerPrimitive, + autoNumberPrimitive, + longPrimitive, + booleanPrimitive, + decimalPrimitive, + floatPrimitive, + currencyPrimitive, + hashStringPrimitive, + enumPrimitive, + stringPrimitive, + binaryPrimitive, + dateTimePrimitive + ); + Mockito.>when(metaObject.getMetaPrimitives()).thenReturn(allPrimitives); + return metaObject; + } + + private IMetaObject mockMetaObjectWithThreePrimitives() { + final IMetaObject metaObject = mock(IMetaObject.class); + final Collection allPrimitives = Arrays.asList( + integerPrimitive, + booleanPrimitive, + stringPrimitive + ); + Mockito.>when(metaObject.getMetaPrimitives()).thenReturn(allPrimitives); + return metaObject; + } + + private IMetaObject mockMetaObjectWithStringPrimitive() { + final IMetaObject metaObject = mock(IMetaObject.class); + final Collection allPrimitives = Arrays.asList(stringPrimitive); + Mockito.>when(metaObject.getMetaPrimitives()).thenReturn(allPrimitives); + return metaObject; + } + + private IMetaPrimitive mockMetaPrimitive(PrimitiveType primitiveType) { + return mockMetaPrimitive(primitiveType.name(), primitiveType); + } + + + private IMetaPrimitive mockMetaPrimitive(String name, PrimitiveType type) { + final IMetaPrimitive primitive = mock(IMetaPrimitive.class); + when(primitive.getType()).thenReturn(type); + when(primitive.getName()).thenReturn(name); + return primitive; + } + private ResultSet mockResultSet() throws SQLException { final ResultSet rs = mock(ResultSet.class); final ResultSetMetaData md = mock(ResultSetMetaData.class); when(md.getColumnCount()).thenReturn(3); - when(md.getColumnName(1)).thenReturn("a"); - when(md.getColumnName(2)).thenReturn("b"); - when(md.getColumnName(3)).thenReturn("c"); + when(md.getColumnName(1)).thenReturn("Integer"); + when(md.getColumnName(2)).thenReturn("Boolean"); + when(md.getColumnName(3)).thenReturn("String"); when(rs.getMetaData()).thenReturn(md); return rs; } @@ -33,7 +114,7 @@ private ResultSet mockResultSet() throws SQLException { public void testReadAll_Empty() throws SQLException { final ResultSet rs = mockResultSet(); when(rs.next()).thenReturn(false); - final ResultSetReader rsr = new ResultSetReader(rs); + final ResultSetReader rsr = new ResultSetReader(rs, metaObjectWithThreePrimitives); assertTrue(rsr.readAll().isEmpty()); } @@ -41,42 +122,152 @@ public void testReadAll_Empty() throws SQLException { public void testReadAll_OneRecord() throws SQLException { final ResultSet rs = mockResultSet(); when(rs.next()).thenReturn(true, false); - when(rs.getObject(1)).thenReturn("a1"); - when(rs.getObject(2)).thenReturn("b1"); - when(rs.getObject(3)).thenReturn("c1"); + when(rs.getInt(1)).thenReturn(1); + when(rs.getBoolean(2)).thenReturn(true); + when(rs.getString(3)).thenReturn("a"); - final ResultSetReader rsr = new ResultSetReader(rs); - final List> records = rsr.readAll(); + final ResultSetReader rsr = new ResultSetReader(rs, metaObjectWithThreePrimitives); + final List>> records = rsr.readAll(); assertEquals(1, records.size()); - final Map record = records.get(0); + final Map> record = records.get(0); assertEquals(3, record.size()); - assertEquals("a1", record.get("a")); - assertEquals("b1", record.get("b")); - assertEquals("c1", record.get("c")); + assertEquals(1, record.get("Integer").get()); + assertEquals(true, record.get("Boolean").get()); + assertEquals("a", record.get("String").get()); + } + + @Test + public void testReadAll_OneRecordWithDifferentCasing() throws SQLException { + final ResultSet rs = mock(ResultSet.class); + final ResultSetMetaData md = mock(ResultSetMetaData.class); + when(md.getColumnCount()).thenReturn(1); + when(md.getColumnName(1)).thenReturn("StRiNg"); + when(rs.getMetaData()).thenReturn(md); + when(rs.next()).thenReturn(true, false); + when(rs.getString(1)).thenReturn("a"); + + final ResultSetReader rsr = new ResultSetReader(rs, metaObjectWithStringPrimitive); + final List>> records = rsr.readAll(); + assertEquals(1, records.size()); + final Map> record = records.get(0); + assertEquals(1, record.size()); + assertEquals("a", record.get("StRiNg").get()); } @Test public void testReadAll_TwoRecord() throws SQLException { final ResultSet rs = mockResultSet(); when(rs.next()).thenReturn(true, true, false); - when(rs.getObject(1)).thenReturn("a1", "a2"); - when(rs.getObject(2)).thenReturn("b1", "b2"); - when(rs.getObject(3)).thenReturn("c1", "c2"); + when(rs.getInt(1)).thenReturn(1, 2); + when(rs.getBoolean(2)).thenReturn(true, false); + when(rs.getString(3)).thenReturn("a", "b"); - final ResultSetReader rsr = new ResultSetReader(rs); - final List> records = rsr.readAll(); + final ResultSetReader rsr = new ResultSetReader(rs, metaObjectWithThreePrimitives); + final List>> records = rsr.readAll(); assertEquals(2, records.size()); - final Map record1 = records.get(0); + final Map> record1 = records.get(0); assertEquals(3, record1.size()); - assertEquals("a1", record1.get("a")); - assertEquals("b1", record1.get("b")); - assertEquals("c1", record1.get("c")); + assertEquals(1, record1.get("Integer").get()); + assertEquals(true, record1.get("Boolean").get()); + assertEquals("a", record1.get("String").get()); - final Map record2 = records.get(1); + final Map> record2 = records.get(1); assertEquals(3, record2.size()); - assertEquals("a2", record2.get("a")); - assertEquals("b2", record2.get("b")); - assertEquals("c2", record2.get("c")); + assertEquals(2, record2.get("Integer").get()); + assertEquals(false, record2.get("Boolean").get()); + assertEquals("b", record2.get("String").get()); } + + @Test + public void testAllTypes() throws SQLException { + final ResultSet rs = mock(ResultSet.class); + final ResultSetMetaData md = mock(ResultSetMetaData.class); + when(md.getColumnCount()).thenReturn(12); + when(md.getColumnName(1)).thenReturn("Integer"); + when(md.getColumnName(2)).thenReturn("AutoNumber"); + when(md.getColumnName(3)).thenReturn("Long"); + when(md.getColumnName(4)).thenReturn("Boolean"); + when(md.getColumnName(5)).thenReturn("Decimal"); + when(md.getColumnName(6)).thenReturn("Float"); + when(md.getColumnName(7)).thenReturn("Currency"); + when(md.getColumnName(8)).thenReturn("HashString"); + when(md.getColumnName(9)).thenReturn("Enum"); + when(md.getColumnName(10)).thenReturn("String"); + when(md.getColumnName(11)).thenReturn("Binary"); + when(md.getColumnName(12)).thenReturn("DateTime"); + when(rs.getMetaData()).thenReturn(md); + + when(rs.next()).thenReturn(true, false); + when(rs.getInt(1)).thenReturn(1); + when(rs.getLong(2)).thenReturn(2L); + when(rs.getLong(3)).thenReturn(3L); + when(rs.getBoolean(4)).thenReturn(true); + when(rs.getBigDecimal(5)).thenReturn(new BigDecimal("123")); + when(rs.getDouble(6)).thenReturn(4.0); + when(rs.getDouble(7)).thenReturn(5.0); + when(rs.getString(8)).thenReturn("A0"); + when(rs.getString(9)).thenReturn("A1"); + when(rs.getString(10)).thenReturn("A2"); + when(rs.getBytes(11)).thenReturn("привет мир".getBytes()); + when(rs.getTimestamp(Mockito.eq(12), Mockito.any(Calendar.class))).thenReturn(new Timestamp(0L)); + + final ResultSetReader rsr = new ResultSetReader(rs, metaObjectWithAllPrimitives); + final List>> records = rsr.readAll(); + assertEquals(1, records.size()); + + final Map> record = records.get(0); + assertEquals(12, record.size()); + assertEquals(1, record.get("Integer").get()); + assertEquals(2L, record.get("AutoNumber").get()); + assertEquals(3L, record.get("Long").get()); + assertEquals(true, record.get("Boolean").get()); + assertEquals(new BigDecimal("123"), record.get("Decimal").get()); + assertEquals(4.0, record.get("Float").get()); + assertEquals(5.0, record.get("Currency").get()); + assertEquals("A0", record.get("HashString").get()); + assertEquals("A1", record.get("Enum").get()); + assertEquals("A2", record.get("String").get()); + assertEquals("привет мир", new String((byte[]) record.get("Binary").get())); + assertEquals(new Date(0L), record.get("DateTime").get()); + } + + @Test + public void testNullResultSet() throws SQLException { + final ResultSet rs = mock(ResultSet.class); + when(rs.wasNull()).thenReturn(true); + final ResultSetMetaData md = mock(ResultSetMetaData.class); + when(md.getColumnCount()).thenReturn(1); + when(md.getColumnName(1)).thenReturn("String"); + when(rs.getMetaData()).thenReturn(md); + + when(rs.next()).thenReturn(true, false); + when(rs.getString("String")).thenReturn(null); + + final ResultSetReader rsr = new ResultSetReader(rs, metaObjectWithStringPrimitive); + final List>> records = rsr.readAll(); + assertEquals(1, records.size()); + final Map> record = records.get(0); + assertFalse(record.get("String").isPresent()); + } + + @Test + public void testNullResult() throws SQLException { + final ResultSet rs = mock(ResultSet.class); + final ResultSetMetaData md = mock(ResultSetMetaData.class); + when(md.getColumnCount()).thenReturn(1); + when(md.getColumnName(1)).thenReturn("String"); + when(rs.getMetaData()).thenReturn(md); + + when(rs.next()).thenReturn(true, false); + when(rs.getString("String")).thenReturn(null); + + final ResultSetReader rsr = new ResultSetReader(rs, metaObjectWithStringPrimitive); + final List>> records = rsr.readAll(); + assertEquals(1, records.size()); + final Map> record = records.get(0); + assertFalse(record.get("String").isPresent()); + } + + } diff --git a/userlib/mariadb-java-client-1.4.6.jar b/userlib/mariadb-java-client-1.4.6.jar new file mode 100644 index 0000000..2aba5d0 Binary files /dev/null and b/userlib/mariadb-java-client-1.4.6.jar differ diff --git a/userlib/mariadb-java-client-1.4.6.jar.DatabaseConnectorTest.RequiredLib b/userlib/mariadb-java-client-1.4.6.jar.DatabaseConnectorTest.RequiredLib new file mode 100644 index 0000000..e69de29 diff --git a/userlib/ojdbc7.jar b/userlib/ojdbc7.jar new file mode 100644 index 0000000..cf9eb48 Binary files /dev/null and b/userlib/ojdbc7.jar differ diff --git a/userlib/ojdbc7.jar.DatabaseConnectorTest.RequiredLib b/userlib/ojdbc7.jar.DatabaseConnectorTest.RequiredLib new file mode 100644 index 0000000..e69de29 diff --git a/userlib/sqljdbc42.jar b/userlib/sqljdbc42.jar new file mode 100644 index 0000000..2cfdf26 Binary files /dev/null and b/userlib/sqljdbc42.jar differ diff --git a/userlib/sqljdbc42.jar.DatabaseConnectorTest.RequiredLib b/userlib/sqljdbc42.jar.DatabaseConnectorTest.RequiredLib new file mode 100644 index 0000000..e69de29