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