/*
 * Copyright 2002-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.jdbc.support;

import java.sql.DatabaseMetaData;
import java.util.Collections;
import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;

Factory for creating SQLErrorCodes based on the "databaseProductName" taken from the DatabaseMetaData.

Returns SQLErrorCodes populated with vendor codes defined in a configuration file named "sql-error-codes.xml". Reads the default file in this package if not overridden by a file in the root of the class path (for example in the "/WEB-INF/classes" directory).

Author:Thomas Risberg, Rod Johnson, Juergen Hoeller
See Also:
/** * Factory for creating {@link SQLErrorCodes} based on the * "databaseProductName" taken from the {@link java.sql.DatabaseMetaData}. * * <p>Returns {@code SQLErrorCodes} populated with vendor codes * defined in a configuration file named "sql-error-codes.xml". * Reads the default file in this package if not overridden by a file in * the root of the class path (for example in the "/WEB-INF/classes" directory). * * @author Thomas Risberg * @author Rod Johnson * @author Juergen Hoeller * @see java.sql.DatabaseMetaData#getDatabaseProductName() */
public class SQLErrorCodesFactory {
The name of custom SQL error codes file, loading from the root of the class path (e.g. from the "/WEB-INF/classes" directory).
/** * The name of custom SQL error codes file, loading from the root * of the class path (e.g. from the "/WEB-INF/classes" directory). */
public static final String SQL_ERROR_CODE_OVERRIDE_PATH = "sql-error-codes.xml";
The name of default SQL error code files, loading from the class path.
/** * The name of default SQL error code files, loading from the class path. */
public static final String SQL_ERROR_CODE_DEFAULT_PATH = "org/springframework/jdbc/support/sql-error-codes.xml"; private static final Log logger = LogFactory.getLog(SQLErrorCodesFactory.class);
Keep track of a single instance so we can return it to classes that request it.
/** * Keep track of a single instance so we can return it to classes that request it. */
private static final SQLErrorCodesFactory instance = new SQLErrorCodesFactory();
Return the singleton instance.
/** * Return the singleton instance. */
public static SQLErrorCodesFactory getInstance() { return instance; }
Map to hold error codes for all databases defined in the config file. Key is the database product name, value is the SQLErrorCodes instance.
/** * Map to hold error codes for all databases defined in the config file. * Key is the database product name, value is the SQLErrorCodes instance. */
private final Map<String, SQLErrorCodes> errorCodesMap;
Map to cache the SQLErrorCodes instance per DataSource.
/** * Map to cache the SQLErrorCodes instance per DataSource. */
private final Map<DataSource, SQLErrorCodes> dataSourceCache = new ConcurrentReferenceHashMap<>(16);
Create a new instance of the SQLErrorCodesFactory class.

Not public to enforce Singleton design pattern. Would be private except to allow testing via overriding the loadResource(String) method.

Do not subclass in application code.

See Also:
/** * Create a new instance of the {@link SQLErrorCodesFactory} class. * <p>Not public to enforce Singleton design pattern. Would be private * except to allow testing via overriding the * {@link #loadResource(String)} method. * <p><b>Do not subclass in application code.</b> * @see #loadResource(String) */
protected SQLErrorCodesFactory() { Map<String, SQLErrorCodes> errorCodes; try { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); lbf.setBeanClassLoader(getClass().getClassLoader()); XmlBeanDefinitionReader bdr = new XmlBeanDefinitionReader(lbf); // Load default SQL error codes. Resource resource = loadResource(SQL_ERROR_CODE_DEFAULT_PATH); if (resource != null && resource.exists()) { bdr.loadBeanDefinitions(resource); } else { logger.info("Default sql-error-codes.xml not found (should be included in spring-jdbc jar)"); } // Load custom SQL error codes, overriding defaults. resource = loadResource(SQL_ERROR_CODE_OVERRIDE_PATH); if (resource != null && resource.exists()) { bdr.loadBeanDefinitions(resource); logger.debug("Found custom sql-error-codes.xml file at the root of the classpath"); } // Check all beans of type SQLErrorCodes. errorCodes = lbf.getBeansOfType(SQLErrorCodes.class, true, false); if (logger.isTraceEnabled()) { logger.trace("SQLErrorCodes loaded: " + errorCodes.keySet()); } } catch (BeansException ex) { logger.warn("Error loading SQL error codes from config file", ex); errorCodes = Collections.emptyMap(); } this.errorCodesMap = errorCodes; }
Load the given resource from the class path.

Not to be overridden by application developers, who should obtain instances of this class from the static getInstance() method.

Protected for testability.

Params:
See Also:
Returns:the resource, or null if the resource wasn't found
/** * Load the given resource from the class path. * <p><b>Not to be overridden by application developers, who should obtain * instances of this class from the static {@link #getInstance()} method.</b> * <p>Protected for testability. * @param path resource path; either a custom path or one of either * {@link #SQL_ERROR_CODE_DEFAULT_PATH} or * {@link #SQL_ERROR_CODE_OVERRIDE_PATH}. * @return the resource, or {@code null} if the resource wasn't found * @see #getInstance */
@Nullable protected Resource loadResource(String path) { return new ClassPathResource(path, getClass().getClassLoader()); }
Return the SQLErrorCodes instance for the given database.

No need for a database meta-data lookup.

Params:
  • databaseName – the database name (must not be null)
Throws:
Returns:the SQLErrorCodes instance for the given database (never null; potentially empty)
/** * Return the {@link SQLErrorCodes} instance for the given database. * <p>No need for a database meta-data lookup. * @param databaseName the database name (must not be {@code null}) * @return the {@code SQLErrorCodes} instance for the given database * (never {@code null}; potentially empty) * @throws IllegalArgumentException if the supplied database name is {@code null} */
public SQLErrorCodes getErrorCodes(String databaseName) { Assert.notNull(databaseName, "Database product name must not be null"); SQLErrorCodes sec = this.errorCodesMap.get(databaseName); if (sec == null) { for (SQLErrorCodes candidate : this.errorCodesMap.values()) { if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), databaseName)) { sec = candidate; break; } } } if (sec != null) { checkCustomTranslatorRegistry(databaseName, sec); if (logger.isDebugEnabled()) { logger.debug("SQL error codes for '" + databaseName + "' found"); } return sec; } // Could not find the database among the defined ones. if (logger.isDebugEnabled()) { logger.debug("SQL error codes for '" + databaseName + "' not found"); } return new SQLErrorCodes(); }
Return SQLErrorCodes for the given DataSource, evaluating "databaseProductName" from the DatabaseMetaData, or an empty error codes instance if no SQLErrorCodes were found.
Params:
  • dataSource – the DataSource identifying the database
See Also:
Returns:the corresponding SQLErrorCodes object (never null; potentially empty)
/** * Return {@link SQLErrorCodes} for the given {@link DataSource}, * evaluating "databaseProductName" from the * {@link java.sql.DatabaseMetaData}, or an empty error codes * instance if no {@code SQLErrorCodes} were found. * @param dataSource the {@code DataSource} identifying the database * @return the corresponding {@code SQLErrorCodes} object * (never {@code null}; potentially empty) * @see java.sql.DatabaseMetaData#getDatabaseProductName() */
public SQLErrorCodes getErrorCodes(DataSource dataSource) { SQLErrorCodes sec = resolveErrorCodes(dataSource); return (sec != null ? sec : new SQLErrorCodes()); }
Return SQLErrorCodes for the given DataSource, evaluating "databaseProductName" from the DatabaseMetaData, or null if case of a JDBC meta-data access problem.
Params:
  • dataSource – the DataSource identifying the database
See Also:
Returns:the corresponding SQLErrorCodes object, or null in case of a JDBC meta-data access problem
Since:5.2.9
/** * Return {@link SQLErrorCodes} for the given {@link DataSource}, * evaluating "databaseProductName" from the * {@link java.sql.DatabaseMetaData}, or {@code null} if case * of a JDBC meta-data access problem. * @param dataSource the {@code DataSource} identifying the database * @return the corresponding {@code SQLErrorCodes} object, * or {@code null} in case of a JDBC meta-data access problem * @since 5.2.9 * @see java.sql.DatabaseMetaData#getDatabaseProductName() */
@Nullable public SQLErrorCodes resolveErrorCodes(DataSource dataSource) { Assert.notNull(dataSource, "DataSource must not be null"); if (logger.isDebugEnabled()) { logger.debug("Looking up default SQLErrorCodes for DataSource [" + identify(dataSource) + "]"); } // Try efficient lock-free access for existing cache entry SQLErrorCodes sec = this.dataSourceCache.get(dataSource); if (sec == null) { synchronized (this.dataSourceCache) { // Double-check within full dataSourceCache lock sec = this.dataSourceCache.get(dataSource); if (sec == null) { // We could not find it - got to look it up. try { String name = JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getDatabaseProductName); if (StringUtils.hasLength(name)) { return registerDatabase(dataSource, name); } } catch (MetaDataAccessException ex) { logger.warn("Error while extracting database name", ex); } return null; } } } if (logger.isDebugEnabled()) { logger.debug("SQLErrorCodes found in cache for DataSource [" + identify(dataSource) + "]"); } return sec; }
Associate the specified database name with the given DataSource.
Params:
  • dataSource – the DataSource identifying the database
  • databaseName – the corresponding database name as stated in the error codes definition file (must not be null)
See Also:
Returns:the corresponding SQLErrorCodes object (never null)
/** * Associate the specified database name with the given {@link DataSource}. * @param dataSource the {@code DataSource} identifying the database * @param databaseName the corresponding database name as stated in the error codes * definition file (must not be {@code null}) * @return the corresponding {@code SQLErrorCodes} object (never {@code null}) * @see #unregisterDatabase(DataSource) */
public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName) { SQLErrorCodes sec = getErrorCodes(databaseName); if (logger.isDebugEnabled()) { logger.debug("Caching SQL error codes for DataSource [" + identify(dataSource) + "]: database product name is '" + databaseName + "'"); } this.dataSourceCache.put(dataSource, sec); return sec; }
Clear the cache for the specified DataSource, if registered.
Params:
  • dataSource – the DataSource identifying the database
See Also:
Returns:the corresponding SQLErrorCodes object that got removed, or null if not registered
Since:4.3.5
/** * Clear the cache for the specified {@link DataSource}, if registered. * @param dataSource the {@code DataSource} identifying the database * @return the corresponding {@code SQLErrorCodes} object that got removed, * or {@code null} if not registered * @since 4.3.5 * @see #registerDatabase(DataSource, String) */
@Nullable public SQLErrorCodes unregisterDatabase(DataSource dataSource) { return this.dataSourceCache.remove(dataSource); }
Build an identification String for the given DataSource, primarily for logging purposes.
Params:
  • dataSource – the DataSource to introspect
Returns:the identification String
/** * Build an identification String for the given {@link DataSource}, * primarily for logging purposes. * @param dataSource the {@code DataSource} to introspect * @return the identification String */
private String identify(DataSource dataSource) { return dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()); }
Check the CustomSQLExceptionTranslatorRegistry for any entries.
/** * Check the {@link CustomSQLExceptionTranslatorRegistry} for any entries. */
private void checkCustomTranslatorRegistry(String databaseName, SQLErrorCodes errorCodes) { SQLExceptionTranslator customTranslator = CustomSQLExceptionTranslatorRegistry.getInstance().findTranslatorForDatabase(databaseName); if (customTranslator != null) { if (errorCodes.getCustomSqlExceptionTranslator() != null && logger.isDebugEnabled()) { logger.debug("Overriding already defined custom translator '" + errorCodes.getCustomSqlExceptionTranslator().getClass().getSimpleName() + " with '" + customTranslator.getClass().getSimpleName() + "' found in the CustomSQLExceptionTranslatorRegistry for database '" + databaseName + "'"); } else if (logger.isTraceEnabled()) { logger.trace("Using custom translator '" + customTranslator.getClass().getSimpleName() + "' found in the CustomSQLExceptionTranslatorRegistry for database '" + databaseName + "'"); } errorCodes.setCustomSqlExceptionTranslator(customTranslator); } } }