All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eeichinger.servicevirtualisation.jdbc.JdbcServiceVirtualizationFactory Maven / Gradle / Ivy

Go to download

A library to support JDBC spying and mocking via HTTP using a potentially remote WireMock or similar.

The newest version!
package org.eeichinger.servicevirtualisation.jdbc;

import com.mockrunner.jdbc.CallableStatementResultSetHandler;
import com.mockrunner.jdbc.PreparedStatementResultSetHandler;
import com.mockrunner.jdbc.StatementResultSetHandler;
import com.mockrunner.mock.jdbc.MockConnection;
import com.mockrunner.mock.jdbc.MockDataSource;
import com.mockrunner.mock.jdbc.MockStatement;
import com.p6spy.engine.common.ConnectionInformation;
import com.p6spy.engine.logging.P6LogOptions;
import com.p6spy.engine.proxy.Delegate;
import com.p6spy.engine.proxy.GenericInvocationHandler;
import com.p6spy.engine.proxy.MethodNameMatcher;
import com.p6spy.engine.proxy.ProxyFactory;
import com.p6spy.engine.spy.P6Factory;
import com.p6spy.engine.spy.P6LoadableOptions;
import com.p6spy.engine.spy.option.P6OptionsRepository;
import lombok.SneakyThrows;
import org.apache.http.Header;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import javax.sql.DataSource;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Stream;

/**
 * This is implemented as a {@link P6Factory}, the plan is to integrate it as a P6Module.
 *
 * @author Erich Eichinger
 * @since 30/10/2015
 */
public class JdbcServiceVirtualizationFactory implements P6Factory {

    private String targetUrl;

    public String getTargetUrl() {
        return targetUrl;
    }

    public void setTargetUrl(String targetUrl) {
        this.targetUrl = targetUrl;
    }

    public DataSource spyOnDataSource(DataSource ds) {
        return interceptDataSource(ds);
    }

    public DataSource createMockDataSource() {
        MockDataSource dataSource = new MockDataSource() {{
            setupConnection(new StubbingMockConnection());
        }};
        return interceptDataSource(dataSource);
    }

    private static class StubbingMockConnection extends MockConnection {
        public StubbingMockConnection() {
            this(new SynchronizedStatementResultSetHandler()
                , new PreparedStatementResultSetHandler() {
                    @Override
                    public SQLException getSQLException(String sql) {
                        throw new AssertionError("unmatched sql statement: '" + sql + "'");
                    }
                }
                , new CallableStatementResultSetHandler() {
                    @Override
                    public SQLException getSQLException(String sql) {
                        throw new AssertionError("unmatched sql statement: '" + sql + "'");
                    }
                }
            );
        }

        public StubbingMockConnection(StatementResultSetHandler statementHandler, PreparedStatementResultSetHandler preparedStatementHandler, CallableStatementResultSetHandler callableStatementHandler) {
            super(synchronizeMembers(statementHandler), synchronizeMembers(preparedStatementHandler), synchronizeMembers(callableStatementHandler));
        }

        @SneakyThrows
        private static  T synchronizeMembers(T o) {
            doWithFields(o.getClass(), f->syncField(o, f));
            for(Field f : o.getClass().getDeclaredFields()) {
                syncField(o, f);
            }
            return o;
        }

        private static void doWithFields(Class clazz, Consumer fc) {
            // Keep backing up the inheritance hierarchy.
            Class targetClass = clazz;
            do {
                Field[] fields = targetClass.getDeclaredFields();
                for (Field field : fields) {
                    field.setAccessible(true);
                    fc.accept(field);
                }
                targetClass = targetClass.getSuperclass();
            }
            while (targetClass != null && targetClass != Object.class);
        }

        @SneakyThrows
        private static  void syncField(T o, Field f) {
            Class fieldType = f.getType();
            Object value = f.get(o);
            if (List.class.isAssignableFrom(fieldType) && value != null) {
                f.set(o, Collections.synchronizedList((List) value));
            } else if (Map.class.isAssignableFrom(fieldType) && value != null) {
                f.set(o, Collections.synchronizedMap((Map)value));
            }
        }

        private static class SynchronizedStatementResultSetHandler extends StatementResultSetHandler {
            @Override
            public SQLException getSQLException(String sql) {
                throw new AssertionError("unmatched sql statement: '" + sql + "'");
            }

            @Override
            public synchronized void addStatement(MockStatement statement) {
                super.addStatement(statement);
            }
        }
    }

    @Override
    public Connection getConnection(Connection conn) throws SQLException {
        return interceptConnection(conn);
    }

    @Override
    public P6LoadableOptions getOptions(P6OptionsRepository optionsRepository) {
        // TODO
        return new P6LogOptions(optionsRepository);
    }

    protected DataSource interceptDataSource(DataSource ds) {
        GenericInvocationHandler invocationHandler = createDataSourceInvocationHandler(ds);
        return ProxyFactory.createProxy(ds, invocationHandler);
    }

    protected Connection interceptConnection(Connection conn) {
        GenericInvocationHandler invocationHandler = createConnectionInvocationHandler(conn);
        return ProxyFactory.createProxy(conn, invocationHandler);
    }

