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.Map;
38  import java.util.concurrent.ConcurrentHashMap;
39  import java.util.concurrent.ConcurrentMap;
40  
41  /**
42   * Factory for obtaining instances of {@link DataFieldMaxValueIncrementer} for a given {@link DataSource} and
43   * incrementer name.
44   *
45   * <p>
46   * These incrementers are used for getting generated incrementing values like that provided by a database-level sequence
47   * generator.
48   * </p>
49   *
50   * <p>
51   * Note that not all database platforms support sequences natively, so incrementers can be returned that emulate
52   * sequence-like behavior. The Spring Framework provides incrementer implementations for numerous different database
53   * platforms. This classes uses {@link DatabasePlatforms} to determine the platform of the given {@link DataSource}.
54   * </p>
55   *
56   * <p>
57   * Note that this class will cache internally any incrementers for a given {@link DataSource} + Incrementer Name
58   * combination.
59   * </p>
60   *
61   * @author Kuali Rice Team (rice.collab@kuali.org)
62   */
63  public final class MaxValueIncrementerFactory {
64  
65      private static final String ID_COLUMN_NAME = "ID";
66  
67      /**
68       * Prefix for property names used to identify the classname for a {@link DataFieldMaxValueIncrementer} to use for a
69       * given platform.
70       *
71       * <p>To construct a full property name, concatenate this prefix with the platform name.</p>
72       *
73       * @see org.kuali.rice.krad.data.platform.MaxValueIncrementerFactory
74       */
75      public static final String PLATFORM_INCREMENTER_PREFIX = "rice.krad.data.platform.incrementer.";
76  
77      private static final ConcurrentMap<DataSource, ConcurrentMap<String, DataFieldMaxValueIncrementer>> cache =
78              new ConcurrentHashMap<DataSource, ConcurrentMap<String, DataFieldMaxValueIncrementer>>(8, 0.9f, 1);
79  
80      /**
81       * Either constructs a new incrementer or retrieves a cached instance for the given DataSource and target
82       * incrementer name.
83       *
84       * @param dataSource the {@link DataSource} for which to retrieve the incrementer.
85       * @param incrementerName the case-insensitive name of the incrementer to use, this will generally be the name of
86       *        the database object which is used to implement the incrementer.
87       * @return an incrementer that can be used to generate the next incremented value for the given incrementer against
88       *         the specified {@link DataSource}.
89       *
90       * @throws IllegalArgumentException if dataSource or incrementerName are null or blank.
91       */
92      public static DataFieldMaxValueIncrementer getIncrementer(DataSource dataSource, String incrementerName) {
93          if (dataSource == null) {
94              throw new IllegalArgumentException("DataSource must not be null");
95          }
96          if (StringUtils.isBlank(incrementerName)) {
97              throw new IllegalArgumentException("Incrementer name must not be null or blank");
98          }
99          // yes, we want to check if it's there first, then put if absent, for max speed! This is like ConcurrentMap's
100         // version of double-checked locking.
101         ConcurrentMap<String, DataFieldMaxValueIncrementer> incrementerCache = cache.get(dataSource);
102         if (incrementerCache == null) {
103             incrementerCache = cache.putIfAbsent(dataSource,
104                     new ConcurrentHashMap<String, DataFieldMaxValueIncrementer>(8, 0.9f, 1));
105             if (incrementerCache == null) {
106                 incrementerCache = cache.get(dataSource);
107             }
108         }
109 
110         // now check if we have a cached incrementer
111         DataFieldMaxValueIncrementer incrementer = incrementerCache.get(incrementerName.toUpperCase());
112         if (incrementer == null) {
113             incrementer = incrementerCache.putIfAbsent(incrementerName.toUpperCase(), createIncrementer(dataSource,
114                     incrementerName));
115             if (incrementer == null) {
116                 incrementer = incrementerCache.get(incrementerName.toUpperCase());
117             }
118         }
119         return incrementer;
120 
121     }
122 
123     /**
124      * Creates an {@link DataFieldMaxValueIncrementer} from a {@link DataSource}.
125      *
126      * @param dataSource the {@link DataSource} for which to retrieve the incrementer.
127      * @param incrementerName the name of the incrementer.
128      * @return an {@link DataFieldMaxValueIncrementer} from a {@link DataSource}.
129      */
130     private static DataFieldMaxValueIncrementer createIncrementer(DataSource dataSource, String incrementerName) {
131         DatabasePlatformInfo platformInfo = DatabasePlatforms.detectPlatform(dataSource);
132         DataFieldMaxValueIncrementer incrementer = getCustomizedIncrementer(platformInfo, dataSource,incrementerName,ID_COLUMN_NAME);
133         if(incrementer != null){
134             return incrementer;
135         }
136 
137         if (DatabasePlatforms.ORACLE.equalsIgnoreCase(platformInfo.getName())) {
138             incrementer = new OracleSequenceMaxValueIncrementer(dataSource, incrementerName);
139         } else if (DatabasePlatforms.MYSQL.equalsIgnoreCase(platformInfo.getName())) {
140             incrementer = new EnhancedMySQLMaxValueIncrementer(dataSource, incrementerName, ID_COLUMN_NAME);
141         }
142         if (incrementer == null) {
143             throw new UnsupportedDatabasePlatformException(platformInfo);
144         }
145         if (incrementer instanceof InitializingBean) {
146             try {
147                 ((InitializingBean) incrementer).afterPropertiesSet();
148             } catch (Exception e) {
149                 throw new DataAccessResourceFailureException(
150                         "Failed to initialize max value incrementer for given datasource and incrementer. dataSource="
151                                 + dataSource.toString()
152                                 + ", incrementerName = "
153                                 + incrementerName, e);
154             }
155         }
156         return incrementer;
157     }
158 
159     /**
160      * Checks the config file for any references to
161      * {@code rice.krad.data.platform.incrementer.(DATASOURCE, ex mysql, oracle).(VERSION optional)}.
162      *
163      * <p>If matching one found attempts to instantiate it to return back to factory for use.</p>
164      *
165      * @param platformInfo the {@link DatabasePlatformInfo}.
166      * @param dataSource the {@link DataSource} for which to retrieve the incrementer.
167      * @param incrementerName the name of the incrementer.
168      * @param columnName the name of the column to increment.
169      * @return a config set customized incrementer that matches and can be used to generate the next incremented value
170      *         for the given incrementer against the specified {@link DataSource}
171      * @throws InstantiationError if cannot instantiate passed in class.
172      */
173     private static DataFieldMaxValueIncrementer getCustomizedIncrementer(DatabasePlatformInfo platformInfo, DataSource dataSource, String incrementerName, String columnName){
174         if(platformInfo == null){
175             throw new  IllegalArgumentException("DataSource platform must not be null");
176         }
177         if(ConfigContext.getCurrentContextConfig() == null){
178             return null;
179         }
180         Map<String,String> incrementerPropToIncrementer = ConfigContext.getCurrentContextConfig().
181                                 getPropertiesWithPrefix(PLATFORM_INCREMENTER_PREFIX, true);
182         String platformNameVersion = platformInfo.getName().toLowerCase() + "." + platformInfo.getMajorVersion();
183         String incrementerClassName = "";
184 
185          if(incrementerPropToIncrementer.containsKey(platformNameVersion)){
186             incrementerClassName = incrementerPropToIncrementer.get(platformNameVersion);
187          } else if(incrementerPropToIncrementer.containsKey(platformInfo.getName().toLowerCase())){
188              incrementerClassName = incrementerPropToIncrementer.get(platformInfo.getName().toLowerCase());
189          }
190 
191         if(StringUtils.isNotBlank(incrementerClassName)){
192             try {
193                 Class incrementerClass = Class.forName(incrementerClassName);
194                 if(AbstractSequenceMaxValueIncrementer.class.isAssignableFrom(incrementerClass)){
195                     AbstractSequenceMaxValueIncrementer abstractSequenceMaxValueIncrementer = (AbstractSequenceMaxValueIncrementer)incrementerClass.newInstance();
196                     abstractSequenceMaxValueIncrementer.setDataSource(dataSource);
197                     abstractSequenceMaxValueIncrementer.setIncrementerName(incrementerName);
198                     return abstractSequenceMaxValueIncrementer;
199 
200                 } else if(AbstractColumnMaxValueIncrementer.class.isAssignableFrom(incrementerClass)){
201                     AbstractColumnMaxValueIncrementer abstractColumnMaxValueIncrementer = (AbstractColumnMaxValueIncrementer)incrementerClass.newInstance();
202                     abstractColumnMaxValueIncrementer.setDataSource(dataSource);
203                     abstractColumnMaxValueIncrementer.setIncrementerName(incrementerName);
204                     abstractColumnMaxValueIncrementer.setColumnName(columnName);
205                     return abstractColumnMaxValueIncrementer;
206                 } else {
207                     throw new InstantiationError("Cannot create incrementer class "+incrementerClassName +" it has to extend "
208                             + "AbstractSequenceMaxValueIncrementer or AbstractColumnMaxValueIncrementer");
209                 }
210             } catch (Exception e){
211                 throw new InstantiationError("Could not instantiate custom incrementer "+incrementerClassName);
212             }
213         }
214         return null;
215     }
216 
217     /**
218      * Defines an incrementer for MySQL.
219      *
220      * <p>
221      * Since MySQL does not have any sense of a sequence, this class uses the concept of a sequence table, which is a
222      * regular table that has an auto increment feature on it and is used only for that sequence.  When null values are
223      * inserted into the table, the auto increment feature will insert the next value into that field, and then the
224      * database will be queried for the last insert ID to get the next sequence value.
225      * </p>
226      */
227     static final class EnhancedMySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer {
228 
229         private JdbcTemplate template;
230 
231         /**
232          * Creates an incrementer for MySQL.
233          */
234         private EnhancedMySQLMaxValueIncrementer() {}
235 
236         /**
237          * Creates an incrementer for MySQL.
238          *
239          * @param dataSource the {@link DataSource} for which to retrieve the incrementer.
240          * @param incrementerName the name of the incrementer.
241          * @param columnName the name of the column to increment.
242          */
243         private EnhancedMySQLMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) {
244             super(dataSource, incrementerName, columnName);
245         }
246 
247         /**
248          * {@inheritDoc}
249          */
250         @Override
251         public synchronized void afterPropertiesSet() {
252             super.afterPropertiesSet();
253             template = new JdbcTemplate(getDataSource());
254         }
255 
256         /**
257          * {@inheritDoc}
258          */
259         @Override
260         protected synchronized long getNextKey() throws DataAccessException {
261             return template.execute(new ConnectionCallback<Long>() {
262                 @Override
263                 public Long doInConnection(Connection con) throws SQLException, DataAccessException {
264                     Statement statement = null;
265                     ResultSet resultSet = null;
266                     try {
267                         statement = con.createStatement();
268                         String sql = "INSERT INTO " + getIncrementerName() + " VALUES (NULL)";
269                         statement.executeUpdate(sql);
270                         sql = "SELECT LAST_INSERT_ID()";
271                         resultSet = statement.executeQuery(sql);
272                         if (resultSet != null) {
273                             resultSet.first();
274                             return resultSet.getLong(1);
275                         } else {
276                             throw new IncorrectResultSizeDataAccessException("Failed to get last_insert_id() for sequence incrementer table '" + getIncrementerName() + "'", 1);
277                         }
278                     } finally {
279                         JdbcUtils.closeResultSet(resultSet);
280                         JdbcUtils.closeStatement(statement);
281                     }
282                 }
283             }).longValue();
284         }
285     }
286 
287     /**
288      * No-op constructor for final class.
289      */
290     private MaxValueIncrementerFactory() {}
291 
292 }