diff --git a/README.adoc b/README.adoc
index 03633012..f574121f 100644
--- a/README.adoc
+++ b/README.adoc
@@ -188,6 +188,25 @@ For "named" column formats, simply omit the element to emulate NULL values. Alte
----
+### Typed values
+
+If your code is aware of the types (e.g. by calling getInt on the ResultSet) then it is sufficient to read Strings and the MockResultSet will do the coercion. However, if your code ends up using getObject like some of Spring's methods, then it will always return a String when the real driver would return the actual type.
+Type can be encoded to mock how the driver returns:
+[source,xml]
+----
+
+
name
age
+ Erich Eichinger10
+
Matthias Bernlöhr
20
+
+----
+
+More types can be added by modifying or extending the XmlTypeRegistry.
+
+## Recording in spy mode
+It is possible to record the jdbc traffic when in spy mode. This will depend on your specific setup. A default version is provided using WireMock mapping json files.
+see link:src/test/java/org/eeichinger/servicevirtualisation/jdbc/WireMockMappingJsonRecorderTest.java[]
+To enable without any changes, you can set the recorder field on the JdbcServiceVirtualizationFactory to the provided implementation. Likely you will wish to extend this class for your own case.
## Getting the Binaries
@@ -210,7 +229,7 @@ For bugs, feature requests or questions and discussions please use GitHub issues
### Building
-To build the project simply run
+Works on JDK1.8. To build the project simply run
mvn clean install
diff --git a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/JdbcServiceVirtualizationFactory.java b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/JdbcServiceVirtualizationFactory.java
index 38b46d6d..adc9a7d1 100644
--- a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/JdbcServiceVirtualizationFactory.java
+++ b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/JdbcServiceVirtualizationFactory.java
@@ -5,6 +5,7 @@
import com.mockrunner.jdbc.StatementResultSetHandler;
import com.mockrunner.mock.jdbc.MockConnection;
import com.mockrunner.mock.jdbc.MockDataSource;
+import com.mockrunner.mock.jdbc.MockResultSet;
import com.mockrunner.mock.jdbc.MockStatement;
import com.p6spy.engine.common.ConnectionInformation;
import com.p6spy.engine.logging.P6LogOptions;
@@ -15,6 +16,9 @@
import com.p6spy.engine.spy.P6Factory;
import com.p6spy.engine.spy.P6LoadableOptions;
import com.p6spy.engine.spy.option.P6OptionsRepository;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
import lombok.SneakyThrows;
import org.apache.http.Header;
import org.apache.http.client.methods.CloseableHttpResponse;
@@ -25,14 +29,20 @@
import org.apache.http.util.EntityUtils;
import javax.sql.DataSource;
-
+import javax.sql.rowset.CachedRowSet;
+import javax.sql.rowset.RowSetProvider;
import java.lang.reflect.Field;
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.*;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -44,15 +54,15 @@
*/
public class JdbcServiceVirtualizationFactory implements P6Factory {
+ @Getter @Setter
private String targetUrl;
- public String getTargetUrl() {
- return targetUrl;
- }
+ @Setter
+ private WireMockMappingJsonRecorder recorder;
+
+ @Setter
+ private MockResultSetHelper mockResultSetHelper = new MockResultSetHelper();
- public void setTargetUrl(String targetUrl) {
- this.targetUrl = targetUrl;
- }
public DataSource spyOnDataSource(DataSource ds) {
return interceptDataSource(ds);
@@ -135,7 +145,7 @@ public synchronized void addStatement(MockStatement statement) {
}
@Override
- public Connection getConnection(Connection conn) throws SQLException {
+ public Connection getConnection(Connection conn) {
return interceptConnection(conn);
}
@@ -168,62 +178,82 @@ protected Connection interceptConnection(Connection conn) {
protected Object interceptPreparedStatementExecution(PreparedStatementInformation preparedStatementInformation, Object underlying, Method method, Object[] args) {
CloseableHttpClient httpclient = HttpClients.createDefault();
+ HttpPost httpPost = prepareHttpCall(preparedStatementInformation);
+
+ try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
+ if (response.getStatusLine().getStatusCode() == 200) {
+ return getResultFromHttpResponse(response, method.getReturnType());
+ }
+ if (response.getStatusLine().getStatusCode() == 400) {
+ throw getExceptionFromResponse(response);
+ }
+ }
+
+ return callUnderlyingMethod(underlying, method, args, preparedStatementInformation);
+ }
+
+ protected HttpPost prepareHttpCall(PreparedStatementInformation preparedStatementInformation) {
final String sql = preparedStatementInformation.getSql();
HttpPost httpPost = new HttpPost(targetUrl);
for (Map.Entry e : preparedStatementInformation.getParameterValues().entrySet()) {
httpPost.setHeader(e.getKey().toString(), Objects.toString(e.getValue()));
}
httpPost.setEntity(new StringEntity(sql, "utf-8"));
+ return httpPost;
+ }
- try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
- if (response.getStatusLine().getStatusCode() == 200) {
- String responseContent = EntityUtils.toString(response.getEntity(), "utf-8");
- if (int[].class.equals(method.getReturnType())) {
- return parseBatchUpdateRowsAffected(responseContent);
- }
- if (int.class.equals(method.getReturnType())) {
- return Integer.parseInt(responseContent);
- }
- return MockResultSetHelper.parseResultSetFromSybaseXmlString("x", responseContent);
- }
- if (response.getStatusLine().getStatusCode() == 400) {
- final Header reasonHeader = response.getFirstHeader("reason");
- if (reasonHeader == null) throw new AssertionError("missing 'reason' response header");
- final String sqlState = response.getFirstHeader("sqlstate") != null ? response.getFirstHeader("sqlstate").getValue() : null;
- final int vendorCode = response.getFirstHeader("vendorcode") != null ? Integer.parseInt(response.getFirstHeader("vendorcode").getValue()) : 0;
- throw new SQLException(reasonHeader.getValue(), sqlState, vendorCode);
- }
+ @SneakyThrows
+ protected Object getResultFromHttpResponse(CloseableHttpResponse response, Class> returnType) {
+ String responseContent = EntityUtils.toString(response.getEntity(), "utf-8");
+ if (int[].class.equals(returnType)) {
+ return parseBatchUpdateRowsAffected(responseContent);
}
+ if (int.class.equals(returnType)) {
+ return Integer.parseInt(responseContent);
+ }
+ return parseResultSetFromResponseContent(responseContent);
+ }
- final Object result = method.invoke(underlying, args);
- return result;
+ protected MockResultSet parseResultSetFromResponseContent(String responseContent) {
+ return mockResultSetHelper.parseResultSetFromSybaseXmlString("x", responseContent);
}
+ protected Throwable getExceptionFromResponse(CloseableHttpResponse response) {
+ final Header reasonHeader = response.getFirstHeader("reason");
+ if (reasonHeader == null) return new AssertionError("missing 'reason' response header");
+ final String sqlState = response.getFirstHeader("sqlstate") != null ? response.getFirstHeader("sqlstate").getValue() : null;
+ final int vendorCode = response.getFirstHeader("vendorcode") != null ? Integer.parseInt(response.getFirstHeader("vendorcode").getValue()) : 0;
+ return new SQLException(reasonHeader.getValue(), sqlState, vendorCode);
+ }
- static class PreparedStatementInformation {
- ConnectionInformation connectionInformation;
- String sql;
- Map parameterValues = new HashMap();
+ @SneakyThrows
+ public ResultSet cacheResultSet(ResultSet resultSet) {
+ CachedRowSet cachedRowSet = RowSetProvider.newFactory().createCachedRowSet();
+ cachedRowSet.populate(resultSet);
+ return cachedRowSet;
+ }
- public PreparedStatementInformation(ConnectionInformation connectionInformation) {
- this.connectionInformation = connectionInformation;
+ @SneakyThrows
+ protected Object callUnderlyingMethod(Object underlying, Method method, Object[] args, PreparedStatementInformation preparedStatementInformation) {
+ Object actualResult = method.invoke(underlying, args);
+
+ if (recorder != null && actualResult instanceof ResultSet) {
+ ResultSet cachedResultSet = cacheResultSet((ResultSet) actualResult);
+ recorder.writeOutMapping(preparedStatementInformation, cachedResultSet);
+ cachedResultSet.beforeFirst();
+ return cachedResultSet;
}
- public ConnectionInformation getConnectionInformation() {
- return connectionInformation;
- }
+ return actualResult;
- public String getSql() {
- return sql;
- }
+ }
- public Map getParameterValues() {
- return parameterValues;
- }
- public void setStatementQuery(String sql) {
- this.sql = sql;
- }
+ @RequiredArgsConstructor
+ static class PreparedStatementInformation {
+ @Getter private final ConnectionInformation connectionInformation;
+ @Getter @Setter private String sql;
+ @Getter private final Map parameterValues = new HashMap<>();
public void setParameterValue(int position, Object value) {
parameterValues.put(position, value);
@@ -319,7 +349,7 @@ public P6MockPreparedStatementInvocationHandler(PreparedStatement underlying,
super(underlying);
PreparedStatementInformation preparedStatementInformation = new PreparedStatementInformation(connectionInformation);
- preparedStatementInformation.setStatementQuery(query);
+ preparedStatementInformation.setSql(query);
Delegate executeDelegate = createPreparedStatementExecuteDelegate(preparedStatementInformation);
Delegate setParameterValueDelegate = new P6MockPreparedStatementSetParameterValueDelegate(preparedStatementInformation);
@@ -366,7 +396,7 @@ protected P6MockPreparedStatementInvocationHandler createPreparedStatementInvoca
* @return array with corresponding number of updated rows for each batch
*/
private static int[] parseBatchUpdateRowsAffected(String responseContent) {
- return Stream.of(responseContent.split(",")).mapToInt(s -> Integer.parseInt(s)).toArray();
+ return Stream.of(responseContent.split(",")).mapToInt(Integer::parseInt).toArray();
}
}
diff --git a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelper.java b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelper.java
index ba2ff0f1..208067b9 100644
--- a/src/main/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelper.java
+++ b/src/main/java/org/eeichinger/servicevirtualisation/jdbc/MockResultSetHelper.java
@@ -9,6 +9,7 @@
import com.mockrunner.base.NestedApplicationException;
import com.mockrunner.mock.jdbc.MockResultSet;
+import lombok.Setter;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
@@ -20,6 +21,9 @@
*/
public class MockResultSetHelper {
+ @Setter
+ private XmlTypeRegistry xmlTypeRegistry = new XmlTypeRegistry();
+
/**
* Parse a MockResultSet from the provided Sybase-style formatted XML Document.
* See Sybase - Using FOR XML RAW documentation and
@@ -37,8 +41,8 @@ public class MockResultSetHelper {
*
* }
*/
- @SuppressWarnings({"rawtypes", "unchecked"})
- public static MockResultSet parseResultSetFromSybaseXmlString(String id, String xml) {
+ @SuppressWarnings({"unchecked"})
+ public MockResultSet parseResultSetFromSybaseXmlString(String id, String xml) {
MockResultSet resultSet = new MockResultSet(id);
SAXBuilder builder = new SAXBuilder();
Document doc;
@@ -64,10 +68,10 @@ public static MockResultSet parseResultSetFromSybaseXmlString(String id, String
return resultSet;
}
- private static class DatabaseRow {
+ protected class DatabaseRow {
final List colNames;
- final Map namedValues;
- final String[] positionalValues;
+ final Map namedValues;
+ final Object[] positionalValues;
int colCount;
@@ -75,12 +79,12 @@ public DatabaseRow(List colNames) {
this.colNames = colNames;
this.colCount = 0;
this.namedValues = new HashMap<>();
- this.positionalValues = new String[this.colNames.size()];
+ this.positionalValues = new Object[this.colNames.size()];
}
public void add(Element col) {
String name = getElementName(col);
- String val = getNilableElementText(col);
+ Object val = getNilableElementText(col);
if (colNames.contains(name)) {
namedValues.put(name, val);
} else {
@@ -93,7 +97,7 @@ public List