001/*
002 * (C) Copyright 2012-2020 Nuxeo (http://nuxeo.com/) and others.
003 *
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 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
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 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.runtime.datasource;
020
021import static org.apache.commons.lang3.StringUtils.defaultString;
022
023import java.lang.reflect.InvocationTargetException;
024import java.sql.SQLException;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.concurrent.ConcurrentHashMap;
028
029import javax.naming.NamingException;
030import javax.sql.DataSource;
031import javax.sql.XADataSource;
032import javax.transaction.Status;
033import javax.transaction.Transaction;
034import javax.transaction.TransactionManager;
035import javax.transaction.TransactionSynchronizationRegistry;
036
037import org.apache.commons.beanutils.BeanUtils;
038import org.apache.commons.dbcp2.ConnectionFactory;
039import org.apache.commons.dbcp2.managed.BasicManagedDataSource;
040import org.apache.logging.log4j.LogManager;
041import org.apache.logging.log4j.Logger;
042import org.nuxeo.runtime.RuntimeServiceException;
043import org.nuxeo.runtime.transaction.TransactionHelper;
044
045public class PooledDataSourceRegistry {
046
047    protected final Map<String, DataSource> dataSources = new ConcurrentHashMap<>();
048
049    protected final Map<String, DataSource> dataSourcesNoSharing = new ConcurrentHashMap<>();
050
051    public <T> T getDataSource(String name, Class<T> type, boolean noSharing) {
052        Map<String, DataSource> map = noSharing ? dataSourcesNoSharing : dataSources;
053        return type.cast(map.get(name));
054    }
055
056    public void registerPooledDataSource(String name, Map<String, String> properties) {
057        dataSources.computeIfAbsent(name, k -> createPooledDataSource(properties, false));
058        dataSourcesNoSharing.computeIfAbsent(name, k -> createPooledDataSource(properties, true));
059    }
060
061    /**
062     * A {@link BasicManagedDataSource} that can configure its internal {@link XADataSource}.
063     *
064     * @since 11.1
065     */
066    public static class ConfigurableManagedDataSource extends BasicManagedDataSource {
067
068        private static final Logger log = LogManager.getLogger(ConfigurableManagedDataSource.class);
069
070        protected final Map<String, String> properties;
071
072        public ConfigurableManagedDataSource(Map<String, String> properties) {
073            this.properties = properties;
074        }
075
076        @Override
077        protected ConnectionFactory createConnectionFactory() throws SQLException {
078            ConnectionFactory connectionFactory = super.createConnectionFactory();
079            configureXADataSource(getXaDataSourceInstance());
080            return connectionFactory;
081        }
082
083        // initialize the XADataSource through JavaBeans
084        protected void configureXADataSource(XADataSource xaDataSource) {
085            if (xaDataSource != null) {
086                try {
087                    BeanUtils.populate(xaDataSource, properties);
088                } catch (IllegalAccessException | InvocationTargetException e) {
089                    log.error(e, e);
090                }
091            }
092        }
093    }
094
095    public BasicManagedDataSource createPooledDataSource(Map<String, String> properties, boolean noSharing) {
096        // compatibility with previous Geronimo Connection configuration
097        properties.computeIfAbsent("minTotal", k -> properties.get("minPoolSize"));
098        properties.computeIfAbsent("maxTotal", k -> properties.get("maxPoolSize"));
099        properties.computeIfAbsent("maxWaitMillis", k -> properties.get("blockingTimeoutMillis"));
100        // compatibility aliases for username
101        properties.computeIfAbsent("username", k -> defaultString( //
102                properties.get("user"), //
103                properties.get("User")));
104        // JavaBeans sucks
105        properties.computeIfAbsent("XADataSource", k -> properties.get("xaDataSource"));
106        properties.computeIfAbsent("URL", k -> properties.get("url"));
107
108        BasicManagedDataSource ds = new ConfigurableManagedDataSource(properties);
109
110        // properties for which the DBCP default is not suitable
111        ds.setMaxWaitMillis(1000);
112        ds.setAccessToUnderlyingConnectionAllowed(true);
113
114        // populate datasource via JavaBeans properties
115        try {
116            BeanUtils.populate(ds, properties);
117        } catch (IllegalAccessException | InvocationTargetException e) {
118            throw new RuntimeServiceException(e);
119        }
120        // populate connection properties
121        properties.forEach(ds::addConnectionProperty);
122
123        // transaction management
124        TransactionManager transactionManager;
125        TransactionSynchronizationRegistry transactionSynchronizationRegistry;
126        if (noSharing) {
127            // pretend we never have a transaction
128            transactionManager = new TransactionManagerWithoutTransaction();
129            transactionSynchronizationRegistry = null;
130        } else {
131            try {
132                transactionManager = TransactionHelper.lookupTransactionManager();
133                transactionSynchronizationRegistry = TransactionHelper.lookupSynchronizationRegistry();
134            } catch (NamingException e) {
135                throw new RuntimeServiceException(e);
136            }
137        }
138        ds.setTransactionManager(transactionManager);
139        ds.setTransactionSynchronizationRegistry(transactionSynchronizationRegistry);
140
141        return ds;
142    }
143
144    protected void unregisterPooledDataSource(String name) {
145        dataSources.remove(name);
146        dataSourcesNoSharing.remove(name);
147    }
148
149    public void createAlias(String name, DataSource ds) {
150        // alias noSharing version too
151        for (Entry<String, DataSource> es : dataSources.entrySet()) {
152            if (es.getValue() == ds) {
153                DataSource noSharingDs = dataSourcesNoSharing.get(es.getKey());
154                if (noSharingDs != null) {
155                    dataSourcesNoSharing.put(name, noSharingDs);
156                }
157                break;
158            }
159        }
160        dataSources.put(name, ds);
161    }
162
163    public void removeAlias(String name) {
164        unregisterPooledDataSource(name);
165    }
166
167    /**
168     * Transaction Manager that is never in a transaction and doesn't allow starting one.
169     *
170     * @since 11.1
171     */
172    public static class TransactionManagerWithoutTransaction implements TransactionManager {
173
174        @Override
175        public Transaction getTransaction() {
176            return null;
177        }
178
179        @Override
180        public int getStatus() {
181            return Status.STATUS_NO_TRANSACTION;
182        }
183
184        @Override
185        public void setTransactionTimeout(int seconds) {
186            // nothing
187        }
188
189        @Override
190        public void begin() {
191            throw new UnsupportedOperationException();
192        }
193
194        @Override
195        public void commit() {
196            throw new UnsupportedOperationException();
197        }
198
199        @Override
200        public void rollback() {
201            throw new UnsupportedOperationException();
202        }
203
204        @Override
205        public void resume(Transaction transaction) {
206            throw new UnsupportedOperationException();
207        }
208
209        @Override
210        public void setRollbackOnly() {
211            throw new UnsupportedOperationException();
212        }
213
214        @Override
215        public Transaction suspend() {
216            throw new UnsupportedOperationException();
217        }
218    }
219
220}