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

Reply via email to