001/* 002 * Copyright 2002-2018 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.jdbc.support; 018 019import java.util.Collections; 020import java.util.Map; 021import javax.sql.DataSource; 022 023import org.apache.commons.logging.Log; 024import org.apache.commons.logging.LogFactory; 025 026import org.springframework.beans.BeansException; 027import org.springframework.beans.factory.support.DefaultListableBeanFactory; 028import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; 029import org.springframework.core.io.ClassPathResource; 030import org.springframework.core.io.Resource; 031import org.springframework.util.Assert; 032import org.springframework.util.ConcurrentReferenceHashMap; 033import org.springframework.util.PatternMatchUtils; 034 035/** 036 * Factory for creating {@link SQLErrorCodes} based on the 037 * "databaseProductName" taken from the {@link java.sql.DatabaseMetaData}. 038 * 039 * <p>Returns {@code SQLErrorCodes} populated with vendor codes 040 * defined in a configuration file named "sql-error-codes.xml". 041 * Reads the default file in this package if not overridden by a file in 042 * the root of the class path (for example in the "/WEB-INF/classes" directory). 043 * 044 * @author Thomas Risberg 045 * @author Rod Johnson 046 * @author Juergen Hoeller 047 * @see java.sql.DatabaseMetaData#getDatabaseProductName() 048 */ 049public class SQLErrorCodesFactory { 050 051 /** 052 * The name of custom SQL error codes file, loading from the root 053 * of the class path (e.g. from the "/WEB-INF/classes" directory). 054 */ 055 public static final String SQL_ERROR_CODE_OVERRIDE_PATH = "sql-error-codes.xml"; 056 057 /** 058 * The name of default SQL error code files, loading from the class path. 059 */ 060 public static final String SQL_ERROR_CODE_DEFAULT_PATH = "org/springframework/jdbc/support/sql-error-codes.xml"; 061 062 063 private static final Log logger = LogFactory.getLog(SQLErrorCodesFactory.class); 064 065 /** 066 * Keep track of a single instance so we can return it to classes that request it. 067 */ 068 private static final SQLErrorCodesFactory instance = new SQLErrorCodesFactory(); 069 070 071 /** 072 * Return the singleton instance. 073 */ 074 public static SQLErrorCodesFactory getInstance() { 075 return instance; 076 } 077 078 079 /** 080 * Map to hold error codes for all databases defined in the config file. 081 * Key is the database product name, value is the SQLErrorCodes instance. 082 */ 083 private final Map<String, SQLErrorCodes> errorCodesMap; 084 085 /** 086 * Map to cache the SQLErrorCodes instance per DataSource. 087 */ 088 private final Map<DataSource, SQLErrorCodes> dataSourceCache = 089 new ConcurrentReferenceHashMap<DataSource, SQLErrorCodes>(16); 090 091 092 /** 093 * Create a new instance of the {@link SQLErrorCodesFactory} class. 094 * <p>Not public to enforce Singleton design pattern. Would be private 095 * except to allow testing via overriding the 096 * {@link #loadResource(String)} method. 097 * <p><b>Do not subclass in application code.</b> 098 * @see #loadResource(String) 099 */ 100 protected SQLErrorCodesFactory() { 101 Map<String, SQLErrorCodes> errorCodes; 102 103 try { 104 DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); 105 lbf.setBeanClassLoader(getClass().getClassLoader()); 106 XmlBeanDefinitionReader bdr = new XmlBeanDefinitionReader(lbf); 107 108 // Load default SQL error codes. 109 Resource resource = loadResource(SQL_ERROR_CODE_DEFAULT_PATH); 110 if (resource != null && resource.exists()) { 111 bdr.loadBeanDefinitions(resource); 112 } 113 else { 114 logger.warn("Default sql-error-codes.xml not found (should be included in spring.jar)"); 115 } 116 117 // Load custom SQL error codes, overriding defaults. 118 resource = loadResource(SQL_ERROR_CODE_OVERRIDE_PATH); 119 if (resource != null && resource.exists()) { 120 bdr.loadBeanDefinitions(resource); 121 logger.info("Found custom sql-error-codes.xml file at the root of the classpath"); 122 } 123 124 // Check all beans of type SQLErrorCodes. 125 errorCodes = lbf.getBeansOfType(SQLErrorCodes.class, true, false); 126 if (logger.isDebugEnabled()) { 127 logger.debug("SQLErrorCodes loaded: " + errorCodes.keySet()); 128 } 129 } 130 catch (BeansException ex) { 131 logger.warn("Error loading SQL error codes from config file", ex); 132 errorCodes = Collections.emptyMap(); 133 } 134 135 this.errorCodesMap = errorCodes; 136 } 137 138 /** 139 * Load the given resource from the class path. 140 * <p><b>Not to be overridden by application developers, who should obtain 141 * instances of this class from the static {@link #getInstance()} method.</b> 142 * <p>Protected for testability. 143 * @param path resource path; either a custom path or one of either 144 * {@link #SQL_ERROR_CODE_DEFAULT_PATH} or 145 * {@link #SQL_ERROR_CODE_OVERRIDE_PATH}. 146 * @return the resource, or {@code null} if the resource wasn't found 147 * @see #getInstance 148 */ 149 protected Resource loadResource(String path) { 150 return new ClassPathResource(path, getClass().getClassLoader()); 151 } 152 153 154 /** 155 * Return the {@link SQLErrorCodes} instance for the given database. 156 * <p>No need for a database meta-data lookup. 157 * @param databaseName the database name (must not be {@code null}) 158 * @return the {@code SQLErrorCodes} instance for the given database 159 * @throws IllegalArgumentException if the supplied database name is {@code null} 160 */ 161 public SQLErrorCodes getErrorCodes(String databaseName) { 162 Assert.notNull(databaseName, "Database product name must not be null"); 163 164 SQLErrorCodes sec = this.errorCodesMap.get(databaseName); 165 if (sec == null) { 166 for (SQLErrorCodes candidate : this.errorCodesMap.values()) { 167 if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), databaseName)) { 168 sec = candidate; 169 break; 170 } 171 } 172 } 173 if (sec != null) { 174 checkCustomTranslatorRegistry(databaseName, sec); 175 if (logger.isDebugEnabled()) { 176 logger.debug("SQL error codes for '" + databaseName + "' found"); 177 } 178 return sec; 179 } 180 181 // Could not find the database among the defined ones. 182 if (logger.isDebugEnabled()) { 183 logger.debug("SQL error codes for '" + databaseName + "' not found"); 184 } 185 return new SQLErrorCodes(); 186 } 187 188 /** 189 * Return {@link SQLErrorCodes} for the given {@link DataSource}, 190 * evaluating "databaseProductName" from the 191 * {@link java.sql.DatabaseMetaData}, or an empty error codes 192 * instance if no {@code SQLErrorCodes} were found. 193 * @param dataSource the {@code DataSource} identifying the database 194 * @return the corresponding {@code SQLErrorCodes} object 195 * @see java.sql.DatabaseMetaData#getDatabaseProductName() 196 */ 197 public SQLErrorCodes getErrorCodes(DataSource dataSource) { 198 Assert.notNull(dataSource, "DataSource must not be null"); 199 if (logger.isDebugEnabled()) { 200 logger.debug("Looking up default SQLErrorCodes for DataSource [" + identify(dataSource) + "]"); 201 } 202 203 // Try efficient lock-free access for existing cache entry 204 SQLErrorCodes sec = this.dataSourceCache.get(dataSource); 205 if (sec == null) { 206 synchronized (this.dataSourceCache) { 207 // Double-check within full dataSourceCache lock 208 sec = this.dataSourceCache.get(dataSource); 209 if (sec == null) { 210 // We could not find it - got to look it up. 211 try { 212 String name = (String) JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName"); 213 if (name != null) { 214 return registerDatabase(dataSource, name); 215 } 216 } 217 catch (MetaDataAccessException ex) { 218 logger.warn("Error while extracting database name - falling back to empty error codes", ex); 219 } 220 // Fallback is to return an empty SQLErrorCodes instance. 221 return new SQLErrorCodes(); 222 } 223 } 224 } 225 226 if (logger.isDebugEnabled()) { 227 logger.debug("SQLErrorCodes found in cache for DataSource [" + identify(dataSource) + "]"); 228 } 229 230 return sec; 231 } 232 233 /** 234 * Associate the specified database name with the given {@link DataSource}. 235 * @param dataSource the {@code DataSource} identifying the database 236 * @param databaseName the corresponding database name as stated in the error codes 237 * definition file (must not be {@code null}) 238 * @return the corresponding {@code SQLErrorCodes} object (never {@code null}) 239 * @see #unregisterDatabase(DataSource) 240 */ 241 public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName) { 242 SQLErrorCodes sec = getErrorCodes(databaseName); 243 if (logger.isDebugEnabled()) { 244 logger.debug("Caching SQL error codes for DataSource [" + identify(dataSource) + 245 "]: database product name is '" + databaseName + "'"); 246 } 247 this.dataSourceCache.put(dataSource, sec); 248 return sec; 249 } 250 251 /** 252 * Clear the cache for the specified {@link DataSource}, if registered. 253 * @param dataSource the {@code DataSource} identifying the database 254 * @return the corresponding {@code SQLErrorCodes} object that got removed, 255 * or {@code null} if not registered 256 * @since 4.3.5 257 * @see #registerDatabase(DataSource, String) 258 */ 259 public SQLErrorCodes unregisterDatabase(DataSource dataSource) { 260 return this.dataSourceCache.remove(dataSource); 261 } 262 263 /** 264 * Build an identification String for the given {@link DataSource}, 265 * primarily for logging purposes. 266 * @param dataSource the {@code DataSource} to introspect 267 * @return the identification String 268 */ 269 private String identify(DataSource dataSource) { 270 return dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()); 271 } 272 273 /** 274 * Check the {@link CustomSQLExceptionTranslatorRegistry} for any entries. 275 */ 276 private void checkCustomTranslatorRegistry(String databaseName, SQLErrorCodes errorCodes) { 277 SQLExceptionTranslator customTranslator = 278 CustomSQLExceptionTranslatorRegistry.getInstance().findTranslatorForDatabase(databaseName); 279 if (customTranslator != null) { 280 if (errorCodes.getCustomSqlExceptionTranslator() != null && logger.isWarnEnabled()) { 281 logger.warn("Overriding already defined custom translator '" + 282 errorCodes.getCustomSqlExceptionTranslator().getClass().getSimpleName() + 283 " with '" + customTranslator.getClass().getSimpleName() + 284 "' found in the CustomSQLExceptionTranslatorRegistry for database '" + databaseName + "'"); 285 } 286 else if (logger.isInfoEnabled()) { 287 logger.info("Using custom translator '" + customTranslator.getClass().getSimpleName() + 288 "' found in the CustomSQLExceptionTranslatorRegistry for database '" + databaseName + "'"); 289 } 290 errorCodes.setCustomSqlExceptionTranslator(customTranslator); 291 } 292 } 293 294}