001/*
002 * Copyright 2002-2020 the original author or authors.
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 *      https://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
017package org.springframework.scheduling.quartz;
018
019import java.sql.Connection;
020import java.sql.DatabaseMetaData;
021import java.sql.SQLException;
022
023import javax.sql.DataSource;
024
025import org.quartz.SchedulerConfigException;
026import org.quartz.impl.jdbcjobstore.JobStoreCMT;
027import org.quartz.impl.jdbcjobstore.SimpleSemaphore;
028import org.quartz.spi.ClassLoadHelper;
029import org.quartz.spi.SchedulerSignaler;
030import org.quartz.utils.ConnectionProvider;
031import org.quartz.utils.DBConnectionManager;
032
033import org.springframework.jdbc.datasource.DataSourceUtils;
034import org.springframework.jdbc.support.JdbcUtils;
035import org.springframework.jdbc.support.MetaDataAccessException;
036import org.springframework.lang.Nullable;
037
038/**
039 * Subclass of Quartz's {@link JobStoreCMT} class that delegates to a Spring-managed
040 * {@link DataSource} instead of using a Quartz-managed JDBC connection pool.
041 * This JobStore will be used if SchedulerFactoryBean's "dataSource" property is set.
042 *
043 * <p>Supports both transactional and non-transactional DataSource access.
044 * With a non-XA DataSource and local Spring transactions, a single DataSource
045 * argument is sufficient. In case of an XA DataSource and global JTA transactions,
046 * SchedulerFactoryBean's "nonTransactionalDataSource" property should be set,
047 * passing in a non-XA DataSource that will not participate in global transactions.
048 *
049 * <p>Operations performed by this JobStore will properly participate in any
050 * kind of Spring-managed transaction, as it uses Spring's DataSourceUtils
051 * connection handling methods that are aware of a current transaction.
052 *
053 * <p>Note that all Quartz Scheduler operations that affect the persistent
054 * job store should usually be performed within active transactions,
055 * as they assume to get proper locks etc.
056 *
057 * @author Juergen Hoeller
058 * @since 1.1
059 * @see SchedulerFactoryBean#setDataSource
060 * @see SchedulerFactoryBean#setNonTransactionalDataSource
061 * @see org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection
062 * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
063 */
064@SuppressWarnings("unchecked")  // due to a warning in Quartz 2.2's JobStoreCMT
065public class LocalDataSourceJobStore extends JobStoreCMT {
066
067        /**
068         * Name used for the transactional ConnectionProvider for Quartz.
069         * This provider will delegate to the local Spring-managed DataSource.
070         * @see org.quartz.utils.DBConnectionManager#addConnectionProvider
071         * @see SchedulerFactoryBean#setDataSource
072         */
073        public static final String TX_DATA_SOURCE_PREFIX = "springTxDataSource.";
074
075        /**
076         * Name used for the non-transactional ConnectionProvider for Quartz.
077         * This provider will delegate to the local Spring-managed DataSource.
078         * @see org.quartz.utils.DBConnectionManager#addConnectionProvider
079         * @see SchedulerFactoryBean#setDataSource
080         */
081        public static final String NON_TX_DATA_SOURCE_PREFIX = "springNonTxDataSource.";
082
083
084        @Nullable
085        private DataSource dataSource;
086
087
088        @Override
089        public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler) throws SchedulerConfigException {
090                // Absolutely needs thread-bound DataSource to initialize.
091                this.dataSource = SchedulerFactoryBean.getConfigTimeDataSource();
092                if (this.dataSource == null) {
093                        throw new SchedulerConfigException("No local DataSource found for configuration - " +
094                                        "'dataSource' property must be set on SchedulerFactoryBean");
095                }
096
097                // Configure transactional connection settings for Quartz.
098                setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName());
099                setDontSetAutoCommitFalse(true);
100
101                // Register transactional ConnectionProvider for Quartz.
102                DBConnectionManager.getInstance().addConnectionProvider(
103                                TX_DATA_SOURCE_PREFIX + getInstanceName(),
104                                new ConnectionProvider() {
105                                        @Override
106                                        public Connection getConnection() throws SQLException {
107                                                // Return a transactional Connection, if any.
108                                                return DataSourceUtils.doGetConnection(dataSource);
109                                        }
110                                        @Override
111                                        public void shutdown() {
112                                                // Do nothing - a Spring-managed DataSource has its own lifecycle.
113                                        }
114                                        @Override
115                                        public void initialize() {
116                                                // Do nothing - a Spring-managed DataSource has its own lifecycle.
117                                        }
118                                }
119                );
120
121                // Non-transactional DataSource is optional: fall back to default
122                // DataSource if not explicitly specified.
123                DataSource nonTxDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
124                final DataSource nonTxDataSourceToUse = (nonTxDataSource != null ? nonTxDataSource : this.dataSource);
125
126                // Configure non-transactional connection settings for Quartz.
127                setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());
128
129                // Register non-transactional ConnectionProvider for Quartz.
130                DBConnectionManager.getInstance().addConnectionProvider(
131                                NON_TX_DATA_SOURCE_PREFIX + getInstanceName(),
132                                new ConnectionProvider() {
133                                        @Override
134                                        public Connection getConnection() throws SQLException {
135                                                // Always return a non-transactional Connection.
136                                                return nonTxDataSourceToUse.getConnection();
137                                        }
138                                        @Override
139                                        public void shutdown() {
140                                                // Do nothing - a Spring-managed DataSource has its own lifecycle.
141                                        }
142                                        @Override
143                                        public void initialize() {
144                                                // Do nothing - a Spring-managed DataSource has its own lifecycle.
145                                        }
146                                }
147                );
148
149                // No, if HSQL is the platform, we really don't want to use locks...
150                try {
151                        String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource,
152                                        DatabaseMetaData::getDatabaseProductName);
153                        productName = JdbcUtils.commonDatabaseName(productName);
154                        if (productName != null && productName.toLowerCase().contains("hsql")) {
155                                setUseDBLocks(false);
156                                setLockHandler(new SimpleSemaphore());
157                        }
158                }
159                catch (MetaDataAccessException ex) {
160                        logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken.");
161                }
162
163                super.initialize(loadHelper, signaler);
164
165        }
166
167        @Override
168        protected void closeConnection(Connection con) {
169                // Will work for transactional and non-transactional connections.
170                DataSourceUtils.releaseConnection(con, this.dataSource);
171        }
172
173}