View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.data.platform;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.config.property.ConfigContext;
20  import org.springframework.beans.factory.InitializingBean;
21  import org.springframework.dao.DataAccessException;
22  import org.springframework.dao.DataAccessResourceFailureException;
23  import org.springframework.dao.IncorrectResultSizeDataAccessException;
24  import org.springframework.jdbc.core.ConnectionCallback;
25  import org.springframework.jdbc.core.JdbcTemplate;
26  import org.springframework.jdbc.support.JdbcUtils;
27  import org.springframework.jdbc.support.incrementer.AbstractColumnMaxValueIncrementer;
28  import org.springframework.jdbc.support.incrementer.AbstractSequenceMaxValueIncrementer;
29  import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
30  import org.springframework.jdbc.support.incrementer.OracleSequenceMaxValueIncrementer;
31  
32  import javax.sql.DataSource;
33  import java.sql.Connection;
34  import java.sql.ResultSet;
35  import java.sql.SQLException;
36  import java.sql.Statement;
37  import java.util.Collections;
38  import java.util.IdentityHashMap;
39  import java.util.Map;
40  import java.util.concurrent.ConcurrentHashMap;
41  import java.util.concurrent.ConcurrentMap;
42  
43  /**
44   * Factory for obtaining instances of {@link DataFieldMaxValueIncrementer} for a given {@link DataSource} and
45   * incrementer name.
46   *
47   * <p>
48   * These incrementers are used for getting generated incrementing values like that provided by a database-level sequence
49   * generator.
50   * </p>
51   *
52   * <p>
53   * Note that not all database platforms support sequences natively, so incrementers can be returned that emulate
54   * sequence-like behavior. The Spring Framework provides incrementer implementations for numerous different database
55   * platforms. This classes uses {@link DatabasePlatforms} to determine the platform of the given {@link DataSource}.
56   * </p>
57   *
58   * <p>
59   * Note that this class will cache internally any incrementers for a given {@link DataSource} + Incrementer Name
60   * combination.
61   * </p>
62   *
63   * @author Kuali Rice Team (rice.collab@kuali.org)
64   */
65  public final class MaxValueIncrementerFactory {
66  
67      private static final String ID_COLUMN_NAME = "ID";
68  
69      /**
70       * Prefix for property names used to identify the classname for a {@link DataFieldMaxValueIncrementer} to use for a
71       * given platform.
72       *
73       * <p>To construct a full property name, concatenate this prefix with the platform name.</p>
74       *
75       * @see org.kuali.rice.krad.data.platform.MaxValueIncrementerFactory
76       */
77      public static final String PLATFORM_INCREMENTER_PREFIX = "rice.krad.data.platform.incrementer.";
78  
79      private static final Map<DataSource, ConcurrentMap<String, DataFieldMaxValueIncrementer>> cache
80              = Collections.synchronizedMap(new IdentityHashMap<DataSource, ConcurrentMap<String, DataFieldMaxValueIncrementer>>(8));
81  
82      /**
83       * Either constructs a new incrementer or retrieves a cached instance for the given DataSource and target
84       * incrementer name.
85       *
86       * @param dataSource the {@link DataSource} for which to retrieve the incrementer.
87       * @param incrementerName the case-insensitive name of the incrementer to use, this will generally be the name of
88       *        the database object which is used to implement the incrementer.
89       * @return an incrementer that can be used to generate the next incremented value for the given incrementer against
90       *         the specified {@link DataSource}.
91       *
92       * @throws IllegalArgumentException if dataSource or incrementerName are null or blank.
93       */
94      public static DataFieldMaxValueIncrementer getIncrementer(DataSource dataSource, String incrementerName) {
95          if (dataSource == null) {
96              throw new IllegalArgumentException("DataSource must not be null");
97          }
98          if (StringUtils.isBlank(incrementerName)) {
99              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 }