001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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 */
016package org.kuali.rice.krad.data.platform;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.config.property.ConfigContext;
020import org.springframework.beans.factory.InitializingBean;
021import org.springframework.dao.DataAccessException;
022import org.springframework.dao.DataAccessResourceFailureException;
023import org.springframework.dao.IncorrectResultSizeDataAccessException;
024import org.springframework.jdbc.core.ConnectionCallback;
025import org.springframework.jdbc.core.JdbcTemplate;
026import org.springframework.jdbc.support.JdbcUtils;
027import org.springframework.jdbc.support.incrementer.AbstractColumnMaxValueIncrementer;
028import org.springframework.jdbc.support.incrementer.AbstractSequenceMaxValueIncrementer;
029import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
030import org.springframework.jdbc.support.incrementer.OracleSequenceMaxValueIncrementer;
031
032import javax.sql.DataSource;
033import java.sql.Connection;
034import java.sql.ResultSet;
035import java.sql.SQLException;
036import java.sql.Statement;
037import java.util.Collections;
038import java.util.IdentityHashMap;
039import java.util.Map;
040import java.util.concurrent.ConcurrentHashMap;
041import java.util.concurrent.ConcurrentMap;
042
043/**
044 * Factory for obtaining instances of {@link DataFieldMaxValueIncrementer} for a given {@link DataSource} and
045 * incrementer name.
046 *
047 * <p>
048 * These incrementers are used for getting generated incrementing values like that provided by a database-level sequence
049 * generator.
050 * </p>
051 *
052 * <p>
053 * Note that not all database platforms support sequences natively, so incrementers can be returned that emulate
054 * sequence-like behavior. The Spring Framework provides incrementer implementations for numerous different database
055 * platforms. This classes uses {@link DatabasePlatforms} to determine the platform of the given {@link DataSource}.
056 * </p>
057 *
058 * <p>
059 * Note that this class will cache internally any incrementers for a given {@link DataSource} + Incrementer Name
060 * combination.
061 * </p>
062 *
063 * @author Kuali Rice Team (rice.collab@kuali.org)
064 */
065public final class MaxValueIncrementerFactory {
066
067    private static final String ID_COLUMN_NAME = "ID";
068
069    /**
070     * Prefix for property names used to identify the classname for a {@link DataFieldMaxValueIncrementer} to use for a
071     * given platform.
072     *
073     * <p>To construct a full property name, concatenate this prefix with the platform name.</p>
074     *
075     * @see org.kuali.rice.krad.data.platform.MaxValueIncrementerFactory
076     */
077    public static final String PLATFORM_INCREMENTER_PREFIX = "rice.krad.data.platform.incrementer.";
078
079    private static final Map<DataSource, ConcurrentMap<String, DataFieldMaxValueIncrementer>> cache
080            = Collections.synchronizedMap(new IdentityHashMap<DataSource, ConcurrentMap<String, DataFieldMaxValueIncrementer>>(8));
081
082    /**
083     * Either constructs a new incrementer or retrieves a cached instance for the given DataSource and target
084     * incrementer name.
085     *
086     * @param dataSource the {@link DataSource} for which to retrieve the incrementer.
087     * @param incrementerName the case-insensitive name of the incrementer to use, this will generally be the name of
088     *        the database object which is used to implement the incrementer.
089     * @return an incrementer that can be used to generate the next incremented value for the given incrementer against
090     *         the specified {@link DataSource}.
091     *
092     * @throws IllegalArgumentException if dataSource or incrementerName are null or blank.
093     */
094    public static DataFieldMaxValueIncrementer getIncrementer(DataSource dataSource, String incrementerName) {
095        if (dataSource == null) {
096            throw new IllegalArgumentException("DataSource must not be null");
097        }
098        if (StringUtils.isBlank(incrementerName)) {
099            throw new IllegalArgumentException("Incrementer name must not be null or blank");
100        }
101
102        // yes, we want to check if it's there first, then put if absent, for max speed! This is like ConcurrentMap's
103        // version of double-checked locking.
104        ConcurrentMap<String, DataFieldMaxValueIncrementer> incrementerCache = cache.get(dataSource);
105
106        if (incrementerCache == null) {
107            cache.put(dataSource,
108                    new ConcurrentHashMap<String, DataFieldMaxValueIncrementer>(8, 0.9f, 1));
109            if (incrementerCache == null) {
110                incrementerCache = cache.get(dataSource);
111            }
112        }
113
114        // now check if we have a cached incrementer
115        DataFieldMaxValueIncrementer incrementer = incrementerCache.get(incrementerName.toUpperCase());
116        if (incrementer == null) {
117            incrementer = incrementerCache.putIfAbsent(incrementerName.toUpperCase(), createIncrementer(dataSource,
118                    incrementerName));
119            if (incrementer == null) {
120                incrementer = incrementerCache.get(incrementerName.toUpperCase());
121            }
122        }
123        return incrementer;
124
125    }
126
127    /**
128     * Creates an {@link DataFieldMaxValueIncrementer} from a {@link DataSource}.
129     *
130     * @param dataSource the {@link DataSource} for which to retrieve the incrementer.
131     * @param incrementerName the name of the incrementer.
132     * @return an {@link DataFieldMaxValueIncrementer} from a {@link DataSource}.
133     */
134    private static DataFieldMaxValueIncrementer createIncrementer(DataSource dataSource, String incrementerName) {
135        DatabasePlatformInfo platformInfo = DatabasePlatforms.detectPlatform(dataSource);
136        DataFieldMaxValueIncrementer incrementer = getCustomizedIncrementer(platformInfo, dataSource,incrementerName,ID_COLUMN_NAME);
137        if(incrementer != null){
138            return incrementer;
139        }
140
141        if (DatabasePlatforms.ORACLE.equalsIgnoreCase(platformInfo.getName())) {
142            incrementer = new OracleSequenceMaxValueIncrementer(dataSource, incrementerName);
143        } else if (DatabasePlatforms.MYSQL.equalsIgnoreCase(platformInfo.getName())) {
144            incrementer = new EnhancedMySQLMaxValueIncrementer(dataSource, incrementerName, ID_COLUMN_NAME);
145        }
146        if (incrementer == null) {
147            throw new UnsupportedDatabasePlatformException(platformInfo);
148        }
149        if (incrementer instanceof InitializingBean) {
150            try {
151                ((InitializingBean) incrementer).afterPropertiesSet();
152            } catch (Exception e) {
153                throw new DataAccessResourceFailureException(
154                        "Failed to initialize max value incrementer for given datasource and incrementer. dataSource="
155                                + dataSource.toString()
156                                + ", incrementerName = "
157                                + incrementerName, e);
158            }
159        }
160        return incrementer;
161    }
162
163    /**
164     * Checks the config file for any references to
165     * {@code rice.krad.data.platform.incrementer.(DATASOURCE, ex mysql, oracle).(VERSION optional)}.
166     *
167     * <p>If matching one found attempts to instantiate it to return back to factory for use.</p>
168     *
169     * @param platformInfo the {@link DatabasePlatformInfo}.
170     * @param dataSource the {@link DataSource} for which to retrieve the incrementer.
171     * @param incrementerName the name of the incrementer.
172     * @param columnName the name of the column to increment.
173     * @return a config set customized incrementer that matches and can be used to generate the next incremented value
174     *         for the given incrementer against the specified {@link DataSource}
175     * @throws InstantiationError if cannot instantiate passed in class.
176     */
177    private static DataFieldMaxValueIncrementer getCustomizedIncrementer(DatabasePlatformInfo platformInfo, DataSource dataSource, String incrementerName, String columnName){
178        if(platformInfo == null){
179            throw new  IllegalArgumentException("DataSource platform must not be null");
180        }
181        if(ConfigContext.getCurrentContextConfig() == null){
182            return null;
183        }
184        Map<String,String> incrementerPropToIncrementer = ConfigContext.getCurrentContextConfig().
185                                getPropertiesWithPrefix(PLATFORM_INCREMENTER_PREFIX, true);
186        String platformNameVersion = platformInfo.getName().toLowerCase() + "." + platformInfo.getMajorVersion();
187        String incrementerClassName = "";
188
189         if(incrementerPropToIncrementer.containsKey(platformNameVersion)){
190            incrementerClassName = incrementerPropToIncrementer.get(platformNameVersion);
191         } else if(incrementerPropToIncrementer.containsKey(platformInfo.getName().toLowerCase())){
192             incrementerClassName = incrementerPropToIncrementer.get(platformInfo.getName().toLowerCase());
193         }
194
195        if(StringUtils.isNotBlank(incrementerClassName)){
196            try {
197                Class incrementerClass = Class.forName(incrementerClassName);
198                if(AbstractSequenceMaxValueIncrementer.class.isAssignableFrom(incrementerClass)){
199                    AbstractSequenceMaxValueIncrementer abstractSequenceMaxValueIncrementer = (AbstractSequenceMaxValueIncrementer)incrementerClass.newInstance();
200                    abstractSequenceMaxValueIncrementer.setDataSource(dataSource);
201                    abstractSequenceMaxValueIncrementer.setIncrementerName(incrementerName);
202                    return abstractSequenceMaxValueIncrementer;
203
204                } else if(AbstractColumnMaxValueIncrementer.class.isAssignableFrom(incrementerClass)){
205                    AbstractColumnMaxValueIncrementer abstractColumnMaxValueIncrementer = (AbstractColumnMaxValueIncrementer)incrementerClass.newInstance();
206                    abstractColumnMaxValueIncrementer.setDataSource(dataSource);
207                    abstractColumnMaxValueIncrementer.setIncrementerName(incrementerName);
208                    abstractColumnMaxValueIncrementer.setColumnName(columnName);
209                    return abstractColumnMaxValueIncrementer;
210                } else {
211                    throw new InstantiationError("Cannot create incrementer class "+incrementerClassName +" it has to extend "
212                            + "AbstractSequenceMaxValueIncrementer or AbstractColumnMaxValueIncrementer");
213                }
214            } catch (Exception e){
215                throw new InstantiationError("Could not instantiate custom incrementer "+incrementerClassName);
216            }
217        }
218        return null;
219    }
220
221    /**
222     * Defines an incrementer for MySQL.
223     *
224     * <p>
225     * Since MySQL does not have any sense of a sequence, this class uses the concept of a sequence table, which is a
226     * regular table that has an auto increment feature on it and is used only for that sequence.  When null values are
227     * inserted into the table, the auto increment feature will insert the next value into that field, and then the
228     * database will be queried for the last insert ID to get the next sequence value.
229     * </p>
230     */
231    static final class EnhancedMySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer {
232
233        private JdbcTemplate template;
234
235        /**
236         * Creates an incrementer for MySQL.
237         */
238        private EnhancedMySQLMaxValueIncrementer() {}
239
240        /**
241         * Creates an incrementer for MySQL.
242         *
243         * @param dataSource the {@link DataSource} for which to retrieve the incrementer.
244         * @param incrementerName the name of the incrementer.
245         * @param columnName the name of the column to increment.
246         */
247        private EnhancedMySQLMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) {
248            super(dataSource, incrementerName, columnName);
249        }
250
251        /**
252         * {@inheritDoc}
253         */
254        @Override
255        public synchronized void afterPropertiesSet() {
256            super.afterPropertiesSet();
257            template = new JdbcTemplate(getDataSource());
258        }
259
260        /**
261         * {@inheritDoc}
262         */
263        @Override
264        protected synchronized long getNextKey() throws DataAccessException {
265            return template.execute(new ConnectionCallback<Long>() {
266                @Override
267                public Long doInConnection(Connection con) throws SQLException, DataAccessException {
268                    Statement statement = null;
269                    ResultSet resultSet = null;
270                    try {
271                        statement = con.createStatement();
272                        String sql = "INSERT INTO " + getIncrementerName() + " VALUES (NULL)";
273                        statement.executeUpdate(sql);
274                        sql = "SELECT LAST_INSERT_ID()";
275                        resultSet = statement.executeQuery(sql);
276                        if (resultSet != null) {
277                            resultSet.first();
278                            return resultSet.getLong(1);
279                        } else {
280                            throw new IncorrectResultSizeDataAccessException("Failed to get last_insert_id() for sequence incrementer table '" + getIncrementerName() + "'", 1);
281                        }
282                    } finally {
283                        JdbcUtils.closeResultSet(resultSet);
284                        JdbcUtils.closeStatement(statement);
285                    }
286                }
287            }).longValue();
288        }
289    }
290
291    /**
292     * No-op constructor for final class.
293     */
294    private MaxValueIncrementerFactory() {}
295
296}