View Javadoc
1   /*
2    *
3    * The DbUnit Database Testing Framework
4    * Copyright (C)2005, DbUnit.org
5    *
6    * This library is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License as published by the Free Software Foundation; either
9    * version 2.1 of the License, or (at your option) any later version.
10   *
11   * This library is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   * Lesser General Public License for more details.
15   *
16   * You should have received a copy of the GNU Lesser General Public
17   * License along with this library; if not, write to the Free Software
18   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19   *
20   */
21  
22  package org.dbunit.database.search;
23  
24  import java.sql.Connection;
25  import java.sql.DatabaseMetaData;
26  import java.sql.ResultSet;
27  import java.sql.SQLException;
28  import java.util.SortedSet;
29  import java.util.TreeSet;
30  
31  import org.dbunit.database.DatabaseConfig;
32  import org.dbunit.database.IDatabaseConnection;
33  import org.dbunit.database.IMetadataHandler;
34  import org.dbunit.dataset.NoSuchTableException;
35  import org.dbunit.util.QualifiedTableName;
36  import org.dbunit.util.SQLHelper;
37  import org.dbunit.util.search.AbstractNodesFilterSearchCallback;
38  import org.dbunit.util.search.IEdge;
39  import org.dbunit.util.search.SearchException;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * Super-class for the ISearchCallback that implements the
45   * <code>getEdges()</code> method using the database meta-data.
46   * 
47   * @author Felipe Leme (dbunit@felipeal.net)
48   * @version $Revision$
49   * @since Aug 25, 2005
50   */
51  public abstract class AbstractMetaDataBasedSearchCallback extends AbstractNodesFilterSearchCallback {
52  
53      /**
54       * Logger for this class
55       */
56      private static final Logger logger = LoggerFactory.getLogger(AbstractMetaDataBasedSearchCallback.class);
57  
58      private final IDatabaseConnection connection;
59  
60      /**
61       * Default constructor.
62       * @param connection connection where the edges will be calculated from
63       */
64      public AbstractMetaDataBasedSearchCallback(IDatabaseConnection connection) {
65          this.connection = connection;
66      }
67  
68      /**
69       * Get the connection where the edges will be calculated from.
70       * @return the connection where the edges will be calculated from
71       */
72      public IDatabaseConnection getConnection() {
73          return connection;
74      }
75  
76      protected static final int IMPORT = 0;
77      protected static final int EXPORT = 1;
78  
79      /** 
80       * indexes of the column names on the MetaData result sets.
81       */
82      protected static final int[] TABLENAME_INDEXES = { 3, 7 };  
83      protected static final int[] SCHEMANAME_INDEXES = { 2, 6 };  
84      protected static final int[] PK_INDEXES = { 4, 4 };
85      protected static final int[] FK_INDEXES = { 8, 8 };
86  
87  
88      /**
89       * Get the nodes using the direct foreign key dependency, i.e, if table A has
90       * a FK for a table B, then getNodesFromImportedKeys(A) will return B.
91       * @param node table name 
92       * @return tables with direct FK dependency from node
93       * @throws SearchException
94       */
95      protected SortedSet getNodesFromImportedKeys(Object node)
96      throws SearchException {
97          logger.debug("getNodesFromImportedKeys(node={}) - start", node);
98  
99          return getNodes(IMPORT, node);
100     }
101 
102     /**
103      * Get the nodes using the reverse foreign key dependency, i.e, if table C has
104      * a FK for a table A, then getNodesFromExportedKeys(A) will return C.<br>
105      * 
106      * <strong>NOTE:</strong> this method should be used only as an auxiliary
107      * method for sub-classes that also use <code>getNodesFromImportedKeys()</code>
108      * or something similar, otherwise the generated sequence of tables might not
109      * work when inserted in the database (as some tables might be missing).
110      * <br>
111      * @param node table name 
112      * @return tables with reverse FK dependency from node
113      * @throws SearchException
114      */
115     protected SortedSet getNodesFromExportedKeys(Object node)
116     throws SearchException {
117         logger.debug("getNodesFromExportedKeys(node={}) - start", node);
118 
119         return getNodes(EXPORT, node);
120     }
121 
122     /**
123      * Get the nodes using the both direct and reverse foreign key dependency, i.e, 
124      * if table C has a FK for a table A and table A has a FK for a table B, then 
125      * getNodesFromImportAndExportedKeys(A) will return B and C.
126      * @param node table name 
127      * @return tables with reverse and direct FK dependency from node
128      * @throws SearchException
129      */
130     protected SortedSet getNodesFromImportAndExportKeys(Object node)
131     throws SearchException {
132         logger.debug("getNodesFromImportAndExportKeys(node={}) - start", node);
133 
134         SortedSet importedNodes = getNodesFromImportedKeys( node );
135         SortedSet exportedNodes = getNodesFromExportedKeys( node );
136         importedNodes.addAll( exportedNodes );
137         return importedNodes;
138     }
139 
140     private SortedSet getNodes(int type, Object node) throws SearchException {
141     	if(logger.isDebugEnabled())
142     		logger.debug("getNodes(type={}, node={}) - start", Integer.toString(type), node);
143 
144         try {
145             Connection conn = this.connection.getConnection();
146             String schema = this.connection.getSchema();
147             DatabaseMetaData metaData = conn.getMetaData();
148             SortedSet edges = new TreeSet();
149             getNodes(type, node, conn, schema, metaData, edges);
150             return edges;
151         } catch (SQLException e) {
152             throw new SearchException(e);
153         } catch (NoSuchTableException e) {
154             throw new SearchException(e);
155         }
156     }
157 
158     private void getNodes(int type, Object node, Connection conn,
159             String schema, DatabaseMetaData metaData, SortedSet edges)
160     throws SearchException, NoSuchTableException 
161     {
162         if (logger.isDebugEnabled())
163         {
164             logger.debug("getNodes(type={}, node={}, conn={}, schema={}, metaData={}, edges={}) - start", 
165                     new Object[] {String.valueOf(type), node, conn, schema, metaData, edges});
166             logger.debug("Getting edges for node " + node);
167         }
168         
169         if (!(node instanceof String)) {
170             throw new IllegalArgumentException("node '" + node + "' should be a String, not a "
171                     + node.getClass().getName());
172         }
173         String tableName = (String) node;
174 
175     	QualifiedTableName qualifiedTableName = new QualifiedTableName(tableName, schema);
176     	schema = qualifiedTableName.getSchema();
177     	tableName = qualifiedTableName.getTable();
178         
179         ResultSet rs = null;
180         try {
181             IMetadataHandler metadataHandler = (IMetadataHandler) 
182                     this.connection.getConfig().getProperty(DatabaseConfig.PROPERTY_METADATA_HANDLER);
183             // Validate if the table exists
184             if(!metadataHandler.tableExists(metaData, schema, tableName))
185             {
186                 throw new NoSuchTableException("The table '"+tableName+"' does not exist in schema '"+schema+"'");
187             }
188 
189             switch (type) {
190             case IMPORT:
191                 rs = metaData.getImportedKeys(null, schema, tableName);
192                 break;
193             case EXPORT:
194                 rs = metaData.getExportedKeys(null, schema, tableName);
195                 break;
196             }
197             
198             
199             
200             DatabaseConfig dbConfig = this.connection.getConfig();
201             while (rs.next()) {
202                 int index = TABLENAME_INDEXES[type];
203                 int schemaindex = SCHEMANAME_INDEXES[type];
204                 String dependentTableName = rs.getString(index);
205                 String dependentSchemaName = rs.getString(schemaindex);
206                 String pkColumn = rs.getString( PK_INDEXES[type] );
207                 String fkColumn = rs.getString( FK_INDEXES[type] );
208 
209                 // set the schema in front if there is none ("SCHEMA.TABLE") - depending on the "qualified table names" feature
210             	tableName = new QualifiedTableName(tableName, schema).getQualifiedNameIfEnabled(dbConfig);
211             	dependentTableName = new QualifiedTableName(dependentTableName, dependentSchemaName).getQualifiedNameIfEnabled(dbConfig);
212                 
213                 IEdge edge = newEdge(rs, type, tableName, dependentTableName, fkColumn, pkColumn );
214                 if ( logger.isDebugEnabled() ) {
215                     logger.debug("Adding edge " + edge);
216                 }
217                 edges.add(edge);
218             }
219         } 
220         catch (SQLException e) {
221             throw new SearchException(e);
222         }
223         finally
224         {
225         	try {
226         		SQLHelper.close(rs);
227             } catch (SQLException e) {
228                 throw new SearchException(e);
229             }        		
230         }
231     }
232 
233 
234     /**
235      * Creates an edge representing a foreign key relationship between 2 tables.<br>
236      * @param rs database meta-data result set
237      * @param type type of relationship (IMPORT or EXPORT)
238      * @param from name of the table representing the 'from' node
239      * @param to name of the table representing the 'to' node
240      * @param fkColumn name of the foreign key column
241      * @param pkColumn name of the primary key column
242      * @return edge representing the relationship between the 2 tables, according to 
243      * the type
244      * @throws SearchException not thrown in this method (but might on sub-classes)
245      */
246     protected static ForeignKeyRelationshipEdge createFKEdge(ResultSet rs, int type, 
247             String from, String to, String fkColumn, String pkColumn)
248     throws SearchException {
249         if (logger.isDebugEnabled()) {
250             logger.debug("createFKEdge(rs={}, type={}, from={}, to={}, fkColumn={}, pkColumn={}) - start",
251                     new Object[] {rs, String.valueOf(type), from, to, fkColumn, pkColumn});
252         }
253 
254         return type == IMPORT ? 
255                 new ForeignKeyRelationshipEdge( from, to, fkColumn, pkColumn ) :
256                     new ForeignKeyRelationshipEdge( to, from, fkColumn, pkColumn );
257     }
258 
259 
260     /**
261      * This method can be overwritten by the sub-classes if they need to decorate
262      * the edge (for instance, providing an Edge that contains the primary and 
263      * foreign keys used).
264      * @param rs database meta-data result set
265      * @param type type of relationship (IMPORT or EXPORT)
266      * @param from name of the table representing the 'from' node
267      * @param to name of the table representing the 'to' node
268      * @param fkColumn name of the foreign key column
269      * @param pkColumn name of the primary key column
270      * @return edge representing the relationship between the 2 tables, according to 
271      * the type
272      * @throws SearchException not thrown in this method (but might on sub-classes)
273      */
274     protected IEdge newEdge(ResultSet rs, int type, String from, String to, String fkColumn, String pkColumn)
275     throws SearchException {
276         if (logger.isDebugEnabled()) {
277             logger.debug("newEdge(rs={}, type={}, from={}, to={}, fkColumn={}, pkColumn={}) - start",
278                     new Object[] {rs, String.valueOf(type), from, to, fkColumn, pkColumn});
279         }
280 
281         return createFKEdge( rs, type, from, to, fkColumn, pkColumn );
282     }
283 }