001/*
002 *  Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.mybatisflex.core.datasource;
017
018import com.mybatisflex.core.dialect.DbType;
019import com.mybatisflex.core.dialect.DbTypeUtil;
020import com.mybatisflex.core.transaction.TransactionContext;
021import com.mybatisflex.core.transaction.TransactionalManager;
022import com.mybatisflex.core.util.ArrayUtil;
023import com.mybatisflex.core.util.StringUtil;
024import org.apache.ibatis.logging.Log;
025import org.apache.ibatis.logging.LogFactory;
026
027import javax.sql.DataSource;
028import java.lang.reflect.InvocationHandler;
029import java.lang.reflect.Method;
030import java.lang.reflect.Proxy;
031import java.sql.Connection;
032import java.sql.SQLException;
033import java.util.HashMap;
034import java.util.Map;
035import java.util.Objects;
036
037/**
038 * @author michael
039 */
040public class FlexDataSource extends AbstractDataSource {
041
042    private static final Log log = LogFactory.getLog(FlexDataSource.class);
043
044    private final Map<String, DataSource> dataSourceMap = new HashMap<>();
045    private final Map<String, DbType> dbTypeHashMap = new HashMap<>();
046
047    private final DbType defaultDbType;
048    private final String defaultDataSourceKey;
049    private final DataSource defaultDataSource;
050
051    public FlexDataSource(String dataSourceKey, DataSource dataSource) {
052
053        DataSourceManager.decryptDataSource(dataSource);
054
055        this.defaultDataSourceKey = dataSourceKey;
056        this.defaultDataSource = dataSource;
057        this.defaultDbType = DbTypeUtil.getDbType(dataSource);
058
059        dataSourceMap.put(dataSourceKey, dataSource);
060        dbTypeHashMap.put(dataSourceKey, defaultDbType);
061    }
062
063    public void addDataSource(String dataSourceKey, DataSource dataSource) {
064        DataSourceManager.decryptDataSource(dataSource);
065        dataSourceMap.put(dataSourceKey, dataSource);
066        dbTypeHashMap.put(dataSourceKey, DbTypeUtil.getDbType(dataSource));
067    }
068
069    public void removeDatasource(String dataSourceKey) {
070        dataSourceMap.remove(dataSourceKey);
071        dbTypeHashMap.remove(dataSourceKey);
072    }
073
074    public Map<String, DataSource> getDataSourceMap() {
075        return dataSourceMap;
076    }
077
078    public Map<String, DbType> getDbTypeHashMap() {
079        return dbTypeHashMap;
080    }
081
082    public String getDefaultDataSourceKey() {
083        return defaultDataSourceKey;
084    }
085
086    public DataSource getDefaultDataSource() {
087        return defaultDataSource;
088    }
089
090    public DbType getDefaultDbType() {
091        return defaultDbType;
092    }
093
094    public DbType getDbType(String dataSourceKey) {
095        return dbTypeHashMap.get(dataSourceKey);
096    }
097
098
099    @Override
100    public Connection getConnection() throws SQLException {
101        String xid = TransactionContext.getXID();
102        if (StringUtil.isNotBlank(xid)) {
103            String dataSourceKey = DataSourceKey.get();
104            if (StringUtil.isBlank(dataSourceKey)) {
105                dataSourceKey = defaultDataSourceKey;
106            }
107
108            Connection connection = TransactionalManager.getConnection(xid, dataSourceKey);
109            if (connection == null) {
110                connection = proxy(getDataSource().getConnection(), xid);
111                TransactionalManager.hold(xid, dataSourceKey, connection);
112            }
113            return connection;
114        } else {
115            return getDataSource().getConnection();
116        }
117    }
118
119
120    @Override
121    public Connection getConnection(String username, String password) throws SQLException {
122        String xid = TransactionContext.getXID();
123        if (StringUtil.isNotBlank(xid)) {
124            String dataSourceKey = DataSourceKey.get();
125            if (StringUtil.isBlank(dataSourceKey)) {
126                dataSourceKey = defaultDataSourceKey;
127            }
128            Connection connection = TransactionalManager.getConnection(xid, dataSourceKey);
129            if (connection == null) {
130                connection = proxy(getDataSource().getConnection(username, password), xid);
131                TransactionalManager.hold(xid, dataSourceKey, connection);
132            }
133            return connection;
134        } else {
135            return getDataSource().getConnection(username, password);
136        }
137    }
138
139    static void closeAutoCommit(Connection connection) {
140        try {
141            connection.setAutoCommit(false);
142        } catch (SQLException e) {
143            if (log.isDebugEnabled()) {
144                log.debug("Error set autoCommit to false. Cause: " + e);
145            }
146        }
147    }
148
149    static void resetAutoCommit(Connection connection) {
150        try {
151            if (!connection.getAutoCommit()) {
152                connection.setAutoCommit(true);
153            }
154        } catch (SQLException e) {
155            if (log.isDebugEnabled()) {
156                log.debug("Error resetting autoCommit to true before closing the connection. " +
157                    "Cause: " + e);
158            }
159        }
160    }
161
162
163    public Connection proxy(Connection connection, String xid) {
164        return (Connection) Proxy.newProxyInstance(FlexDataSource.class.getClassLoader()
165            , new Class[]{Connection.class}
166            , new ConnectionHandler(connection, xid));
167    }
168
169    /**
170     * 方便用于 {@link DbTypeUtil#getDbType(DataSource)}
171     */
172    public String getUrl() {
173        return DbTypeUtil.getJdbcUrl(defaultDataSource);
174    }
175
176
177    @Override
178    @SuppressWarnings("unchecked")
179    public <T> T unwrap(Class<T> iface) throws SQLException {
180        if (iface.isInstance(this)) {
181            return (T) this;
182        }
183        return getDataSource().unwrap(iface);
184    }
185
186    @Override
187    public boolean isWrapperFor(Class<?> iface) throws SQLException {
188        return (iface.isInstance(this) || getDataSource().isWrapperFor(iface));
189    }
190
191
192    private DataSource getDataSource() {
193        DataSource dataSource = defaultDataSource;
194        if (dataSourceMap.size() > 1) {
195            String dataSourceKey = DataSourceKey.get();
196            if (StringUtil.isNotBlank(dataSourceKey)) {
197                dataSource = dataSourceMap.get(dataSourceKey);
198                if (dataSource == null) {
199                    throw new IllegalStateException("Cannot get target DataSource for dataSourceKey [" + dataSourceKey + "]");
200                }
201            }
202        }
203        return dataSource;
204    }
205
206    private static class ConnectionHandler implements InvocationHandler {
207
208        private static final String[] proxyMethods = new String[]{"commit", "rollback", "close", "setAutoCommit"};
209        private final Connection original;
210        private final String xid;
211
212        public ConnectionHandler(Connection original, String xid) {
213
214            closeAutoCommit(original);
215
216            this.original = original;
217            this.xid = xid;
218        }
219
220        @Override
221        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
222            if (ArrayUtil.contains(proxyMethods, method.getName())
223                && isTransactional()) {
224                //do nothing
225                return null;
226            }
227
228            //setAutoCommit: true
229            if ("close".equalsIgnoreCase(method.getName())) {
230                resetAutoCommit(original);
231            }
232
233            return method.invoke(original, args);
234        }
235
236        private boolean isTransactional() {
237            return Objects.equals(xid, TransactionContext.getXID());
238        }
239
240    }
241
242
243}