com.amazon.carbonado.repo.jdbc.JDBCStorableIntrospector.java Source code

Java tutorial

Introduction

Here is the source code for com.amazon.carbonado.repo.jdbc.JDBCStorableIntrospector.java

Source

/*
 * 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;
        }
    }
}