    /**
     * This is where the magic happens - decide whether to intercept or pass through
     *
     * @param preparedStatementInformation
     * @param underlying
     * @param method
     * @param args
     * @return the resultset (executeQuery) or int (executeUpdate)
     */
    @SneakyThrows
    protected Object interceptPreparedStatementExecution(PreparedStatementInformation preparedStatementInformation, Object underlying, Method method, Object[] args) {
        CloseableHttpClient httpclient = HttpClients.createDefault();

        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"));

        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);
            }
        }

        final Object result = method.invoke(underlying, args);
        return result;
    }


    static class PreparedStatementInformation {
        ConnectionInformation connectionInformation;
        String sql;
        Map parameterValues = new HashMap();

        public PreparedStatementInformation(ConnectionInformation connectionInformation) {
            this.connectionInformation = connectionInformation;
        }

        public ConnectionInformation getConnectionInformation() {
            return connectionInformation;
        }

        public String getSql() {
            return sql;
        }

        public Map getParameterValues() {
            return parameterValues;
        }

        public void setStatementQuery(String sql) {
            this.sql = sql;
        }

        public void setParameterValue(int position, Object value) {
            parameterValues.put(position, value);
        }
    }

    protected Delegate createDataSourceGetConnectionDelegate() {
        return (final Object proxy, final Object underlying, final Method method, final Object[] args) -> {
            Connection conn = (Connection) method.invoke(underlying, args);
            return interceptConnection(conn);
        };
    }

    protected Delegate createConnectionPrepareStatementDelegate(final ConnectionInformation connectionInformation) {
        return (final Object proxy, final Object underlying, final Method method, final Object[] args) -> {
            synchronized (this) {
                PreparedStatement statement = (PreparedStatement) method.invoke(underlying, args);
                String query = (String) args[0];
                GenericInvocationHandler invocationHandler = createPreparedStatementInvocationHandler(connectionInformation, statement, query);
                return ProxyFactory.createProxy(statement, invocationHandler);
            }
        };
    }

    protected Delegate createPreparedStatementExecuteDelegate(final PreparedStatementInformation preparedStatementInformation) {
        return (final Object proxy, final Object underlying, final Method method, final Object[] args) -> {
            synchronized (preparedStatementInformation.getConnectionInformation()) {
                return interceptPreparedStatementExecution(preparedStatementInformation, underlying, method, args);
            }
        };
    }

    protected class P6MockDataSourceInvocationHandler extends GenericInvocationHandler {

        public P6MockDataSourceInvocationHandler(DataSource underlying) {
            super(underlying);

            Delegate getConnectionDelegate = createDataSourceGetConnectionDelegate();

            // add delegates to return proxies for other methods
            addDelegate(
                new MethodNameMatcher("getConnection"),
                getConnectionDelegate
            );
        }
    }

    protected class P6MockConnectionInvocationHandler extends GenericInvocationHandler {

        public P6MockConnectionInvocationHandler(Connection underlying) {
            super(underlying);
            ConnectionInformation connectionInformation = new ConnectionInformation();

            Delegate prepareStatementDelegate = createConnectionPrepareStatementDelegate(connectionInformation);

            // add delegates to return proxies for other methods
            addDelegate(
                new MethodNameMatcher("prepareStatement"),
                prepareStatementDelegate
            );
        }
    }

    protected class P6MockPreparedStatementInvocationHandler extends GenericInvocationHandler {

        class P6MockPreparedStatementSetParameterValueDelegate implements Delegate {
            protected final PreparedStatementInformation preparedStatementInformation;

            public P6MockPreparedStatementSetParameterValueDelegate(PreparedStatementInformation preparedStatementInformation) {
                this.preparedStatementInformation = preparedStatementInformation;
            }

            @Override
            public Object invoke(final Object proxy, final Object underlying, final Method method, final Object[] args) throws Throwable {
                // ignore calls to any methods defined on the Statement interface!
                if (!Statement.class.equals(method.getDeclaringClass())) {
                    int position = (Integer) args[0];
                    Object value = null;
                    if (!method.getName().equals("setNull") && args.length > 1) {
                        value = args[1];
                    }
                    preparedStatementInformation.setParameterValue(position, value);
                }
                return method.invoke(underlying, args);
            }


        }

        public P6MockPreparedStatementInvocationHandler(PreparedStatement underlying,
                                                        ConnectionInformation connectionInformation,
                                                        String query) {

            super(underlying);
            PreparedStatementInformation preparedStatementInformation = new PreparedStatementInformation(connectionInformation);
            preparedStatementInformation.setStatementQuery(query);

            Delegate executeDelegate = createPreparedStatementExecuteDelegate(preparedStatementInformation);
            Delegate setParameterValueDelegate = new P6MockPreparedStatementSetParameterValueDelegate(preparedStatementInformation);

            addDelegate(
                new MethodNameMatcher("executeBatch"),
                executeDelegate
            );
            addDelegate(
                new MethodNameMatcher("execute"),
                executeDelegate
            );
            addDelegate(
                new MethodNameMatcher("executeQuery"),
                executeDelegate
            );
            addDelegate(
                new MethodNameMatcher("executeUpdate"),
                executeDelegate
            );
            addDelegate(
                new MethodNameMatcher("set*"),
                setParameterValueDelegate
            );
        }
    }

    protected P6MockDataSourceInvocationHandler createDataSourceInvocationHandler(DataSource dataSource) {
        return new P6MockDataSourceInvocationHandler(dataSource);
    }

    protected P6MockConnectionInvocationHandler createConnectionInvocationHandler(Connection conn) {
        return new P6MockConnectionInvocationHandler(conn);
    }

    protected P6MockPreparedStatementInvocationHandler createPreparedStatementInvocationHandler(ConnectionInformation connectionInformation, PreparedStatement statement, String query) {
        return new P6MockPreparedStatementInvocationHandler(statement, connectionInformation, query);
    }

    /**
     * Returns an integer array with all number of affected rows for one batch.
     * List must be comma-seperated, e.g. "-2,-2,-2".
     *
     * @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();
    }

}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy