001: /*
002: * Copyright 2002-2006 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: * http://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:
017: package org.springframework.jdbc.support;
018:
019: import java.util.Collections;
020: import java.util.HashMap;
021: import java.util.Iterator;
022: import java.util.Map;
023:
024: import javax.sql.DataSource;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028:
029: import org.springframework.beans.BeansException;
030: import org.springframework.beans.factory.support.DefaultListableBeanFactory;
031: import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
032: import org.springframework.core.io.ClassPathResource;
033: import org.springframework.core.io.Resource;
034: import org.springframework.util.Assert;
035: import org.springframework.util.PatternMatchUtils;
036:
037: /**
038: * Factory for creating {@link SQLErrorCodes} based on the
039: * "databaseProductName" taken from the {@link java.sql.DatabaseMetaData}.
040: *
041: * <p>Returns <code>SQLErrorCodes</code> populated with vendor codes
042: * defined in a configuration file named "sql-error-codes.xml".
043: * Reads the default file in this package if not overridden by a file in
044: * the root of the class path (for example in the "/WEB-INF/classes" directory).
045: *
046: * @author Thomas Risberg
047: * @author Rod Johnson
048: * @author Juergen Hoeller
049: * @see java.sql.DatabaseMetaData#getDatabaseProductName()
050: */
051: public class SQLErrorCodesFactory {
052:
053: /**
054: * The name of custom SQL error codes file, loading from the root
055: * of the class path (e.g. from the "/WEB-INF/classes" directory).
056: */
057: public static final String SQL_ERROR_CODE_OVERRIDE_PATH = "sql-error-codes.xml";
058:
059: /**
060: * The name of default SQL error code files, loading from the class path.
061: */
062: public static final String SQL_ERROR_CODE_DEFAULT_PATH = "org/springframework/jdbc/support/sql-error-codes.xml";
063:
064: private static final Log logger = LogFactory
065: .getLog(SQLErrorCodesFactory.class);
066:
067: /**
068: * Keep track of a single instance so we can return it to classes that request it.
069: */
070: private static final SQLErrorCodesFactory instance = new SQLErrorCodesFactory();
071:
072: /**
073: * Return the singleton instance.
074: */
075: public static SQLErrorCodesFactory getInstance() {
076: return instance;
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 errorCodesMap;
084:
085: /**
086: * Map to cache the SQLErrorCodes instance per DataSource.
087: * Key is the DataSource, value is the SQLErrorCodes instance.
088: */
089: private final Map dataSourceCache = new HashMap(16);
090:
091: /**
092: * Create a new instance of the {@link SQLErrorCodesFactory} class.
093: * <p>Not public to enforce Singleton design pattern. Would be private
094: * except to allow testing via overriding the
095: * {@link #loadResource(String)} method.
096: * <p><b>Do not subclass in application code.</b>
097: * @see #loadResource(String)
098: */
099: protected SQLErrorCodesFactory() {
100: Map errorCodes = null;
101:
102: try {
103: DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
104: XmlBeanDefinitionReader bdr = new XmlBeanDefinitionReader(
105: lbf);
106:
107: // Load default SQL error codes.
108: Resource resource = loadResource(SQL_ERROR_CODE_DEFAULT_PATH);
109: if (resource != null && resource.exists()) {
110: bdr.loadBeanDefinitions(resource);
111: } else {
112: logger
113: .warn("Default sql-error-codes.xml not found (should be included in spring.jar)");
114: }
115:
116: // Load custom SQL error codes, overriding defaults.
117: resource = loadResource(SQL_ERROR_CODE_OVERRIDE_PATH);
118: if (resource != null && resource.exists()) {
119: bdr.loadBeanDefinitions(resource);
120: logger
121: .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,
126: false);
127: if (logger.isInfoEnabled()) {
128: logger.info("SQLErrorCodes loaded: "
129: + errorCodes.keySet());
130: }
131: } catch (BeansException ex) {
132: logger.warn(
133: "Error loading SQL error codes from config file",
134: ex);
135: errorCodes = Collections.EMPTY_MAP;
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</code> if the resource wasn't found
150: * @see #getInstance
151: */
152: protected Resource loadResource(String path) {
153: return new ClassPathResource(path);
154: }
155:
156: /**
157: * Return the {@link SQLErrorCodes} instance for the given database.
158: * <p>No need for a database metadata lookup.
159: * @param dbName the database name (must not be <code>null</code> )
160: * @return the <code>SQLErrorCodes</code> instance for the given database
161: * @throws IllegalArgumentException if the supplied database name is <code>null</code>
162: */
163: public SQLErrorCodes getErrorCodes(String dbName) {
164: Assert
165: .notNull(dbName,
166: "Database product name must not be null");
167:
168: SQLErrorCodes sec = (SQLErrorCodes) this .errorCodesMap
169: .get(dbName);
170: if (sec == null) {
171: for (Iterator it = this .errorCodesMap.values().iterator(); it
172: .hasNext();) {
173: SQLErrorCodes candidate = (SQLErrorCodes) it.next();
174: if (PatternMatchUtils.simpleMatch(candidate
175: .getDatabaseProductNames(), dbName)) {
176: sec = candidate;
177: break;
178: }
179: }
180: }
181: if (sec != null) {
182: if (logger.isDebugEnabled()) {
183: logger.debug("SQL error codes for '" + dbName
184: + "' found");
185: }
186: return sec;
187: }
188:
189: // Could not find the database among the defined ones.
190: if (logger.isDebugEnabled()) {
191: logger.debug("SQL error codes for '" + dbName
192: + "' not found");
193: }
194: return new SQLErrorCodes();
195: }
196:
197: /**
198: * Return {@link SQLErrorCodes} for the given {@link DataSource},
199: * evaluating "databaseProductName" from the
200: * {@link java.sql.DatabaseMetaData}, or an empty error codes
201: * instance if no <code>SQLErrorCodes</code> were found.
202: * @param dataSource the <code>DataSource</code> identifying the database
203: * @see java.sql.DatabaseMetaData#getDatabaseProductName
204: */
205: public SQLErrorCodes getErrorCodes(DataSource dataSource) {
206: Assert.notNull(dataSource, "DataSource must not be null");
207: if (logger.isDebugEnabled()) {
208: logger
209: .debug("Looking up default SQLErrorCodes for DataSource ["
210: + dataSource + "]");
211: }
212:
213: synchronized (this .dataSourceCache) {
214: // Let's avoid looking up database product info if we can.
215: SQLErrorCodes sec = (SQLErrorCodes) this .dataSourceCache
216: .get(dataSource);
217: if (sec != null) {
218: if (logger.isDebugEnabled()) {
219: logger
220: .debug("SQLErrorCodes found in cache for DataSource ["
221: + dataSource + "]");
222: }
223: return sec;
224: }
225: // We could not find it - got to look it up.
226: try {
227: String dbName = (String) JdbcUtils
228: .extractDatabaseMetaData(dataSource,
229: "getDatabaseProductName");
230: if (dbName != null) {
231: if (logger.isDebugEnabled()) {
232: logger
233: .debug("Database product name cached for DataSource ["
234: + dataSource
235: + "]: name is '"
236: + dbName + "'");
237: }
238: sec = getErrorCodes(dbName);
239: this .dataSourceCache.put(dataSource, sec);
240: return sec;
241: }
242: } catch (MetaDataAccessException ex) {
243: logger
244: .warn(
245: "Error while extracting database product name - falling back to empty error codes",
246: ex);
247: }
248: }
249:
250: // Fallback is to return an empty SQLErrorCodes instance.
251: return new SQLErrorCodes();
252: }
253:
254: }
|