-
Notifications
You must be signed in to change notification settings - Fork 9
Add ability to record spy; add type handling for record and replay #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
fb32868
896fa9f
4020739
16babae
905298c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
|
@@ -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); | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
|
@@ -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(); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 <a href="http://dcx.sybase.com/1200/en/dbusage/xmldraftchapter-s-3468454.html">Sybase - Using FOR XML RAW</a> documentation and | ||
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
|
@@ -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 { | ||
|
|
@@ -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) { | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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<>(); | ||
|
|
@@ -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; | ||
|
|
||
There was a problem hiding this comment.
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