cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aadamc...@apache.org
Subject [12/17] cayenne git commit: CAY-2026 Java 7
Date Sat, 12 Sep 2015 10:02:40 GMT
http://git-wip-us.apache.org/repos/asf/cayenne/blob/26d8434d/cayenne-server/src/main/java/org/apache/cayenne/access/DbLoader.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DbLoader.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DbLoader.java
index f2c473a..7b1bce0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DbLoader.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DbLoader.java
@@ -18,10 +18,25 @@
  ****************************************************************/
 package org.apache.cayenne.access;
 
-import java.sql.*;
-import java.util.*;
-
-import org.apache.cayenne.access.loader.*;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.cayenne.access.loader.DbAttributesPerSchemaLoader;
+import org.apache.cayenne.access.loader.DbLoaderConfiguration;
+import org.apache.cayenne.access.loader.DbTableLoader;
+import org.apache.cayenne.access.loader.DefaultDbLoaderDelegate;
+import org.apache.cayenne.access.loader.ManyToManyCandidateEntity;
 import org.apache.cayenne.access.loader.filters.CatalogFilter;
 import org.apache.cayenne.access.loader.filters.FiltersConfig;
 import org.apache.cayenne.access.loader.filters.PatternFilter;
@@ -54,739 +69,744 @@ import org.apache.commons.logging.LogFactory;
  */
 public class DbLoader {
 
-    private static final Log LOGGER = LogFactory.getLog(DbLoader.class);
-
-    public static final String WILDCARD = "%";
-    public static final String WILDCARD_PATTERN = ".*";
-
-    private final Connection connection;
-    private final DbAdapter adapter;
-    private final DbLoaderDelegate delegate;
-
-    private boolean creatingMeaningfulPK;
-
-    private DatabaseMetaData metaData;
-
-
-    /**
-     * Strategy for choosing names for entities, attributes and relationships
-     */
-    private ObjectNameGenerator nameGenerator;
-
-    /**
-     * Creates new DbLoader.
-     */
-    public DbLoader(Connection connection, DbAdapter adapter, DbLoaderDelegate delegate) {
-        this(connection, adapter, delegate, new LegacyNameGenerator());
-    }
-
-    /**
-     * Creates new DbLoader with specified naming strategy.
-     *
-     * @since 3.0
-     */
-    public DbLoader(Connection connection, DbAdapter adapter, DbLoaderDelegate delegate, ObjectNameGenerator strategy) {
-        this.adapter = adapter;
-        this.connection = connection;
-        this.delegate = delegate == null ? new DefaultDbLoaderDelegate() : delegate;
-
-        setNameGenerator(strategy);
-    }
-
-    /**
-     * Returns DatabaseMetaData object associated with this DbLoader.
-     */
-    private DatabaseMetaData getMetaData() throws SQLException {
-        if (metaData == null) {
-            metaData = connection.getMetaData();
-        }
-        return metaData;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public void setCreatingMeaningfulPK(boolean creatingMeaningfulPK) {
-        this.creatingMeaningfulPK = creatingMeaningfulPK;
-    }
-
-    /**
-     * Returns true if the generator should map all primary key columns as
-     * ObjAttributes.
-     *
-     * @since 3.0
-     */
-    public boolean isCreatingMeaningfulPK() {
-        return creatingMeaningfulPK;
-    }
-
-    /**
-     * Returns database connection used by this DbLoader.
-     *
-     * @since 3.0
-     */
-    public Connection getConnection() {
-        return connection;
-    }
-
-    /**
-     * Returns DbAdapter associated with this DbLoader.
-     *
-     * @since 1.1
-     */
-    public DbAdapter getAdapter() {
-        return adapter;
-    }
-
-    /**
-     * Retrieves catalogs for the database associated with this DbLoader.
-     *
-     * @return List with the catalog names, empty Array if none found.
-     */
-    public List<String> getCatalogs() throws SQLException {
-        return getStrings(getMetaData().getCatalogs());
-    }
-
-    /**
-     * Retrieves the schemas for the database.
-     *
-     * @return List with the schema names, empty Array if none found.
-     */
-    public List<String> getSchemas() throws SQLException {
-        return getStrings(getMetaData().getSchemas());
-    }
-
-    private static List<String> getStrings(ResultSet rs) throws SQLException {
-        List<String> strings = new ArrayList<String>();
-        try {
-            while (rs.next()) {
-                strings.add(rs.getString(1));
-            }
-        } finally {
-            rs.close();
-        }
-        return strings;
-    }
-
-    /**
-     * Returns all the table types for the given database. Types may be such as
-     * Typical types are "TABLE",
-     * "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY",
-     * "LOCAL TEMPORARY", "ALIAS", "SYNONYM"., etc.
-     *
-     * @return List of Strings, empty array if nothing found.
-     */
-    public List<String> getTableTypes() throws SQLException {
-        List<String> types = new ArrayList<String>();
-        ResultSet rs = getMetaData().getTableTypes();
-
-        try {
-            while (rs.next()) {
-                types.add(rs.getString("TABLE_TYPE").trim());
-            }
-        } finally {
-            rs.close();
-        }
-        return types;
-    }
-
-    /**
-     * Creates an ObjEntity for each DbEntity in the map.
-     */
-    public Collection<ObjEntity> loadObjEntities(DataMap map, DbLoaderConfiguration config, Collection<DbEntity> entities) {
-        Collection<ObjEntity> loadedEntities = DbLoader.loadObjEntities(map, config, entities, nameGenerator);
-
-        createEntityMerger(map).synchronizeWithDbEntities(loadedEntities);
-
-        return loadedEntities;
-    }
-
-    public static Collection<ObjEntity> loadObjEntities(DataMap map, DbLoaderConfiguration config, Collection<DbEntity> entities, ObjectNameGenerator nameGenerator) {
-        if (entities.isEmpty()) {
-            return Collections.emptyList();
-        }
-
-        Collection<ObjEntity> loadedEntities = new ArrayList<ObjEntity>(entities.size());
-
-        // doLoad empty ObjEntities for all the tables
-        for (DbEntity dbEntity : entities) {
-
-            // check if there are existing entities
-
-            // TODO: performance. This is an O(n^2) search and it shows on
-            // YourKit profiles. Pre-cache mapped entities perhaps (?)
-            Collection<ObjEntity> existing = map.getMappedEntities(dbEntity);
-            if (!existing.isEmpty()) {
-                loadedEntities.addAll(existing);
-                continue;
-            }
-
-            String objEntityName = DefaultUniqueNameGenerator.generate(NameCheckers.objEntity, map,
-                    nameGenerator.createObjEntityName(dbEntity));
-
-            ObjEntity objEntity = new ObjEntity(objEntityName);
-            objEntity.setDbEntity(dbEntity);
-            objEntity.setClassName(config.getGenericClassName() != null ? config.getGenericClassName()
-                    : map.getNameWithDefaultPackage(objEntity.getName()));
-
-            map.addObjEntity(objEntity);
-            loadedEntities.add(objEntity);
-        }
-
-        return loadedEntities;
-    }
-
-    /**
-     * @since 4.0
-     */
-    protected EntityMergeSupport createEntityMerger(DataMap map) {
-        return new EntityMergeSupport(map, nameGenerator, !creatingMeaningfulPK);
-    }
-
-    protected void loadDbRelationships(DbLoaderConfiguration config, String catalog, String schema, List<DbEntity> tables) throws SQLException {
-        if (config.isSkipRelationshipsLoading()) {
-            return;
-        }
-
-        // Get all the foreign keys referencing this table
-        Map<String, DbEntity> tablesMap = new HashMap<String, DbEntity>();
-        for (DbEntity table : tables) {
-            tablesMap.put(table.getName(), table);
-        }
-
-        Map<String, Set<ExportedKey>> keys = loadExportedKeys(config, catalog, schema, tablesMap);
-        for (Map.Entry<String, Set<ExportedKey>> entry : keys.entrySet()) {
-            if (LOGGER.isDebugEnabled()) {
-                LOGGER.debug("Process keys for: " + entry.getKey());
-            }
-
-            Set<ExportedKey> exportedKeys = entry.getValue();
-            ExportedKey key = exportedKeys.iterator().next();
-            if (key == null) {
-                throw new IllegalStateException();
-            }
-
-            DbEntity pkEntity = tablesMap.get(key.getPKTableName());
-            if (pkEntity == null) {
-                skipRelationLog(key, key.getPKTableName());
-                continue;
-            }
-
-            DbEntity fkEntity = tablesMap.get(key.getFKTableName());
-            if (fkEntity == null) {
-                skipRelationLog(key, key.getFKTableName());
-                continue;
-            }
-
-            if (!new EqualsBuilder()
-                    .append(pkEntity.getCatalog(), key.pkCatalog)
-                    .append(pkEntity.getSchema(), key.pkSchema)
-                    .append(fkEntity.getCatalog(), key.fkCatalog)
-                    .append(fkEntity.getSchema(), key.fkSchema)
-                    .isEquals()) {
-
-                LOGGER.info("Skip relation: '" + key + "' because it related to objects from other catalog/schema");
-                LOGGER.info("     relation primary key: '" + key.pkCatalog + "." + key.pkSchema + "'");
-                LOGGER.info("       primary key entity: '" + pkEntity.getCatalog() + "." + pkEntity.getSchema() + "'");
-                LOGGER.info("     relation foreign key: '" + key.fkCatalog + "." + key.fkSchema + "'");
-                LOGGER.info("       foreign key entity: '" + fkEntity.getCatalog() + "." + fkEntity.getSchema() + "'");
-                continue;
-            }
-
-            // forwardRelationship is a reference from table with primary key
-            DbRelationship forwardRelationship = new DbRelationship(generateName(pkEntity, key, true));
-            forwardRelationship.setSourceEntity(pkEntity);
-            forwardRelationship.setTargetEntityName(fkEntity);
-
-            // forwardRelationship is a reference from table with foreign key, it is what exactly we load from db
-            DbRelationshipDetected reverseRelationship = new DbRelationshipDetected(generateName(fkEntity, key, false));
-            reverseRelationship.setFkName(key.getFKName());
-            reverseRelationship.setSourceEntity(fkEntity);
-            reverseRelationship.setTargetEntityName(pkEntity);
-            reverseRelationship.setToMany(false);
-
-            createAndAppendJoins(exportedKeys, pkEntity, fkEntity, forwardRelationship, reverseRelationship);
-
-            boolean toDependentPK = isToDependentPK(forwardRelationship);
-            forwardRelationship.setToDependentPK(toDependentPK);
-
-            boolean isOneToOne = toDependentPK && fkEntity.getPrimaryKeys().size() == forwardRelationship.getJoins().size();
-
-            forwardRelationship.setToMany(!isOneToOne);
-            forwardRelationship.setName(generateName(pkEntity, key, !isOneToOne));
-
-            if (delegate.dbRelationshipLoaded(fkEntity, reverseRelationship)) {
-                fkEntity.addRelationship(reverseRelationship);
-            }
-            if (delegate.dbRelationshipLoaded(pkEntity, forwardRelationship)) {
-                pkEntity.addRelationship(forwardRelationship);
-            }
-        }
-    }
-
-    private boolean isToDependentPK(DbRelationship forwardRelationship) {
-        for (DbJoin dbJoin : forwardRelationship.getJoins()) {
-            if (!dbJoin.getTarget().isPrimaryKey()) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    private void createAndAppendJoins(Set<ExportedKey> exportedKeys, DbEntity pkEntity, DbEntity fkEntity, DbRelationship forwardRelationship, DbRelationshipDetected reverseRelationship) {
-        for (ExportedKey exportedKey : exportedKeys) {
-            // Create and append joins
-            String pkName = exportedKey.getPKColumnName();
-            String fkName = exportedKey.getFKColumnName();
-
-            // skip invalid joins...
-            DbAttribute pkAtt = pkEntity.getAttribute(pkName);
-            if (pkAtt == null) {
-                LOGGER.info("no attribute for declared primary key: " + pkName);
-                continue;
-            }
-
-            DbAttribute fkAtt = fkEntity.getAttribute(fkName);
-            if (fkAtt == null) {
-                LOGGER.info("no attribute for declared foreign key: " + fkName);
-                continue;
-            }
-
-            forwardRelationship.addJoin(new DbJoin(forwardRelationship, pkName, fkName));
-            reverseRelationship.addJoin(new DbJoin(reverseRelationship, fkName, pkName));
-        }
-    }
-
-    private Map<String, Set<ExportedKey>> loadExportedKeys(DbLoaderConfiguration config, String catalog, String schema, Map<String, DbEntity> tables) throws SQLException {
-        Map<String, Set<ExportedKey>> keys = new HashMap<String, Set<ExportedKey>>();
-
-        for (DbEntity dbEntity : tables.values()) {
-            if (!delegate.dbRelationship(dbEntity)) {
-                continue;
-            }
-
-            ResultSet rs;
-            try {
-                rs = getMetaData().getExportedKeys(catalog, schema, dbEntity.getName());
-            } catch (SQLException cay182Ex) {
-                // Sybase-specific - the line above blows on VIEWS, see CAY-182.
-                LOGGER.info("Error getting relationships for '" + catalog + "." + schema + "', ignoring. "
-                        + cay182Ex.getMessage(), cay182Ex);
-                return new HashMap<String, Set<ExportedKey>>();
-            }
-
-            try {
-                while (rs.next()) {
-                    ExportedKey key = ExportedKey.extractData(rs);
-
-                    DbEntity fkEntity = tables.get(key.getFKTableName());
-                    if (fkEntity == null) {
-                        skipRelationLog(key, key.getFKTableName());
-                        continue;
-                    }
-
-                    if (config.getFiltersConfig().tableFilter(fkEntity.getCatalog(), fkEntity.getSchema()).isIncludeTable(fkEntity.getName()) == null) {
-                        continue;
-                    }
-
-                    Set<ExportedKey> exportedKeys = keys.get(key.getStrKey());
-                    if (exportedKeys == null) {
-                        exportedKeys = new TreeSet<ExportedKey>();
-
-                        keys.put(key.getStrKey(), exportedKeys);
-                    }
-                    exportedKeys.add(key);
-                }
-
-            } finally {
-                rs.close();
-            }
-        }
-        return keys;
-    }
-
-    private void skipRelationLog(ExportedKey key, String tableName) {
-        // if (LOGGER.isDebugEnabled()) {
-        LOGGER.info("Skip relation: '" + key + "' because table '" + tableName + "' not found");
-        // }
-    }
-
-    private String generateName(DbEntity entity, ExportedKey key, boolean toMany) {
-        String forwardPreferredName = nameGenerator.createDbRelationshipName(key, toMany);
-        return DefaultUniqueNameGenerator
-                .generate(NameCheckers.dbRelationship, entity, forwardPreferredName);
-    }
-
-    /**
-     * Flattens many-to-many relationships in the generated model.
-     */
-    public static void flattenManyToManyRelationships(DataMap map, Collection<ObjEntity> loadedObjEntities, ObjectNameGenerator objectNameGenerator) {
-        if (loadedObjEntities.isEmpty()) {
-            return;
-        }
-        Collection<ObjEntity> entitiesForDelete = new LinkedList<ObjEntity>();
-
-        for (ObjEntity curEntity : loadedObjEntities) {
-            ManyToManyCandidateEntity entity = ManyToManyCandidateEntity.build(curEntity);
-
-            if (entity != null) {
-                entity.optimizeRelationships(objectNameGenerator);
-                entitiesForDelete.add(curEntity);
-            }
-        }
-
-        // remove needed entities
-        for (ObjEntity curDeleteEntity : entitiesForDelete) {
-            map.removeObjEntity(curDeleteEntity.getName(), true);
-        }
-        loadedObjEntities.removeAll(entitiesForDelete);
-    }
-
-    private void fireObjEntitiesAddedEvents(Collection<ObjEntity> loadedObjEntities) {
-        for (ObjEntity curEntity : loadedObjEntities) {
-            // notify delegate
-            if (delegate != null) {
-                delegate.objEntityAdded(curEntity);
-            }
-        }
-    }
-
-    /**
-     * By default we want to load Tables and Views for mo types
-     *
-     * @see DbLoader#getTableTypes()
-     * @since 4.0
-     */
-    public String[] getDefaultTableTypes() {
-        List<String> list = new ArrayList<String>(2);
-
-        String viewType = adapter.tableTypeForView();
-        if (viewType != null) {
-            list.add(viewType);
-        }
-
-        String tableType = adapter.tableTypeForTable();
-        if (tableType != null) {
-            list.add(tableType);
-        }
-
-        return list.toArray(new String[list.size()]);
-    }
-
-    /**
-     * Performs database reverse engineering and generates DataMap that contains
-     * default mapping of the tables and views. By default will include regular
-     * tables and views.
-     *
-     * @since 1.0.7
-     * @deprecated since 4.0 use
-     * {@link #load(org.apache.cayenne.map.DataMap, DbLoaderConfiguration)}
-     * method that supports catalogs.
-     */
-    @Deprecated
-    public DataMap loadDataMapFromDB(String schemaPattern, String tablePattern, DataMap dataMap) throws SQLException {
-
-        DbLoaderConfiguration configuration = new DbLoaderConfiguration();
-        configuration.setFiltersConfig(FiltersConfig.create(null, schemaPattern,
-                TableFilter.include(tablePattern), PatternFilter.INCLUDE_NOTHING));
-
-        load(dataMap, configuration);
-        return dataMap;
-    }
-
-    /**
-     * Performs database reverse engineering and generates DataMap object that
-     * contains default mapping of the tables and views. Allows to limit types
-     * of tables to read.
-     *
-     * @deprecated since 4.0 use
-     * {@link #load(org.apache.cayenne.map.DataMap, DbLoaderConfiguration)}
-     * method that supports catalogs.
-     */
-    @Deprecated
-    public DataMap loadDataMapFromDB(String schemaPattern, String tablePattern, String[] tableTypes, DataMap dataMap)
-            throws SQLException {
-        dataMap.clear();
-
-        DbLoaderConfiguration config = new DbLoaderConfiguration();
-        config.setFiltersConfig(FiltersConfig.create(null, schemaPattern,
-                TableFilter.include(tablePattern), PatternFilter.INCLUDE_NOTHING));
-        config.setTableTypes(tableTypes);
-
-        load(dataMap, config);
-        return dataMap;
-    }
-
-    /**
-     * Performs database reverse engineering based on the specified config
-     * and fills the specified
-     * DataMap object with DB and object mapping info.
-     *
-     * @since 4.0
-     */
-    public void load(DataMap dataMap, DbLoaderConfiguration config) throws SQLException {
-        LOGGER.info("Schema loading...");
-
-        String[] types = config.getTableTypes();
-        if (types == null || types.length == 0) {
-            types = getDefaultTableTypes();
-        }
-
-        for (CatalogFilter catalog : config.getFiltersConfig().catalogs) {
-            for (SchemaFilter schema : catalog.schemas) {
-
-                List<DbEntity> entities = createTableLoader(catalog.name, schema.name, schema.tables)
-                        .loadDbEntities(dataMap, config, types);
-
-                if (entities != null) {
-                    loadDbRelationships(config, catalog.name, schema.name, entities);
-
-                    prepareObjLayer(dataMap, config, entities);
-                }
-            }
-        }
-    }
-
-    protected DbTableLoader createTableLoader(String catalog, String schema, TableFilter filter) throws SQLException {
-        return new DbTableLoader(catalog, schema, getMetaData(), delegate,
-                new DbAttributesPerSchemaLoader(catalog, schema, getMetaData(), adapter, filter));
-    }
-
-    public void prepareObjLayer(DataMap dataMap, DbLoaderConfiguration config, Collection<DbEntity> entities) {
-        Collection<ObjEntity> loadedObjEntities = loadObjEntities(dataMap, config, entities);
-        flattenManyToManyRelationships(dataMap, loadedObjEntities, getNameGenerator());
-        fireObjEntitiesAddedEvents(loadedObjEntities);
-    }
-
-    /**
-     * Performs database reverse engineering to match the specified catalog,
-     * schema, table name and table type patterns and fills the specified
-     * DataMap object with DB and object mapping info.
-     *
-     * @since 4.0
-     */
-    public DataMap load(DbLoaderConfiguration config) throws SQLException {
-
-        DataMap dataMap = new DataMap();
-        load(dataMap, config);
-        loadProcedures(dataMap, config);
-
-        return dataMap;
-    }
-
-    /**
-     * Loads database stored procedures into the DataMap.
-     * <p>
-     * <i>As of 1.1 there is no boolean property or delegate method to make
-     * procedure loading optional or to implement custom merging logic, so
-     * currently this method is NOT CALLED from "loadDataMapFromDB" and should
-     * be invoked explicitly by the user. </i>
-     * </p>
-     *
-     * @since 1.1
-     * @deprecated since 4.0 use loadProcedures(DataMap, String, String, String) that supports "catalog" pattern.
-     */
-    @Deprecated
-    public void loadProceduresFromDB(String schemaPattern, String namePattern, DataMap dataMap) throws SQLException {
-        DbLoaderConfiguration configuration = new DbLoaderConfiguration();
-        configuration.setFiltersConfig(FiltersConfig.create(null, schemaPattern,
-                TableFilter.everything(), new PatternFilter().include(namePattern)));
-
-        loadProcedures(dataMap, configuration);
-    }
-
-    /**
-     * Loads database stored procedures into the DataMap.
-     * <p>
-     * <i>As of 1.1 there is no boolean property or delegate method to make
-     * procedure loading optional or to implement custom merging logic, so
-     * currently this method is NOT CALLED from "loadDataMapFromDB" and should
-     * be invoked explicitly by the user. </i>
-     * </p>
-     *
-     * @since 4.0
-     */
-    public Map<String, Procedure> loadProcedures(DataMap dataMap, DbLoaderConfiguration config)
-            throws SQLException {
-
-        Map<String, Procedure> procedures = loadProcedures(config);
-        if (procedures.isEmpty()) {
-            return procedures;
-        }
-
-        loadProceduresColumns(config, procedures);
-
-        for (Procedure procedure : procedures.values()) {
-            dataMap.addProcedure(procedure);
-        }
-
-        return procedures;
-    }
-
-    private void loadProceduresColumns(DbLoaderConfiguration config, Map<String, Procedure> procedures) throws SQLException {
-
-        for (CatalogFilter catalog : config.getFiltersConfig().catalogs) {
-            for (SchemaFilter schema : catalog.schemas) {
-                loadProceduresColumns(procedures, catalog.name, schema.name);
-            }
-        }
-    }
-
-    private void loadProceduresColumns(Map<String, Procedure> procedures, String catalog, String schema) throws SQLException {
-        ResultSet columnsRS = getMetaData().getProcedureColumns(catalog, schema, null, null);
-        try {
-            while (columnsRS.next()) {
-
-                String s = columnsRS.getString("PROCEDURE_SCHEM");
-                String name = columnsRS.getString("PROCEDURE_NAME");
-                String key = (s == null ? "" : s + '.') + name;
-                Procedure procedure = procedures.get(key);
-                if (procedure == null) {
-                    continue;
-                }
-
-                ProcedureParameter column = loadProcedureParams(columnsRS, key, procedure);
-                if (column == null) {
-                    continue;
-                }
-                procedure.addCallParameter(column);
-            }
-        } finally {
-            columnsRS.close();
-        }
-    }
-
-    private ProcedureParameter loadProcedureParams(ResultSet columnsRS, String key, Procedure procedure) throws SQLException {
-        String columnName = columnsRS.getString("COLUMN_NAME");
-
-        // skip ResultSet columns, as they are not described in Cayenne procedures yet...
-        short type = columnsRS.getShort("COLUMN_TYPE");
-        if (type == DatabaseMetaData.procedureColumnResult) {
-            LOGGER.debug("skipping ResultSet column: " + key + "." + columnName);
-        }
-
-        if (columnName == null) {
-            if (type == DatabaseMetaData.procedureColumnReturn) {
-                LOGGER.debug("null column name, assuming result column: " + key);
-                columnName = "_return_value";
-                procedure.setReturningValue(true);
-            } else {
-                LOGGER.info("invalid null column name, skipping column : " + key);
-                return null;
-            }
-        }
-
-        int columnType = columnsRS.getInt("DATA_TYPE");
-
-        // ignore precision of non-decimal columns
-        int decimalDigits = -1;
-        if (TypesMapping.isDecimal(columnType)) {
-            decimalDigits = columnsRS.getShort("SCALE");
-            if (columnsRS.wasNull()) {
-                decimalDigits = -1;
-            }
-        }
-
-        ProcedureParameter column = new ProcedureParameter(columnName);
-        int direction = getDirection(type);
-        if (direction != -1) {
-            column.setDirection(direction);
-        }
-
-        column.setType(columnType);
-        column.setMaxLength(columnsRS.getInt("LENGTH"));
-        column.setPrecision(decimalDigits);
-
-        column.setProcedure(procedure);
-        return column;
-    }
-
-    private static int getDirection(short type) {
-        switch (type) {
-            case DatabaseMetaData.procedureColumnIn:
-                return ProcedureParameter.IN_PARAMETER;
-            case DatabaseMetaData.procedureColumnInOut:
-                return ProcedureParameter.IN_OUT_PARAMETER;
-            case DatabaseMetaData.procedureColumnOut:
-                return ProcedureParameter.OUT_PARAMETER;
-            default:
-                return -1;
-        }
-    }
-
-    private Map<String, Procedure> loadProcedures(DbLoaderConfiguration config) throws SQLException {
-        Map<String, Procedure> procedures = new HashMap<String, Procedure>();
-
-        FiltersConfig filters = config.getFiltersConfig();
-        for (CatalogFilter catalog : filters.catalogs) {
-            for (SchemaFilter schema : catalog.schemas) {
-                if (filters.proceduresFilter(catalog.name, schema.name).isEmpty()) {
-                    continue;
-                }
-
-                procedures.putAll(loadProcedures(filters, catalog.name, schema.name));
-            }
-        }
-
-        return procedures;
-    }
-
-    private Map<String, Procedure> loadProcedures(FiltersConfig filters, String catalog, String schema) throws SQLException {
-        Map<String, Procedure> procedures = new HashMap<String, Procedure>();
-        // get procedures
-        ResultSet rs = getMetaData().getProcedures(catalog, schema, WILDCARD);
-        try {
-            while (rs.next()) {
-
-                String name = rs.getString("PROCEDURE_NAME");
-                Procedure procedure = new Procedure(name);
-                procedure.setCatalog(rs.getString("PROCEDURE_CAT"));
-                procedure.setSchema(rs.getString("PROCEDURE_SCHEM"));
-
-                if (filters.proceduresFilter(procedure.getCatalog(), procedure.getSchema())
-                        .isInclude(procedure.getName())) {
-                    LOGGER.info("skipping Cayenne PK procedure: " + name);
-                    continue;
-                }
-
-                switch (rs.getShort("PROCEDURE_TYPE")) {
-                    case DatabaseMetaData.procedureNoResult:
-                    case DatabaseMetaData.procedureResultUnknown:
-                        procedure.setReturningValue(false);
-                        break;
-                    case DatabaseMetaData.procedureReturnsResult:
-                        procedure.setReturningValue(true);
-                        break;
-                }
-
-                procedures.put(procedure.getFullyQualifiedName(), procedure);
-            }
-        } finally {
-            rs.close();
-        }
-        return procedures;
-    }
-
-    /**
-     * Sets new naming strategy for reverse engineering
-     *
-     * @since 3.0
-     */
-    public void setNameGenerator(ObjectNameGenerator strategy) {
-        if (strategy == null) {
-            LOGGER.warn("Attempt to set null into NameGenerator. LegacyNameGenerator will be used.");
-            this.nameGenerator = new LegacyNameGenerator();
-        } else {
-            this.nameGenerator = strategy;
-        }
-    }
-
-    /**
-     * @return naming strategy for reverse engineering
-     * @since 3.0
-     */
-    public ObjectNameGenerator getNameGenerator() {
-        return nameGenerator;
-    }
+	private static final Log LOGGER = LogFactory.getLog(DbLoader.class);
+
+	public static final String WILDCARD = "%";
+	public static final String WILDCARD_PATTERN = ".*";
+
+	private final Connection connection;
+	private final DbAdapter adapter;
+	private final DbLoaderDelegate delegate;
+
+	private boolean creatingMeaningfulPK;
+
+	private DatabaseMetaData metaData;
+
+	/**
+	 * Strategy for choosing names for entities, attributes and relationships
+	 */
+	private ObjectNameGenerator nameGenerator;
+
+	/**
+	 * Creates new DbLoader.
+	 */
+	public DbLoader(Connection connection, DbAdapter adapter, DbLoaderDelegate delegate) {
+		this(connection, adapter, delegate, new LegacyNameGenerator());
+	}
+
+	/**
+	 * Creates new DbLoader with specified naming strategy.
+	 *
+	 * @since 3.0
+	 */
+	public DbLoader(Connection connection, DbAdapter adapter, DbLoaderDelegate delegate, ObjectNameGenerator strategy) {
+		this.adapter = adapter;
+		this.connection = connection;
+		this.delegate = delegate == null ? new DefaultDbLoaderDelegate() : delegate;
+
+		setNameGenerator(strategy);
+	}
+
+	/**
+	 * Returns DatabaseMetaData object associated with this DbLoader.
+	 */
+	private DatabaseMetaData getMetaData() throws SQLException {
+		if (metaData == null) {
+			metaData = connection.getMetaData();
+		}
+		return metaData;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	public void setCreatingMeaningfulPK(boolean creatingMeaningfulPK) {
+		this.creatingMeaningfulPK = creatingMeaningfulPK;
+	}
+
+	/**
+	 * Returns true if the generator should map all primary key columns as
+	 * ObjAttributes.
+	 *
+	 * @since 3.0
+	 */
+	public boolean isCreatingMeaningfulPK() {
+		return creatingMeaningfulPK;
+	}
+
+	/**
+	 * Returns database connection used by this DbLoader.
+	 *
+	 * @since 3.0
+	 */
+	public Connection getConnection() {
+		return connection;
+	}
+
+	/**
+	 * Returns DbAdapter associated with this DbLoader.
+	 *
+	 * @since 1.1
+	 */
+	public DbAdapter getAdapter() {
+		return adapter;
+	}
+
+	/**
+	 * Retrieves catalogs for the database associated with this DbLoader.
+	 *
+	 * @return List with the catalog names, empty Array if none found.
+	 */
+	public List<String> getCatalogs() throws SQLException {
+		try (ResultSet rs = getMetaData().getCatalogs()) {
+			return getStrings(rs);
+		}
+	}
+
+	/**
+	 * Retrieves the schemas for the database.
+	 *
+	 * @return List with the schema names, empty Array if none found.
+	 */
+	public List<String> getSchemas() throws SQLException {
+
+		try (ResultSet rs = getMetaData().getSchemas()) {
+			return getStrings(rs);
+		}
+	}
+
+	private static List<String> getStrings(ResultSet rs) throws SQLException {
+		List<String> strings = new ArrayList<String>();
+
+		while (rs.next()) {
+			strings.add(rs.getString(1));
+		}
+
+		return strings;
+	}
+
+	/**
+	 * Returns all the table types for the given database. Types may be such as
+	 * Typical types are "TABLE", "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY",
+	 * "LOCAL TEMPORARY", "ALIAS", "SYNONYM"., etc.
+	 *
+	 * @return List of Strings, empty array if nothing found.
+	 */
+	public List<String> getTableTypes() throws SQLException {
+		List<String> types = new ArrayList<String>();
+
+		try (ResultSet rs = getMetaData().getTableTypes();) {
+			while (rs.next()) {
+				types.add(rs.getString("TABLE_TYPE").trim());
+			}
+		}
+
+		return types;
+	}
+
+	/**
+	 * Creates an ObjEntity for each DbEntity in the map.
+	 */
+	public Collection<ObjEntity> loadObjEntities(DataMap map, DbLoaderConfiguration config,
+			Collection<DbEntity> entities) {
+		Collection<ObjEntity> loadedEntities = DbLoader.loadObjEntities(map, config, entities, nameGenerator);
+
+		createEntityMerger(map).synchronizeWithDbEntities(loadedEntities);
+
+		return loadedEntities;
+	}
+
+	public static Collection<ObjEntity> loadObjEntities(DataMap map, DbLoaderConfiguration config,
+			Collection<DbEntity> entities, ObjectNameGenerator nameGenerator) {
+		if (entities.isEmpty()) {
+			return Collections.emptyList();
+		}
+
+		Collection<ObjEntity> loadedEntities = new ArrayList<ObjEntity>(entities.size());
+
+		// doLoad empty ObjEntities for all the tables
+		for (DbEntity dbEntity : entities) {
+
+			// check if there are existing entities
+
+			// TODO: performance. This is an O(n^2) search and it shows on
+			// YourKit profiles. Pre-cache mapped entities perhaps (?)
+			Collection<ObjEntity> existing = map.getMappedEntities(dbEntity);
+			if (!existing.isEmpty()) {
+				loadedEntities.addAll(existing);
+				continue;
+			}
+
+			String objEntityName = DefaultUniqueNameGenerator.generate(NameCheckers.objEntity, map,
+					nameGenerator.createObjEntityName(dbEntity));
+
+			ObjEntity objEntity = new ObjEntity(objEntityName);
+			objEntity.setDbEntity(dbEntity);
+			objEntity.setClassName(config.getGenericClassName() != null ? config.getGenericClassName() : map
+					.getNameWithDefaultPackage(objEntity.getName()));
+
+			map.addObjEntity(objEntity);
+			loadedEntities.add(objEntity);
+		}
+
+		return loadedEntities;
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	protected EntityMergeSupport createEntityMerger(DataMap map) {
+		return new EntityMergeSupport(map, nameGenerator, !creatingMeaningfulPK);
+	}
+
+	protected void loadDbRelationships(DbLoaderConfiguration config, String catalog, String schema,
+			List<DbEntity> tables) throws SQLException {
+		if (config.isSkipRelationshipsLoading()) {
+			return;
+		}
+
+		// Get all the foreign keys referencing this table
+		Map<String, DbEntity> tablesMap = new HashMap<String, DbEntity>();
+		for (DbEntity table : tables) {
+			tablesMap.put(table.getName(), table);
+		}
+
+		Map<String, Set<ExportedKey>> keys = loadExportedKeys(config, catalog, schema, tablesMap);
+		for (Map.Entry<String, Set<ExportedKey>> entry : keys.entrySet()) {
+			if (LOGGER.isDebugEnabled()) {
+				LOGGER.debug("Process keys for: " + entry.getKey());
+			}
+
+			Set<ExportedKey> exportedKeys = entry.getValue();
+			ExportedKey key = exportedKeys.iterator().next();
+			if (key == null) {
+				throw new IllegalStateException();
+			}
+
+			DbEntity pkEntity = tablesMap.get(key.getPKTableName());
+			if (pkEntity == null) {
+				skipRelationLog(key, key.getPKTableName());
+				continue;
+			}
+
+			DbEntity fkEntity = tablesMap.get(key.getFKTableName());
+			if (fkEntity == null) {
+				skipRelationLog(key, key.getFKTableName());
+				continue;
+			}
+
+			if (!new EqualsBuilder().append(pkEntity.getCatalog(), key.pkCatalog)
+					.append(pkEntity.getSchema(), key.pkSchema).append(fkEntity.getCatalog(), key.fkCatalog)
+					.append(fkEntity.getSchema(), key.fkSchema).isEquals()) {
+
+				LOGGER.info("Skip relation: '" + key + "' because it related to objects from other catalog/schema");
+				LOGGER.info("     relation primary key: '" + key.pkCatalog + "." + key.pkSchema + "'");
+				LOGGER.info("       primary key entity: '" + pkEntity.getCatalog() + "." + pkEntity.getSchema() + "'");
+				LOGGER.info("     relation foreign key: '" + key.fkCatalog + "." + key.fkSchema + "'");
+				LOGGER.info("       foreign key entity: '" + fkEntity.getCatalog() + "." + fkEntity.getSchema() + "'");
+				continue;
+			}
+
+			// forwardRelationship is a reference from table with primary key
+			DbRelationship forwardRelationship = new DbRelationship(generateName(pkEntity, key, true));
+			forwardRelationship.setSourceEntity(pkEntity);
+			forwardRelationship.setTargetEntityName(fkEntity);
+
+			// forwardRelationship is a reference from table with foreign key,
+			// it is what exactly we load from db
+			DbRelationshipDetected reverseRelationship = new DbRelationshipDetected(generateName(fkEntity, key, false));
+			reverseRelationship.setFkName(key.getFKName());
+			reverseRelationship.setSourceEntity(fkEntity);
+			reverseRelationship.setTargetEntityName(pkEntity);
+			reverseRelationship.setToMany(false);
+
+			createAndAppendJoins(exportedKeys, pkEntity, fkEntity, forwardRelationship, reverseRelationship);
+
+			boolean toDependentPK = isToDependentPK(forwardRelationship);
+			forwardRelationship.setToDependentPK(toDependentPK);
+
+			boolean isOneToOne = toDependentPK
+					&& fkEntity.getPrimaryKeys().size() == forwardRelationship.getJoins().size();
+
+			forwardRelationship.setToMany(!isOneToOne);
+			forwardRelationship.setName(generateName(pkEntity, key, !isOneToOne));
+
+			if (delegate.dbRelationshipLoaded(fkEntity, reverseRelationship)) {
+				fkEntity.addRelationship(reverseRelationship);
+			}
+			if (delegate.dbRelationshipLoaded(pkEntity, forwardRelationship)) {
+				pkEntity.addRelationship(forwardRelationship);
+			}
+		}
+	}
+
+	private boolean isToDependentPK(DbRelationship forwardRelationship) {
+		for (DbJoin dbJoin : forwardRelationship.getJoins()) {
+			if (!dbJoin.getTarget().isPrimaryKey()) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	private void createAndAppendJoins(Set<ExportedKey> exportedKeys, DbEntity pkEntity, DbEntity fkEntity,
+			DbRelationship forwardRelationship, DbRelationshipDetected reverseRelationship) {
+		for (ExportedKey exportedKey : exportedKeys) {
+			// Create and append joins
+			String pkName = exportedKey.getPKColumnName();
+			String fkName = exportedKey.getFKColumnName();
+
+			// skip invalid joins...
+			DbAttribute pkAtt = pkEntity.getAttribute(pkName);
+			if (pkAtt == null) {
+				LOGGER.info("no attribute for declared primary key: " + pkName);
+				continue;
+			}
+
+			DbAttribute fkAtt = fkEntity.getAttribute(fkName);
+			if (fkAtt == null) {
+				LOGGER.info("no attribute for declared foreign key: " + fkName);
+				continue;
+			}
+
+			forwardRelationship.addJoin(new DbJoin(forwardRelationship, pkName, fkName));
+			reverseRelationship.addJoin(new DbJoin(reverseRelationship, fkName, pkName));
+		}
+	}
+
+	private Map<String, Set<ExportedKey>> loadExportedKeys(DbLoaderConfiguration config, String catalog, String schema,
+			Map<String, DbEntity> tables) throws SQLException {
+		Map<String, Set<ExportedKey>> keys = new HashMap<String, Set<ExportedKey>>();
+
+		for (DbEntity dbEntity : tables.values()) {
+			if (!delegate.dbRelationship(dbEntity)) {
+				continue;
+			}
+
+			ResultSet rs;
+			try {
+				rs = getMetaData().getExportedKeys(catalog, schema, dbEntity.getName());
+			} catch (SQLException cay182Ex) {
+				// Sybase-specific - the line above blows on VIEWS, see CAY-182.
+				LOGGER.info(
+						"Error getting relationships for '" + catalog + "." + schema + "', ignoring. "
+								+ cay182Ex.getMessage(), cay182Ex);
+				return new HashMap<String, Set<ExportedKey>>();
+			}
+
+			try {
+				while (rs.next()) {
+					ExportedKey key = ExportedKey.extractData(rs);
+
+					DbEntity fkEntity = tables.get(key.getFKTableName());
+					if (fkEntity == null) {
+						skipRelationLog(key, key.getFKTableName());
+						continue;
+					}
+
+					if (config.getFiltersConfig().tableFilter(fkEntity.getCatalog(), fkEntity.getSchema())
+							.isIncludeTable(fkEntity.getName()) == null) {
+						continue;
+					}
+
+					Set<ExportedKey> exportedKeys = keys.get(key.getStrKey());
+					if (exportedKeys == null) {
+						exportedKeys = new TreeSet<ExportedKey>();
+
+						keys.put(key.getStrKey(), exportedKeys);
+					}
+					exportedKeys.add(key);
+				}
+
+			} finally {
+				rs.close();
+			}
+		}
+		return keys;
+	}
+
+	private void skipRelationLog(ExportedKey key, String tableName) {
+		// if (LOGGER.isDebugEnabled()) {
+		LOGGER.info("Skip relation: '" + key + "' because table '" + tableName + "' not found");
+		// }
+	}
+
+	private String generateName(DbEntity entity, ExportedKey key, boolean toMany) {
+		String forwardPreferredName = nameGenerator.createDbRelationshipName(key, toMany);
+		return DefaultUniqueNameGenerator.generate(NameCheckers.dbRelationship, entity, forwardPreferredName);
+	}
+
+	/**
+	 * Flattens many-to-many relationships in the generated model.
+	 */
+	public static void flattenManyToManyRelationships(DataMap map, Collection<ObjEntity> loadedObjEntities,
+			ObjectNameGenerator objectNameGenerator) {
+		if (loadedObjEntities.isEmpty()) {
+			return;
+		}
+		Collection<ObjEntity> entitiesForDelete = new LinkedList<ObjEntity>();
+
+		for (ObjEntity curEntity : loadedObjEntities) {
+			ManyToManyCandidateEntity entity = ManyToManyCandidateEntity.build(curEntity);
+
+			if (entity != null) {
+				entity.optimizeRelationships(objectNameGenerator);
+				entitiesForDelete.add(curEntity);
+			}
+		}
+
+		// remove needed entities
+		for (ObjEntity curDeleteEntity : entitiesForDelete) {
+			map.removeObjEntity(curDeleteEntity.getName(), true);
+		}
+		loadedObjEntities.removeAll(entitiesForDelete);
+	}
+
+	private void fireObjEntitiesAddedEvents(Collection<ObjEntity> loadedObjEntities) {
+		for (ObjEntity curEntity : loadedObjEntities) {
+			// notify delegate
+			if (delegate != null) {
+				delegate.objEntityAdded(curEntity);
+			}
+		}
+	}
+
+	/**
+	 * By default we want to load Tables and Views for mo types
+	 *
+	 * @see DbLoader#getTableTypes()
+	 * @since 4.0
+	 */
+	public String[] getDefaultTableTypes() {
+		List<String> list = new ArrayList<String>(2);
+
+		String viewType = adapter.tableTypeForView();
+		if (viewType != null) {
+			list.add(viewType);
+		}
+
+		String tableType = adapter.tableTypeForTable();
+		if (tableType != null) {
+			list.add(tableType);
+		}
+
+		return list.toArray(new String[list.size()]);
+	}
+
+	/**
+	 * Performs database reverse engineering and generates DataMap that contains
+	 * default mapping of the tables and views. By default will include regular
+	 * tables and views.
+	 *
+	 * @since 1.0.7
+	 * @deprecated since 4.0 use
+	 *             {@link #load(org.apache.cayenne.map.DataMap, DbLoaderConfiguration)}
+	 *             method that supports catalogs.
+	 */
+	@Deprecated
+	public DataMap loadDataMapFromDB(String schemaPattern, String tablePattern, DataMap dataMap) throws SQLException {
+
+		DbLoaderConfiguration configuration = new DbLoaderConfiguration();
+		configuration.setFiltersConfig(FiltersConfig.create(null, schemaPattern, TableFilter.include(tablePattern),
+				PatternFilter.INCLUDE_NOTHING));
+
+		load(dataMap, configuration);
+		return dataMap;
+	}
+
+	/**
+	 * Performs database reverse engineering and generates DataMap object that
+	 * contains default mapping of the tables and views. Allows to limit types
+	 * of tables to read.
+	 *
+	 * @deprecated since 4.0 use
+	 *             {@link #load(org.apache.cayenne.map.DataMap, DbLoaderConfiguration)}
+	 *             method that supports catalogs.
+	 */
+	@Deprecated
+	public DataMap loadDataMapFromDB(String schemaPattern, String tablePattern, String[] tableTypes, DataMap dataMap)
+			throws SQLException {
+		dataMap.clear();
+
+		DbLoaderConfiguration config = new DbLoaderConfiguration();
+		config.setFiltersConfig(FiltersConfig.create(null, schemaPattern, TableFilter.include(tablePattern),
+				PatternFilter.INCLUDE_NOTHING));
+		config.setTableTypes(tableTypes);
+
+		load(dataMap, config);
+		return dataMap;
+	}
+
+	/**
+	 * Performs database reverse engineering based on the specified config and
+	 * fills the specified DataMap object with DB and object mapping info.
+	 *
+	 * @since 4.0
+	 */
+	public void load(DataMap dataMap, DbLoaderConfiguration config) throws SQLException {
+		LOGGER.info("Schema loading...");
+
+		String[] types = config.getTableTypes();
+		if (types == null || types.length == 0) {
+			types = getDefaultTableTypes();
+		}
+
+		for (CatalogFilter catalog : config.getFiltersConfig().catalogs) {
+			for (SchemaFilter schema : catalog.schemas) {
+
+				List<DbEntity> entities = createTableLoader(catalog.name, schema.name, schema.tables).loadDbEntities(
+						dataMap, config, types);
+
+				if (entities != null) {
+					loadDbRelationships(config, catalog.name, schema.name, entities);
+
+					prepareObjLayer(dataMap, config, entities);
+				}
+			}
+		}
+	}
+
+	protected DbTableLoader createTableLoader(String catalog, String schema, TableFilter filter) throws SQLException {
+		return new DbTableLoader(catalog, schema, getMetaData(), delegate, new DbAttributesPerSchemaLoader(catalog,
+				schema, getMetaData(), adapter, filter));
+	}
+
+	public void prepareObjLayer(DataMap dataMap, DbLoaderConfiguration config, Collection<DbEntity> entities) {
+		Collection<ObjEntity> loadedObjEntities = loadObjEntities(dataMap, config, entities);
+		flattenManyToManyRelationships(dataMap, loadedObjEntities, getNameGenerator());
+		fireObjEntitiesAddedEvents(loadedObjEntities);
+	}
+
+	/**
+	 * Performs database reverse engineering to match the specified catalog,
+	 * schema, table name and table type patterns and fills the specified
+	 * DataMap object with DB and object mapping info.
+	 *
+	 * @since 4.0
+	 */
+	public DataMap load(DbLoaderConfiguration config) throws SQLException {
+
+		DataMap dataMap = new DataMap();
+		load(dataMap, config);
+		loadProcedures(dataMap, config);
+
+		return dataMap;
+	}
+
+	/**
+	 * Loads database stored procedures into the DataMap.
+	 * <p>
+	 * <i>As of 1.1 there is no boolean property or delegate method to make
+	 * procedure loading optional or to implement custom merging logic, so
+	 * currently this method is NOT CALLED from "loadDataMapFromDB" and should
+	 * be invoked explicitly by the user. </i>
+	 * </p>
+	 *
+	 * @since 1.1
+	 * @deprecated since 4.0 use loadProcedures(DataMap, String, String, String)
+	 *             that supports "catalog" pattern.
+	 */
+	@Deprecated
+	public void loadProceduresFromDB(String schemaPattern, String namePattern, DataMap dataMap) throws SQLException {
+		DbLoaderConfiguration configuration = new DbLoaderConfiguration();
+		configuration.setFiltersConfig(FiltersConfig.create(null, schemaPattern, TableFilter.everything(),
+				new PatternFilter().include(namePattern)));
+
+		loadProcedures(dataMap, configuration);
+	}
+
+	/**
+	 * Loads database stored procedures into the DataMap.
+	 * <p>
+	 * <i>As of 1.1 there is no boolean property or delegate method to make
+	 * procedure loading optional or to implement custom merging logic, so
+	 * currently this method is NOT CALLED from "loadDataMapFromDB" and should
+	 * be invoked explicitly by the user. </i>
+	 * </p>
+	 *
+	 * @since 4.0
+	 */
+	public Map<String, Procedure> loadProcedures(DataMap dataMap, DbLoaderConfiguration config) throws SQLException {
+
+		Map<String, Procedure> procedures = loadProcedures(config);
+		if (procedures.isEmpty()) {
+			return procedures;
+		}
+
+		loadProceduresColumns(config, procedures);
+
+		for (Procedure procedure : procedures.values()) {
+			dataMap.addProcedure(procedure);
+		}
+
+		return procedures;
+	}
+
+	private void loadProceduresColumns(DbLoaderConfiguration config, Map<String, Procedure> procedures)
+			throws SQLException {
+
+		for (CatalogFilter catalog : config.getFiltersConfig().catalogs) {
+			for (SchemaFilter schema : catalog.schemas) {
+				loadProceduresColumns(procedures, catalog.name, schema.name);
+			}
+		}
+	}
+
+	private void loadProceduresColumns(Map<String, Procedure> procedures, String catalog, String schema)
+			throws SQLException {
+
+		try (ResultSet columnsRS = getMetaData().getProcedureColumns(catalog, schema, null, null);) {
+			while (columnsRS.next()) {
+
+				String s = columnsRS.getString("PROCEDURE_SCHEM");
+				String name = columnsRS.getString("PROCEDURE_NAME");
+				String key = (s == null ? "" : s + '.') + name;
+				Procedure procedure = procedures.get(key);
+				if (procedure == null) {
+					continue;
+				}
+
+				ProcedureParameter column = loadProcedureParams(columnsRS, key, procedure);
+				if (column == null) {
+					continue;
+				}
+				procedure.addCallParameter(column);
+			}
+		}
+	}
+
+	private ProcedureParameter loadProcedureParams(ResultSet columnsRS, String key, Procedure procedure)
+			throws SQLException {
+		String columnName = columnsRS.getString("COLUMN_NAME");
+
+		// skip ResultSet columns, as they are not described in Cayenne
+		// procedures yet...
+		short type = columnsRS.getShort("COLUMN_TYPE");
+		if (type == DatabaseMetaData.procedureColumnResult) {
+			LOGGER.debug("skipping ResultSet column: " + key + "." + columnName);
+		}
+
+		if (columnName == null) {
+			if (type == DatabaseMetaData.procedureColumnReturn) {
+				LOGGER.debug("null column name, assuming result column: " + key);
+				columnName = "_return_value";
+				procedure.setReturningValue(true);
+			} else {
+				LOGGER.info("invalid null column name, skipping column : " + key);
+				return null;
+			}
+		}
+
+		int columnType = columnsRS.getInt("DATA_TYPE");
+
+		// ignore precision of non-decimal columns
+		int decimalDigits = -1;
+		if (TypesMapping.isDecimal(columnType)) {
+			decimalDigits = columnsRS.getShort("SCALE");
+			if (columnsRS.wasNull()) {
+				decimalDigits = -1;
+			}
+		}
+
+		ProcedureParameter column = new ProcedureParameter(columnName);
+		int direction = getDirection(type);
+		if (direction != -1) {
+			column.setDirection(direction);
+		}
+
+		column.setType(columnType);
+		column.setMaxLength(columnsRS.getInt("LENGTH"));
+		column.setPrecision(decimalDigits);
+
+		column.setProcedure(procedure);
+		return column;
+	}
+
+	private static int getDirection(short type) {
+		switch (type) {
+		case DatabaseMetaData.procedureColumnIn:
+			return ProcedureParameter.IN_PARAMETER;
+		case DatabaseMetaData.procedureColumnInOut:
+			return ProcedureParameter.IN_OUT_PARAMETER;
+		case DatabaseMetaData.procedureColumnOut:
+			return ProcedureParameter.OUT_PARAMETER;
+		default:
+			return -1;
+		}
+	}
+
+	private Map<String, Procedure> loadProcedures(DbLoaderConfiguration config) throws SQLException {
+		Map<String, Procedure> procedures = new HashMap<String, Procedure>();
+
+		FiltersConfig filters = config.getFiltersConfig();
+		for (CatalogFilter catalog : filters.catalogs) {
+			for (SchemaFilter schema : catalog.schemas) {
+				if (filters.proceduresFilter(catalog.name, schema.name).isEmpty()) {
+					continue;
+				}
+
+				procedures.putAll(loadProcedures(filters, catalog.name, schema.name));
+			}
+		}
+
+		return procedures;
+	}
+
+	private Map<String, Procedure> loadProcedures(FiltersConfig filters, String catalog, String schema)
+			throws SQLException {
+		Map<String, Procedure> procedures = new HashMap<String, Procedure>();
+		// get procedures
+
+		try (ResultSet rs = getMetaData().getProcedures(catalog, schema, WILDCARD);) {
+			while (rs.next()) {
+
+				String name = rs.getString("PROCEDURE_NAME");
+				Procedure procedure = new Procedure(name);
+				procedure.setCatalog(rs.getString("PROCEDURE_CAT"));
+				procedure.setSchema(rs.getString("PROCEDURE_SCHEM"));
+
+				if (filters.proceduresFilter(procedure.getCatalog(), procedure.getSchema()).isInclude(
+						procedure.getName())) {
+					LOGGER.info("skipping Cayenne PK procedure: " + name);
+					continue;
+				}
+
+				switch (rs.getShort("PROCEDURE_TYPE")) {
+				case DatabaseMetaData.procedureNoResult:
+				case DatabaseMetaData.procedureResultUnknown:
+					procedure.setReturningValue(false);
+					break;
+				case DatabaseMetaData.procedureReturnsResult:
+					procedure.setReturningValue(true);
+					break;
+				}
+
+				procedures.put(procedure.getFullyQualifiedName(), procedure);
+			}
+		}
+		return procedures;
+	}
+
+	/**
+	 * Sets new naming strategy for reverse engineering
+	 *
+	 * @since 3.0
+	 */
+	public void setNameGenerator(ObjectNameGenerator strategy) {
+		if (strategy == null) {
+			LOGGER.warn("Attempt to set null into NameGenerator. LegacyNameGenerator will be used.");
+			this.nameGenerator = new LegacyNameGenerator();
+		} else {
+			this.nameGenerator = strategy;
+		}
+	}
+
+	/**
+	 * @return naming strategy for reverse engineering
+	 * @since 3.0
+	 */
+	public ObjectNameGenerator getNameGenerator() {
+		return nameGenerator;
+	}
 }
\ No newline at end of file


Mime
View raw message