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}