Java tutorial
/* * Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates. * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks * of Amazon Technologies, Inc. or its affiliates. All rights reserved. * * 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 * * http://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 com.amazon.carbonado.repo.jdbc; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.math.BigDecimal; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import static java.sql.Types.*; import javax.sql.DataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cojen.classfile.TypeDesc; import org.cojen.util.KeyFactory; import org.cojen.util.ThrowUnchecked; import com.amazon.carbonado.capability.IndexInfo; import com.amazon.carbonado.MalformedTypeException; import com.amazon.carbonado.MismatchException; import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; import com.amazon.carbonado.SupportException; import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.OrderedProperty; import com.amazon.carbonado.info.StorableInfo; import com.amazon.carbonado.info.StorableIntrospector; import com.amazon.carbonado.info.StorableIndex; import com.amazon.carbonado.info.StorableKey; import com.amazon.carbonado.info.StorableProperty; import com.amazon.carbonado.info.StorablePropertyAdapter; import com.amazon.carbonado.info.StorablePropertyConstraint; import com.amazon.carbonado.util.SoftValuedCache; /** * Provides additional metadata for a {@link Storable} type needed by * JDBCRepository. The storable type must match to a table in an external * database. All examined data is cached, so repeat examinations are fast, * unless the examination failed. * * @author Brian S O'Neill * @author Adam D Bradley * @author Tobias Holgers * @author Archit Shivaprakash * @author Matt Carlson */ public class JDBCStorableIntrospector extends StorableIntrospector { // Maps compound keys to softly referenced JDBCStorableInfo objects. @SuppressWarnings("unchecked") private static SoftValuedCache<Object, JDBCStorableInfo<?>> cCache = SoftValuedCache.newCache(11); /** * Examines the given class and returns a JDBCStorableInfo describing it. A * MalformedTypeException is thrown for a variety of reasons if the given * class is not a well-defined Storable type or if it can't match up with * an entity in the external database. * * @param type Storable type to examine * @param ds source of JDBC connections to use for matching to a table * @param catalog optional catalog to search * @param schema optional schema to search * @throws MalformedTypeException if Storable type is not well-formed * @throws RepositoryException if there was a problem in accessing the database * @throws IllegalArgumentException if type is null */ @SuppressWarnings("unchecked") public static <S extends Storable> JDBCStorableInfo<S> examine(Class<S> type, DataSource ds, String catalog, String schema) throws SQLException, SupportException { return examine(type, ds, catalog, schema, null, false); } static <S extends Storable> JDBCStorableInfo<S> examine(Class<S> type, DataSource ds, String catalog, String schema, SchemaResolver resolver, boolean primaryKeyCheckDisabled) throws SQLException, SupportException { Object key = KeyFactory.createKey(new Object[] { type, ds, catalog, schema }); synchronized (cCache) { JDBCStorableInfo<S> jInfo = (JDBCStorableInfo<S>) cCache.get(key); if (jInfo != null) { return jInfo; } // Call superclass for most info. StorableInfo<S> mainInfo = examine(type); Connection con = ds.getConnection(); try { try { jInfo = examine(mainInfo, con, catalog, schema, resolver, primaryKeyCheckDisabled); if (!jInfo.isSupported() && resolver != null && resolver.resolve(mainInfo, con, catalog, schema)) { jInfo = examine(mainInfo, con, catalog, schema, resolver, primaryKeyCheckDisabled); } } catch (SupportException e) { if (resolver != null && resolver.resolve(mainInfo, con, catalog, schema)) { jInfo = examine(mainInfo, con, catalog, schema, resolver, primaryKeyCheckDisabled); } else { throw e; } } } finally { try { con.close(); } catch (SQLException e) { // Don't care. } } cCache.put(key, jInfo); // Finish resolving join properties, after properties have been // added to cache. This makes it possible for joins to (directly or // indirectly) reference their own enclosing type. try { for (JDBCStorableProperty<S> jProperty : jInfo.getAllProperties().values()) { JProperty<S> jp = (JProperty<S>) jProperty; jp.fillInternalJoinElements(ds, catalog, schema, resolver); jp.fillExternalJoinElements(ds, catalog, schema, resolver); } } catch (Throwable e) { cCache.remove(key); ThrowUnchecked.fire(e); } return jInfo; } } /** * Uses the given database connection to query database metadata. This is * used to bind storables to tables, and properties to columns. Other * checks are performed to ensure that storable type matches well with the * definition in the database. */ private static <S extends Storable> JDBCStorableInfo<S> examine(StorableInfo<S> mainInfo, Connection con, final String searchCatalog, final String searchSchema, SchemaResolver resolver, boolean primaryKeyCheckDisabled) throws SQLException, SupportException { final DatabaseMetaData meta = con.getMetaData(); final String databaseProductName = meta.getDatabaseProductName(); final String userName = meta.getUserName(); String[] tableAliases; if (mainInfo.getAliasCount() > 0) { tableAliases = mainInfo.getAliases(); } else { String name = mainInfo.getStorableType().getSimpleName(); tableAliases = generateAliases(name); } // Try to find matching table from aliases. String catalog = null, schema = null, tableName = null, tableType = null; findName: { // The call to getTables may return several matching tables. This // map defines the "best" table type we'd like to use. The higher // the number the better. Map<String, Integer> fitnessMap = new HashMap<String, Integer>(); fitnessMap.put("LOCAL TEMPORARY", 1); fitnessMap.put("GLOBAL TEMPORARY", 2); fitnessMap.put("VIEW", 3); fitnessMap.put("SYSTEM TABLE", 4); fitnessMap.put("TABLE", 5); fitnessMap.put("ALIAS", 6); fitnessMap.put("SYNONYM", 7); for (int i = 0; i < tableAliases.length; i++) { ResultSet rs = meta.getTables(searchCatalog, searchSchema, tableAliases[i], null); try { int bestFitness = 0; while (rs.next()) { String type = rs.getString("TABLE_TYPE"); Integer fitness = fitnessMap.get(type); if (fitness != null) { String rsSchema = rs.getString("TABLE_SCHEM"); if (searchSchema == null) { if (userName != null && userName.equalsIgnoreCase(rsSchema)) { // Favor entities whose schema name matches // the user name. fitness += 7; } } if (fitness > bestFitness) { bestFitness = fitness; catalog = rs.getString("TABLE_CAT"); schema = rsSchema; tableName = rs.getString("TABLE_NAME"); tableType = type; } } } } finally { rs.close(); } if (tableName != null) { // Found a match, so stop checking aliases. break; } } } if (tableName == null && !mainInfo.isIndependent()) { StringBuilder buf = new StringBuilder(); buf.append("Unable to find matching table name for type \""); buf.append(mainInfo.getStorableType().getName()); buf.append("\" by looking for "); appendToSentence(buf, tableAliases); buf.append(" with catalog " + searchCatalog + " and schema " + searchSchema); throw new MismatchException(buf.toString()); } String qualifiedTableName = tableName; String resolvedTableName = tableName; // Oracle specific stuff... // TODO: Migrate this to OracleSupportStrategy. if (tableName != null && databaseProductName.toUpperCase().contains("ORACLE")) { if ("TABLE".equals(tableType) && searchSchema != null) { // Qualified table name references the schema. Used by SQL statements. qualifiedTableName = searchSchema + '.' + tableName; } else if ("SYNONYM".equals(tableType)) { // Try to get the real schema. This call is Oracle specific, however. String select = "SELECT TABLE_OWNER,TABLE_NAME " + "FROM ALL_SYNONYMS " + "WHERE OWNER=? AND SYNONYM_NAME=?"; PreparedStatement ps = con.prepareStatement(select); ps.setString(1, schema); // in Oracle, schema is the owner ps.setString(2, tableName); try { ResultSet rs = ps.executeQuery(); try { if (rs.next()) { schema = rs.getString("TABLE_OWNER"); resolvedTableName = rs.getString("TABLE_NAME"); } } finally { rs.close(); } } finally { ps.close(); } } } // Gather information on all columns such that metadata only needs to // be retrieved once. Map<String, ColumnInfo> columnMap = new TreeMap<String, ColumnInfo>(String.CASE_INSENSITIVE_ORDER); if (resolvedTableName != null) { ResultSet rs = meta.getColumns(catalog, schema, resolvedTableName, null); rs.setFetchSize(1000); try { while (rs.next()) { ColumnInfo info = new ColumnInfo(rs); columnMap.put(info.columnName, info); } } finally { rs.close(); } } // Make sure that all properties have a corresponding column. Map<String, ? extends StorableProperty<S>> mainProperties = mainInfo.getAllProperties(); Map<String, String> columnToProperty = new HashMap<String, String>(); Map<String, JDBCStorableProperty<S>> jProperties = new LinkedHashMap<String, JDBCStorableProperty<S>>( mainProperties.size()); ArrayList<String> errorMessages = new ArrayList<String>(); for (StorableProperty<S> mainProperty : mainProperties.values()) { if (mainProperty.isDerived() || mainProperty.isJoin() || tableName == null) { jProperties.put(mainProperty.getName(), new JProperty<S>(mainProperty, primaryKeyCheckDisabled)); continue; } String[] columnAliases; if (mainProperty.getAliasCount() > 0) { columnAliases = mainProperty.getAliases(); } else { columnAliases = generateAliases(mainProperty.getName()); } JDBCStorableProperty<S> jProperty = null; boolean addedError = false; findName: for (int i = 0; i < columnAliases.length; i++) { ColumnInfo columnInfo = columnMap.get(columnAliases[i]); if (columnInfo != null) { AccessInfo accessInfo = getAccessInfo(mainProperty, columnInfo.dataType, columnInfo.dataTypeName, columnInfo.columnSize, columnInfo.decimalDigits); if (accessInfo == null) { TypeDesc propertyType = TypeDesc.forClass(mainProperty.getType()); String message = "Property \"" + mainProperty.getName() + "\" has type \"" + propertyType.getFullName() + "\" which is incompatible with database type \"" + columnInfo.dataTypeName + '"'; if (columnInfo.decimalDigits > 0) { message += " (decimal digits = " + columnInfo.decimalDigits + ')'; } errorMessages.add(message); addedError = true; break findName; } if (columnInfo.nullable) { if (!mainProperty.isNullable() && !mainProperty.isIndependent()) { errorMessages.add( "Property \"" + mainProperty.getName() + "\" must have a Nullable annotation"); } } else { if (mainProperty.isNullable() && !mainProperty.isIndependent()) { errorMessages.add("Property \"" + mainProperty.getName() + "\" must not have a Nullable annotation"); } } boolean autoIncrement = mainProperty.isAutomatic(); if (autoIncrement) { // Need to execute a little query to check if column is // auto-increment or not. This information is not available in // the regular database metadata prior to jdk1.6. PreparedStatement ps = con.prepareStatement( "SELECT " + columnInfo.columnName + " FROM " + tableName + " WHERE 1=0"); try { ResultSet rs = ps.executeQuery(); try { autoIncrement = rs.getMetaData().isAutoIncrement(1); } finally { rs.close(); } } finally { ps.close(); } } jProperty = new JProperty<S>(mainProperty, columnInfo, autoIncrement, primaryKeyCheckDisabled, accessInfo.mResultSetGet, accessInfo.mPreparedStatementSet, accessInfo.getAdapter()); break findName; } } if (jProperty != null) { jProperties.put(mainProperty.getName(), jProperty); columnToProperty.put(jProperty.getColumnName(), jProperty.getName()); } else { if (mainProperty.isIndependent()) { jProperties.put(mainProperty.getName(), new JProperty<S>(mainProperty, primaryKeyCheckDisabled)); } else if (!addedError) { StringBuilder buf = new StringBuilder(); buf.append("Unable to find matching database column for property \""); buf.append(mainProperty.getName()); buf.append("\" by looking for "); appendToSentence(buf, columnAliases); errorMessages.add(buf.toString()); } } } if (errorMessages.size() > 0) { throw new MismatchException(mainInfo.getStorableType(), errorMessages); } // Now verify that primary or alternate keys match. if (resolvedTableName != null) checkPrimaryKey: { ResultSet rs; try { rs = meta.getPrimaryKeys(catalog, schema, resolvedTableName); } catch (SQLException e) { getLog().info("Unable to get primary keys for table \"" + resolvedTableName + "\" with catalog " + catalog + " and schema " + schema + ": " + e); break checkPrimaryKey; } List<String> pkProps = new ArrayList<String>(); try { while (rs.next()) { String columnName = rs.getString("COLUMN_NAME"); String propertyName = columnToProperty.get(columnName); if (propertyName == null) { errorMessages .add("Column \"" + columnName + "\" must be part of primary or alternate key"); continue; } pkProps.add(propertyName); } } finally { rs.close(); } if (errorMessages.size() > 0) { // Skip any extra checks. break checkPrimaryKey; } if (pkProps.size() == 0) { // If no primary keys are reported, don't even bother checking. // There's no consistent way to get primary keys, and entities // like views and synonyms don't usually report primary keys. // A primary key might even be logically defined as a unique // constraint. break checkPrimaryKey; } if (matchesKey(pkProps, mainInfo.getPrimaryKey())) { // Good. Primary key in database is same as in Storable. break checkPrimaryKey; } // Check if Storable has an alternate key which matches the // database's primary key. boolean foundAnyAltKey = false; for (StorableKey<S> altKey : mainInfo.getAlternateKeys()) { if (matchesKey(pkProps, altKey)) { // Okay. Primary key in database matches a Storable // alternate key. foundAnyAltKey = true; // Also check that declared primary key is a strict subset // of the alternate key. If not, keep checking alt keys. if (matchesSubKey(pkProps, mainInfo.getPrimaryKey())) { break checkPrimaryKey; } } } if (foundAnyAltKey) { errorMessages.add("Actual primary key matches a declared alternate key, " + "but declared primary key must be a strict subset. " + mainInfo.getPrimaryKey().getProperties() + " is not a subset of " + pkProps); } else { errorMessages.add("Actual primary key does not match any " + "declared primary or alternate key: " + pkProps); } } if (errorMessages.size() > 0) { if (primaryKeyCheckDisabled) { for (String errorMessage : errorMessages) { getLog().warn("Suppressed error: " + errorMessage); } errorMessages.clear(); } else { throw new MismatchException(mainInfo.getStorableType(), errorMessages); } } // IndexInfo is empty, as querying for it tends to cause a table analyze to run. IndexInfo[] indexInfo = new IndexInfo[0]; if (needsQuotes(tableName)) { String quote = meta.getIdentifierQuoteString(); if (quote != null && !quote.equals(" ")) { tableName = quote + tableName + quote; qualifiedTableName = quote + qualifiedTableName + quote; } } return new JInfo<S>(mainInfo, catalog, schema, tableName, qualifiedTableName, indexInfo, jProperties); } private static boolean needsQuotes(String str) { if (str == null) { return false; } if (str.length() == 0) { return true; } char c = str.charAt(0); if (!(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c == '_')) { return true; } for (int i = str.length(); --i >= 0;) { c = str.charAt(i); if (!(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c == '_' || c >= '0' && c <= '9')) { return true; } } return false; } private static boolean matchesKey(Collection<String> keyProps, StorableKey<?> declaredKey) { if (keyProps.size() != declaredKey.getProperties().size()) { return false; } return matchesSubKey(keyProps, declaredKey); } /** * @return true if declared key properties are all found in the given keyProps set */ private static boolean matchesSubKey(Collection<String> keyProps, StorableKey<?> declaredKey) { for (OrderedProperty<?> declaredKeyProp : declaredKey.getProperties()) { ChainedProperty<?> chained = declaredKeyProp.getChainedProperty(); if (chained.getChainCount() > 0) { return false; } if (!keyProps.contains(chained.getLastProperty().getName())) { return false; } } return true; } private static Log getLog() { return LogFactory.getLog(JDBCStorableIntrospector.class); } /** * Figures out how to best access the given property, or returns null if * not supported. An adapter may be applied. * * @return null if not supported */ private static AccessInfo getAccessInfo(StorableProperty property, int dataType, String dataTypeName, int columnSize, int decimalDigits) { AccessInfo info = getAccessInfo(property.getType(), dataType, dataTypeName, columnSize, decimalDigits); if (info != null) { return info; } // Dynamically typed data sources (e.g. SQLite3) always report // dataType as java.sql.Types.VARCHAR. Infer the dataType from the // dataTypeName and try again. if (dataType == java.sql.Types.VARCHAR) { Integer dataTypeMapping = typeNameToDataTypeMapping.get(dataTypeName.toUpperCase()); if (dataTypeMapping != null) { info = getAccessInfo(property.getType(), dataTypeMapping, dataTypeName, columnSize, decimalDigits); if (info != null) { return info; } } } // See if an appropriate adapter exists. StorablePropertyAdapter adapter = property.getAdapter(); if (adapter != null) { Method[] toMethods = adapter.findAdaptMethodsTo(property.getType()); for (Method toMethod : toMethods) { Class fromType = toMethod.getParameterTypes()[0]; // Verify that reverse adapt method exists as well... if (adapter.findAdaptMethod(property.getType(), fromType) != null) { // ...and try to get access info for fromType. info = getAccessInfo(fromType, dataType, dataTypeName, columnSize, decimalDigits); if (info != null) { info.setAdapter(adapter); return info; } } } } return null; } /** * Figures out how to best access the given property, or returns null if * not supported. An adapter is not applied. * * @return null if not supported */ private static AccessInfo getAccessInfo(Class desiredClass, int dataType, String dataTypeName, int columnSize, int decimalDigits) { if (!desiredClass.isPrimitive()) { TypeDesc desiredType = TypeDesc.forClass(desiredClass); if (desiredType.toPrimitiveType() != null) { desiredType = desiredType.toPrimitiveType(); desiredClass = desiredType.toClass(); } } if (desiredClass == Object.class) { return new AccessInfo("Object", Object.class); } Class actualClass; String suffix; switch (dataType) { default: return null; case BIT: case BOOLEAN: if (desiredClass == boolean.class) { actualClass = boolean.class; suffix = "Boolean"; } else if (desiredClass == String.class) { actualClass = String.class; suffix = "String"; } else { return null; } break; case TINYINT: if (desiredClass == byte.class) { actualClass = byte.class; suffix = "Byte"; } else if (desiredClass == short.class) { actualClass = short.class; suffix = "Short"; } else if (desiredClass == String.class) { actualClass = String.class; suffix = "String"; } else { return null; } break; case SMALLINT: if (desiredClass == short.class) { actualClass = short.class; suffix = "Short"; } else if (desiredClass == int.class) { actualClass = int.class; suffix = "Int"; } else if (desiredClass == String.class) { actualClass = String.class; suffix = "String"; } else { return null; } break; case INTEGER: if (desiredClass == int.class) { actualClass = int.class; suffix = "Int"; } else if (desiredClass == long.class) { actualClass = long.class; suffix = "Long"; } else if (desiredClass == String.class) { actualClass = String.class; suffix = "String"; } else { return null; } break; case BIGINT: if (desiredClass == long.class) { actualClass = long.class; suffix = "Long"; } else if (desiredClass == String.class) { actualClass = String.class; suffix = "String"; } else { return null; } break; case REAL: case FLOAT: case DOUBLE: if (desiredClass == float.class) { actualClass = float.class; suffix = "Float"; } else if (desiredClass == double.class) { actualClass = double.class; suffix = "Double"; } else if (desiredClass == String.class) { actualClass = String.class; suffix = "String"; } else { return null; } break; case NUMERIC: case DECIMAL: if (desiredClass == int.class) { if (decimalDigits == 0) { actualClass = int.class; suffix = "Int"; } else { return null; } } else if (desiredClass == long.class) { if (decimalDigits == 0) { actualClass = long.class; suffix = "Long"; } else { return null; } } else if (desiredClass == double.class) { actualClass = double.class; suffix = "Double"; } else if (desiredClass == BigDecimal.class) { actualClass = BigDecimal.class; suffix = "BigDecimal"; } else if (desiredClass == short.class) { if (decimalDigits == 0) { actualClass = short.class; suffix = "Short"; } else { return null; } } else if (desiredClass == byte.class) { if (decimalDigits == 0) { actualClass = byte.class; suffix = "Byte"; } else { return null; } } else if (desiredClass == String.class) { actualClass = String.class; suffix = "String"; } else { return null; } break; case CHAR: case VARCHAR: case LONGVARCHAR: if (desiredClass == String.class) { actualClass = String.class; suffix = "String"; } else if (desiredClass == char.class && columnSize == 1) { actualClass = String.class; suffix = "String"; } else { return null; } break; case DATE: // Treat Date as a Timestamp since some databases make no // distinction. The DateTimeAdapter can be used to provide // more control over the desired precision. if (desiredClass == java.sql.Date.class) { actualClass = java.sql.Timestamp.class; suffix = "Timestamp"; } else { return null; } break; case TIME: if (desiredClass == java.sql.Time.class) { actualClass = java.sql.Time.class; suffix = "Time"; } else { return null; } break; case TIMESTAMP: if (desiredClass == java.sql.Timestamp.class) { actualClass = java.sql.Timestamp.class; suffix = "Timestamp"; } else { return null; } break; case BINARY: case VARBINARY: case LONGVARBINARY: if (desiredClass == byte[].class) { actualClass = byte[].class; suffix = "Bytes"; } else { return null; } break; case BLOB: if (desiredClass == com.amazon.carbonado.lob.Blob.class) { actualClass = java.sql.Blob.class; suffix = "Blob"; } else { return null; } break; case CLOB: if (desiredClass == com.amazon.carbonado.lob.Clob.class) { actualClass = java.sql.Clob.class; suffix = "Clob"; } else { return null; } break; } return new AccessInfo(suffix, actualClass); } /** * Appends words to a sentence as an "or" list. */ private static void appendToSentence(StringBuilder buf, String[] names) { for (int i = 0; i < names.length; i++) { if (i > 0) { if (i + 1 >= names.length) { buf.append(" or "); } else { buf.append(", "); } } buf.append('"'); buf.append(names[i]); buf.append('"'); } } /** * Generates aliases for the given name, converting camel case form into * various underscore forms. */ static String[] generateAliases(String base) { int length = base.length(); if (length <= 1) { return new String[] { base.toUpperCase(), base.toLowerCase() }; } ArrayList<String> aliases = new ArrayList<String>(4); StringBuilder buf = new StringBuilder(); int i; for (i = 0; i < length;) { char c = base.charAt(i++); if (c == '_' || !Character.isJavaIdentifierPart(c)) { // Keep scanning for first letter. buf.append(c); } else { buf.append(Character.toUpperCase(c)); break; } } boolean canSeparate = false; boolean appendedIdentifierPart = false; for (; i < length; i++) { char c = base.charAt(i); if (c == '_' || !Character.isJavaIdentifierPart(c)) { canSeparate = false; appendedIdentifierPart = false; } else if (Character.isLowerCase(c)) { canSeparate = true; appendedIdentifierPart = true; } else { if (appendedIdentifierPart && i + 1 < length && Character.isLowerCase(base.charAt(i + 1))) { canSeparate = true; } if (canSeparate) { buf.append('_'); } canSeparate = false; appendedIdentifierPart = true; } buf.append(c); } String derived = buf.toString(); addToSet(aliases, derived.toUpperCase()); addToSet(aliases, derived.toLowerCase()); addToSet(aliases, derived); addToSet(aliases, base.toUpperCase()); addToSet(aliases, base.toLowerCase()); addToSet(aliases, base); return aliases.toArray(new String[aliases.size()]); } private static void addToSet(ArrayList<String> list, String value) { if (!list.contains(value)) { list.add(value); } } static String intern(String str) { return str == null ? null : str.intern(); } private static final Map<String, Integer> typeNameToDataTypeMapping; static { // Mapping taken from the following: // http://docs.oracle.com/javase/6/docs/technotes/guides/jdbc/getstart/mapping.html Map<String, Integer> aMap = new HashMap<String, Integer>(); aMap.put("CHAR", java.sql.Types.CHAR); aMap.put("VARCHAR", java.sql.Types.VARCHAR); aMap.put("LONGVARCHAR", java.sql.Types.LONGVARCHAR); aMap.put("NUMERIC", java.sql.Types.NUMERIC); aMap.put("DECIMAL", java.sql.Types.DECIMAL); aMap.put("BIT", java.sql.Types.BIT); aMap.put("TINYINT", java.sql.Types.TINYINT); aMap.put("SMALLINT", java.sql.Types.SMALLINT); aMap.put("INTEGER", java.sql.Types.INTEGER); aMap.put("BIGINT", java.sql.Types.BIGINT); aMap.put("REAL", java.sql.Types.REAL); aMap.put("FLOAT", java.sql.Types.FLOAT); aMap.put("DOUBLE", java.sql.Types.DOUBLE); aMap.put("BINARY", java.sql.Types.BINARY); aMap.put("VARBINARY", java.sql.Types.VARBINARY); aMap.put("LONGVARBINARY", java.sql.Types.LONGVARBINARY); aMap.put("DATE", java.sql.Types.DATE); aMap.put("TIME", java.sql.Types.TIME); aMap.put("TIMESTAMP", java.sql.Types.TIMESTAMP); aMap.put("CLOB", java.sql.Types.CLOB); aMap.put("BLOB", java.sql.Types.BLOB); aMap.put("ARRAY", java.sql.Types.ARRAY); aMap.put("DISTINCT", java.sql.Types.DISTINCT); aMap.put("STRUCT", java.sql.Types.STRUCT); aMap.put("REF", java.sql.Types.REF); typeNameToDataTypeMapping = Collections.unmodifiableMap(aMap); } private static class ColumnInfo { final String columnName; final int dataType; final String dataTypeName; final int columnSize; final int decimalDigits; final boolean nullable; final int charOctetLength; final int ordinalPosition; ColumnInfo(ResultSet rs) throws SQLException { columnName = intern(rs.getString("COLUMN_NAME")); dataTypeName = intern(rs.getString("TYPE_NAME")); columnSize = rs.getInt("COLUMN_SIZE"); decimalDigits = rs.getInt("DECIMAL_DIGITS"); nullable = rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable; charOctetLength = rs.getInt("CHAR_OCTET_LENGTH"); ordinalPosition = rs.getInt("ORDINAL_POSITION"); int dt = rs.getInt("DATA_TYPE"); if (dt == OTHER) { if ("BLOB".equalsIgnoreCase(dataTypeName)) { dt = BLOB; } else if ("CLOB".equalsIgnoreCase(dataTypeName)) { dt = CLOB; } else if ("FLOAT".equalsIgnoreCase(dataTypeName)) { dt = FLOAT; } else if ("TIMESTAMP".equalsIgnoreCase(dataTypeName)) { dt = TIMESTAMP; } else if (dataTypeName.toUpperCase().contains("TIMESTAMP")) { dt = TIMESTAMP; } else if (dataTypeName.equalsIgnoreCase("INT UNSIGNED")) { dt = BIGINT; } } else if (dt == LONGVARBINARY && "BLOB".equalsIgnoreCase(dataTypeName)) { // Workaround MySQL bug. dt = BLOB; } else if (dt == LONGVARCHAR && "CLOB".equalsIgnoreCase(dataTypeName)) { // Workaround MySQL bug. dt = CLOB; } dataType = dt; } } private static class AccessInfo { // ResultSet get method, never null. final Method mResultSetGet; // PreparedStatement set method, never null. final Method mPreparedStatementSet; // Is null if no adapter needed. private StorablePropertyAdapter mAdapter; AccessInfo(String suffix, Class actualClass) { try { mResultSetGet = ResultSet.class.getMethod("get" + suffix, int.class); mPreparedStatementSet = PreparedStatement.class.getMethod("set" + suffix, int.class, actualClass); } catch (NoSuchMethodException e) { throw new UndeclaredThrowableException(e); } } StorablePropertyAdapter getAdapter() { return mAdapter; } void setAdapter(StorablePropertyAdapter adapter) { mAdapter = adapter; } } /** * Implementation of JDBCStorableInfo. The 'J' prefix is just a shorthand * to disambiguate the class name. */ private static class JInfo<S extends Storable> implements JDBCStorableInfo<S> { private final StorableInfo<S> mMainInfo; private final String mCatalogName; private final String mSchemaName; private final String mTableName; private final String mQualifiedTableName; private final IndexInfo[] mIndexInfo; private final Map<String, JDBCStorableProperty<S>> mAllProperties; private transient Map<String, JDBCStorableProperty<S>> mPrimaryKeyProperties; private transient Map<String, JDBCStorableProperty<S>> mDataProperties; private transient Map<String, JDBCStorableProperty<S>> mIdentityProperties; private transient JDBCStorableProperty<S> mVersionProperty; JInfo(StorableInfo<S> mainInfo, String catalogName, String schemaName, String tableName, String qualifiedTableName, IndexInfo[] indexInfo, Map<String, JDBCStorableProperty<S>> allProperties) { mMainInfo = mainInfo; mCatalogName = intern(catalogName); mSchemaName = intern(schemaName); mTableName = intern(tableName); mQualifiedTableName = intern(qualifiedTableName); mIndexInfo = indexInfo; mAllProperties = Collections.unmodifiableMap(allProperties); } public String getName() { return mMainInfo.getName(); } public Class<S> getStorableType() { return mMainInfo.getStorableType(); } public StorableKey<S> getPrimaryKey() { return mMainInfo.getPrimaryKey(); } public int getAlternateKeyCount() { return mMainInfo.getAlternateKeyCount(); } public StorableKey<S> getAlternateKey(int index) { return mMainInfo.getAlternateKey(index); } public StorableKey<S>[] getAlternateKeys() { return mMainInfo.getAlternateKeys(); } public StorableKey<S> getPartitionKey() { return mMainInfo.getPartitionKey(); } public int getAliasCount() { return mMainInfo.getAliasCount(); } public String getAlias(int index) { return mMainInfo.getAlias(index); } public String[] getAliases() { return mMainInfo.getAliases(); } public int getIndexCount() { return mMainInfo.getIndexCount(); } public StorableIndex<S> getIndex(int index) { return mMainInfo.getIndex(index); } public StorableIndex<S>[] getIndexes() { return mMainInfo.getIndexes(); } public boolean isIndependent() { return mMainInfo.isIndependent(); } public boolean isAuthoritative() { return mMainInfo.isAuthoritative(); } public boolean isSupported() { return mTableName != null; } public String getCatalogName() { return mCatalogName; } public String getSchemaName() { return mSchemaName; } public String getTableName() { return mTableName; } public String getQualifiedTableName() { return mQualifiedTableName; } public IndexInfo[] getIndexInfo() { return mIndexInfo.clone(); } public Map<String, JDBCStorableProperty<S>> getAllProperties() { return mAllProperties; } public Map<String, JDBCStorableProperty<S>> getPrimaryKeyProperties() { if (mPrimaryKeyProperties == null) { Map<String, JDBCStorableProperty<S>> pkProps = new LinkedHashMap<String, JDBCStorableProperty<S>>( mAllProperties.size()); for (Map.Entry<String, JDBCStorableProperty<S>> entry : mAllProperties.entrySet()) { JDBCStorableProperty<S> property = entry.getValue(); if (property.isPrimaryKeyMember()) { pkProps.put(entry.getKey(), property); } } mPrimaryKeyProperties = Collections.unmodifiableMap(pkProps); } return mPrimaryKeyProperties; } public Map<String, JDBCStorableProperty<S>> getDataProperties() { if (mDataProperties == null) { Map<String, JDBCStorableProperty<S>> dataProps = new LinkedHashMap<String, JDBCStorableProperty<S>>( mAllProperties.size()); for (Map.Entry<String, JDBCStorableProperty<S>> entry : mAllProperties.entrySet()) { JDBCStorableProperty<S> property = entry.getValue(); if (!property.isPrimaryKeyMember() && !property.isJoin()) { dataProps.put(entry.getKey(), property); } } mDataProperties = Collections.unmodifiableMap(dataProps); } return mDataProperties; } public Map<String, JDBCStorableProperty<S>> getIdentityProperties() { if (mIdentityProperties == null) { Map<String, JDBCStorableProperty<S>> idProps = new LinkedHashMap<String, JDBCStorableProperty<S>>( 1); for (Map.Entry<String, JDBCStorableProperty<S>> entry : getPrimaryKeyProperties().entrySet()) { JDBCStorableProperty<S> property = entry.getValue(); if (property.isAutoIncrement()) { idProps.put(entry.getKey(), property); } } mIdentityProperties = Collections.unmodifiableMap(idProps); } return mIdentityProperties; } public JDBCStorableProperty<S> getVersionProperty() { if (mVersionProperty == null) { for (JDBCStorableProperty<S> property : mAllProperties.values()) { if (property.isVersion()) { mVersionProperty = property; break; } } } return mVersionProperty; } } /** * Implementation of JDBCStorableProperty. The 'J' prefix is just a * shorthand to disambiguate the class name. */ private static class JProperty<S extends Storable> implements JDBCStorableProperty<S> { private static final long serialVersionUID = -7333912817502875485L; private final StorableProperty<S> mMainProperty; private final String mColumnName; private final Integer mDataType; private final String mDataTypeName; private final boolean mColumnNullable; private final Method mResultSetGet; private final Method mPreparedStatementSet; private final StorablePropertyAdapter mAdapter; private final Integer mColumnSize; private final Integer mDecimalDigits; private final Integer mCharOctetLength; private final Integer mOrdinalPosition; private final boolean mAutoIncrement; private final boolean mPrimaryKeyCheckDisabled; private JDBCStorableProperty<S>[] mInternal; private JDBCStorableProperty<?>[] mExternal; /** * Join properties need to be filled in later. */ JProperty(StorableProperty<S> mainProperty, ColumnInfo columnInfo, boolean autoIncrement, boolean primaryKeyCheckDisabled, Method resultSetGet, Method preparedStatementSet, StorablePropertyAdapter adapter) { mMainProperty = mainProperty; mColumnName = columnInfo.columnName; mDataType = columnInfo.dataType; mDataTypeName = columnInfo.dataTypeName; mColumnNullable = columnInfo.nullable; mResultSetGet = resultSetGet; mPreparedStatementSet = preparedStatementSet; mAdapter = adapter; mColumnSize = columnInfo.columnSize; mDecimalDigits = columnInfo.decimalDigits; mCharOctetLength = columnInfo.charOctetLength; mOrdinalPosition = columnInfo.ordinalPosition; mAutoIncrement = autoIncrement; mPrimaryKeyCheckDisabled = primaryKeyCheckDisabled; } JProperty(StorableProperty<S> mainProperty, boolean primaryKeyCheckDisabled) { mMainProperty = mainProperty; mColumnName = null; mDataType = null; mDataTypeName = null; mColumnNullable = false; mResultSetGet = null; mPreparedStatementSet = null; mAdapter = null; mColumnSize = null; mDecimalDigits = null; mCharOctetLength = null; mOrdinalPosition = null; mAutoIncrement = false; mPrimaryKeyCheckDisabled = primaryKeyCheckDisabled; } public String getName() { return mMainProperty.getName(); } public String getBeanName() { return mMainProperty.getBeanName(); } public Class<?> getType() { return mMainProperty.getType(); } public Class<?>[] getCovariantTypes() { return mMainProperty.getCovariantTypes(); } public int getNumber() { return mMainProperty.getNumber(); } public Class<S> getEnclosingType() { return mMainProperty.getEnclosingType(); } public Method getReadMethod() { return mMainProperty.getReadMethod(); } public String getReadMethodName() { return mMainProperty.getReadMethodName(); } public Method getWriteMethod() { return mMainProperty.getWriteMethod(); } public String getWriteMethodName() { return mMainProperty.getWriteMethodName(); } public boolean isNullable() { return mMainProperty.isNullable(); } public boolean isPrimaryKeyMember() { return mMainProperty.isPrimaryKeyMember(); } public boolean isAlternateKeyMember() { return mMainProperty.isAlternateKeyMember(); } public boolean isPartitionKeyMember() { return mMainProperty.isPartitionKeyMember(); } public int getAliasCount() { return mMainProperty.getAliasCount(); } public String getAlias(int index) { return mMainProperty.getAlias(index); } public String[] getAliases() { return mMainProperty.getAliases(); } public boolean isJoin() { return mMainProperty.isJoin(); } public boolean isOneToOneJoin() { return mMainProperty.isOneToOneJoin(); } public Class<? extends Storable> getJoinedType() { return mMainProperty.getJoinedType(); } public int getJoinElementCount() { return mMainProperty.getJoinElementCount(); } public boolean isQuery() { return mMainProperty.isQuery(); } public int getConstraintCount() { return mMainProperty.getConstraintCount(); } public StorablePropertyConstraint getConstraint(int index) { return mMainProperty.getConstraint(index); } public StorablePropertyConstraint[] getConstraints() { return mMainProperty.getConstraints(); } public StorablePropertyAdapter getAdapter() { return mMainProperty.getAdapter(); } public String getSequenceName() { return mMainProperty.getSequenceName(); } public boolean isAutomatic() { return mMainProperty.isAutomatic(); } public boolean isVersion() { return mMainProperty.isVersion(); } public boolean isIndependent() { return mMainProperty.isIndependent(); } public boolean isDerived() { return mMainProperty.isDerived(); } public ChainedProperty<S>[] getDerivedFromProperties() { return mMainProperty.getDerivedFromProperties(); } public ChainedProperty<?>[] getDerivedToProperties() { return mMainProperty.getDerivedToProperties(); } public boolean shouldCopyDerived() { return mMainProperty.shouldCopyDerived(); } public boolean isSupported() { if (isJoin()) { // TODO: Check if joined type is supported return true; } else { return mColumnName != null; } } public boolean isSelectable() { return mColumnName != null && !isJoin() && !isDerived(); } public boolean isAutoIncrement() { return mAutoIncrement; } public String getColumnName() { return mColumnName; } public Integer getDataType() { return mDataType; } public String getDataTypeName() { return mDataTypeName; } public boolean isColumnNullable() { return mColumnNullable; } public Method getResultSetGetMethod() { return mResultSetGet; } public Method getPreparedStatementSetMethod() { return mPreparedStatementSet; } public StorablePropertyAdapter getAppliedAdapter() { return mAdapter; } public Integer getColumnSize() { return mColumnSize; } public Integer getDecimalDigits() { return mDecimalDigits; } public Integer getCharOctetLength() { return mCharOctetLength; } public Integer getOrdinalPosition() { return mOrdinalPosition; } public JDBCStorableProperty<S> getInternalJoinElement(int index) { if (mInternal == null) { throw new IndexOutOfBoundsException(); } return mInternal[index]; } @SuppressWarnings("unchecked") public JDBCStorableProperty<S>[] getInternalJoinElements() { if (mInternal == null) { return new JDBCStorableProperty[0]; } return mInternal.clone(); } public JDBCStorableProperty<?> getExternalJoinElement(int index) { if (mExternal == null) { throw new IndexOutOfBoundsException(); } return mExternal[index]; } public JDBCStorableProperty<?>[] getExternalJoinElements() { if (mExternal == null) { return new JDBCStorableProperty[0]; } return mExternal.clone(); } @Override public String toString() { return mMainProperty.toString(); } public void appendTo(Appendable app) throws IOException { mMainProperty.appendTo(app); } @SuppressWarnings("unchecked") void fillInternalJoinElements(DataSource ds, String catalog, String schema, SchemaResolver resolver) throws SQLException, SupportException { StorableProperty<S>[] mainInternal = mMainProperty.getInternalJoinElements(); if (mainInternal.length == 0) { mInternal = null; return; } JDBCStorableInfo<S> info = examine(getEnclosingType(), ds, catalog, schema, resolver, mPrimaryKeyCheckDisabled); JDBCStorableProperty<S>[] internal = new JDBCStorableProperty[mainInternal.length]; for (int i = mainInternal.length; --i >= 0;) { internal[i] = info.getAllProperties().get(mainInternal[i].getName()); } mInternal = internal; } void fillExternalJoinElements(DataSource ds, String catalog, String schema, SchemaResolver resolver) throws SQLException, SupportException { StorableProperty<?>[] mainExternal = mMainProperty.getExternalJoinElements(); if (mainExternal.length == 0) { mExternal = null; return; } JDBCStorableInfo<?> info = examine(getJoinedType(), ds, catalog, schema, resolver, mPrimaryKeyCheckDisabled); JDBCStorableProperty<?>[] external = new JDBCStorableProperty[mainExternal.length]; for (int i = mainExternal.length; --i >= 0;) { external[i] = info.getAllProperties().get(mainExternal[i].getName()); } mExternal = external; } } }