Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,25 @@ For "named" column formats, simply omit the element to emulate NULL values. Alte
</resultset>
----

### 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]
----
<resultset xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
<cols><col>name</col><col>age</col></cols>
<row><name>Erich Eichinger</name><age xsi:type='xs:integer'>10</age></row>
<row><col>Matthias Bernlöhr</col><col xsi:type='xs:integer'>20</col></row>
</resultset>
----

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

Expand All @@ -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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Various conflicts on higher versions of java


mvn clean install

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -44,15 +54,15 @@
*/
public class JdbcServiceVirtualizationFactory implements P6Factory {

@Getter @Setter
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw lombok was being used some places, so tidied up and started using it more.

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);
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this is just refactoring so others can extend. When I made my changes originally, I had to copy/paste source files to get it to work, so in case others want to extend more easily.


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<Integer, Object> 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<Integer, Object> parameterValues = new HashMap<Integer, Object>();
@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) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not recording anything for updates as didn't have the use case

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<Integer, Object> 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<Integer, Object> parameterValues = new HashMap<>();

public void setParameterValue(int position, Object value) {
parameterValues.put(position, value);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <a href="http://dcx.sybase.com/1200/en/dbusage/xmldraftchapter-s-3468454.html">Sybase - Using FOR XML RAW</a> documentation and
Expand All @@ -37,8 +41,8 @@ public class MockResultSetHelper {
* </resultset>
* }</pre>
*/
@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;
Expand All @@ -64,23 +68,23 @@ public static MockResultSet parseResultSetFromSybaseXmlString(String id, String
return resultSet;
}

private static class DatabaseRow {
protected class DatabaseRow {
final List<String> colNames;
final Map<String, String> namedValues;
final String[] positionalValues;
final Map<String, Object> namedValues;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to migrate to Object type, but this doesn't change the behaviour for Strings

final Object[] positionalValues;

int colCount;

public DatabaseRow(List<String> 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 {
Expand All @@ -93,7 +97,7 @@ public List<Object> toRowValues() {
List<Object> vals = new ArrayList<>(this.colNames.size());
for(int i=0;i<colNames.size();i++) {
String colName = colNames.get(i);
String colValue;
Object colValue;
if (namedValues.containsKey(colName)) {
colValue = namedValues.get(colName);
} else {
Expand All @@ -105,15 +109,21 @@ public List<Object> toRowValues() {
}
}

private static String getNilableElementText(Element col) {
protected Object getNilableElementText(Element col) {
String val = col.getText();
if ("true".equalsIgnoreCase(col.getAttributeValue("nil", nsXsi))) {
val = null;
return null;
}
else {
String type = col.getAttributeValue("type", nsXsi);
if (type != null) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behaviour is on by default, which seems safe since it will only happen if xs:type is in the xml.

return xmlTypeRegistry.parseValue(type, val);
}
}
return val;
}

private static List<String> parseColumnNames(MockResultSet resultSet, Element root) {
protected List<String> parseColumnNames(MockResultSet resultSet, Element root) {
// determine columns
Element colsHeaderRow = root.getChild("cols");
List<String> colNames = new ArrayList<>();
Expand All @@ -134,19 +144,14 @@ private static List<String> parseColumnNames(MockResultSet resultSet, Element ro
return Collections.unmodifiableList(colNames);
}

private static String getElementName(Element col) {
protected String getElementName(Element col) {
String name = col.getAttributeValue("name");
if (name == null) {
name = col.getName();
}
return name;
}

@SuppressWarnings("unchecked")
private static <T> List<T> cast(List<?> list) {
return (List<T>) list;
}

@SuppressWarnings("unchecked")
private static <T> List<T> cast(List<?> list, Class<T> elementType) {
return (List<T>) list;
Expand Down
Loading