View Javadoc
1   /*
2    *
3    * The DbUnit Database Testing Framework
4    * Copyright (C)2002-2004, 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  package org.dbunit.dataset;
22  
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Comparator;
26  import java.util.List;
27  
28  import org.dbunit.dataset.filter.IColumnFilter;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  /**
33   * This class exclusively provides static methods that operate on {@link Column} objects.
34   * 
35   * @author gommma
36   * @version $Revision$ 
37   * @since 2.3.0
38   */
39  public class Columns 
40  {
41      /**
42       * Logger for this class
43       */
44      private static final Logger logger = LoggerFactory.getLogger(Columns.class);
45  
46      private static final ColumnComparator COLUMN_COMPARATOR = new ColumnComparator();
47  
48      private static final Column[] EMPTY_COLUMNS = new Column[0];
49  
50      
51      private Columns()
52      {
53      }
54      
55      
56  	/**
57       * Search and return the {@link Column}s from the specified column array that
58       * match one of the given <code>columnNames</code>.
59       * <br>
60       * Note that this method has a bad performance compared to {@link #findColumnsByName(String[], ITableMetaData)}
61       * because it iterates over all columns.
62       * 
63  	 * @param columnNames the names of the columns to search.
64  	 * @param columns the array of columns in which the <code>columnNames</code> will be searched.
65  	 * @return the column array which is empty if no column has been found or no 
66  	 * column names have been given
67  	 * @see #findColumnsByName(String[], ITableMetaData)
68  	 */
69  	public static Column[] getColumns(String[] columnNames, Column[] columns) {
70      	if (logger.isDebugEnabled())
71      		logger.debug("getColumns(columnNames={}, columns={}) - start",
72      				new Object[]{ columnNames, columns });
73  
74      	if (columnNames == null || columnNames.length == 0)
75          {
76              return EMPTY_COLUMNS;
77          }
78  
79          List resultList = new ArrayList();
80          for (int i = 0; i < columnNames.length; i++)
81          {
82              Column column = Columns.getColumn(columnNames[i], columns);
83              if (column != null)
84              {
85                  resultList.add(column);
86              }
87          }
88  
89          return (Column[])resultList.toArray(new Column[0]);
90  	}
91  
92      /**
93       * Searches for the given <code>columns</code> using only the {@link Column#getColumnName()} 
94       * in the given <code>tableMetaData</code>
95       * @param columnNames The column names that are searched in the given table metadata
96       * @param tableMetaData The table metadata in which the columns are searched by name
97       * @return The column objects from the given <code>tableMetaData</code>
98       * @throws NoSuchColumnException if the given column has not been found
99       * @throws DataSetException if something goes wrong when trying to retrieve the columns
100      */
101     public static Column[] findColumnsByName(String[] columnNames,
102             ITableMetaData tableMetaData) 
103     throws NoSuchColumnException, DataSetException 
104     {
105         logger.debug("findColumnsByName(columnNames={}, tableMetaData={}) - start", columnNames, tableMetaData);
106 
107         Column[] resultColumns = new Column[columnNames.length];
108         for (int i = 0; i < columnNames.length; i++) 
109         {
110             String sortColumn = columnNames[i];
111             int colIndex = tableMetaData.getColumnIndex(sortColumn);
112             resultColumns[i] = tableMetaData.getColumns()[colIndex];            
113         }
114         return resultColumns;
115     }
116 
117     /**
118      * Searches for the given <code>columns</code> using only the {@link Column#getColumnName()} 
119      * in the given <code>tableMetaData</code>
120      * @param columns The columns whose names are searched in the given table metadata
121      * @param tableMetaData The table metadata in which the columns are searched by name
122      * @return The column objects from the given <code>tableMetaData</code>
123      * @throws NoSuchColumnException if the given column has not been found
124      * @throws DataSetException if something goes wrong when trying to retrieve the columns
125      */
126     public static Column[] findColumnsByName(Column[] columns,
127             ITableMetaData tableMetaData) 
128     throws NoSuchColumnException, DataSetException 
129     {
130         logger.debug("findColumnsByName(columns={}, tableMetaData={}) - start", columns, tableMetaData);
131 
132         Column[] resultColumns = new Column[columns.length];
133         for (int i = 0; i < columns.length; i++) 
134         {
135             Column sortColumn = columns[i];
136             int colIndex = tableMetaData.getColumnIndex(sortColumn.getColumnName());
137             resultColumns[i] = tableMetaData.getColumns()[colIndex];            
138         }
139         return resultColumns;
140     }
141 
142     /**
143      * Search and return the specified column from the specified column array.
144      * <br>
145      * Note that this method has a bad performance compared to {@link ITableMetaData#getColumnIndex(String)}
146      * because it iterates over all columns.
147      *
148      * @param columnName the name of the column to search.
149 	 * @param columns the array of columns in which the <code>columnName</code> will be searched.
150      * @return the column or <code>null</code> if the column is not found
151      */
152     public static Column getColumn(String columnName, Column[] columns)
153     {
154         logger.debug("getColumn(columnName={}, columns={}) - start", columnName, columns);
155 
156         for (int i = 0; i < columns.length; i++)
157         {
158             Column column = columns[i];
159             if (columnName.equalsIgnoreCase(columns[i].getColumnName()))
160             {
161                 return column;
162             }
163         }
164 
165         return null;
166     }
167 
168     /**
169      * Search and return the specified column from the specified column array.
170      *
171      * @param columnName the name of the column to search.
172 	 * @param columns the array of columns in which the <code>columnName</code> will be searched.
173      * @param tableName The name of the table to which the column array belongs - 
174      * only needed for the exception message in case of a validation failure
175      * @return the valid column
176      * @throws NoSuchColumnException If no column exists with the given name
177      */
178     public static Column getColumnValidated(String columnName, Column[] columns, String tableName) 
179     throws NoSuchColumnException
180     {
181         if (logger.isDebugEnabled())
182             logger.debug("getColumn(columnName={}, columns={}, tableName={}) - start", 
183                 new Object[] {columnName, columns, tableName } );
184 
185         Column column = Columns.getColumn(columnName, columns);
186         if(column==null)
187         {
188             throw new NoSuchColumnException(tableName, columnName);
189         }
190         
191         return column;
192     }
193 
194     /**
195      * Search and return the columns from the specified column array which are
196      * accepted by the given {@link IColumnFilter}.
197      * @param tableName The name of the table which is needed for the filter invocation
198      * @param columns All available columns to which the filter will be applied
199      * @param columnFilter The column filter that is applied to the given <code>columns</code>
200      * @return The columns that are accepted by the given filter
201      */
202     public static Column[] getColumns(String tableName, Column[] columns,
203             IColumnFilter columnFilter)
204     {
205     	if (logger.isDebugEnabled())
206     		logger.debug("getColumns(tableName={}, columns={}, columnFilter={}) - start",
207     				new Object[]{ tableName, columns, columnFilter });
208 
209         List resultList = new ArrayList();
210         for (int i = 0; i < columns.length; i++)
211         {
212             Column column = columns[i];
213             if (columnFilter.accept(tableName, column))
214             {
215                 resultList.add(column);
216             }
217         }
218 
219         return (Column[])resultList.toArray(new Column[0]);
220     }
221 
222     /**
223      * Returns a sorted array of column objects
224      * 
225      * @param metaData The metaData needed to get the columns to be sorted
226      * @return The columns sorted by their column names, ignoring the case of the column names
227      * @throws DataSetException
228      */
229     public static Column[] getSortedColumns(ITableMetaData metaData)
230     throws DataSetException
231     {
232         logger.debug("getSortedColumns(metaData={}) - start", metaData);
233 
234         Column[] columns = metaData.getColumns();
235         Column[] sortColumns = new Column[columns.length];
236         System.arraycopy(columns, 0, sortColumns, 0, columns.length);
237         Arrays.sort(sortColumns, COLUMN_COMPARATOR);
238         return sortColumns;
239     }
240 
241     /**
242      * Returns the names of the given column objects as string array
243      * @param columns The column objects
244      * @return The names of the given column objects
245      * @since 2.4
246      */
247     public static String[] getColumnNames(Column[] columns) 
248     {
249         String[] result = new String[columns.length];
250         for (int i = 0; i < columns.length; i++) {
251             result[i] = columns[i].getColumnName();
252         }
253         return result;
254     }
255 
256     /**
257      * Creates a pretty string representation of the given column names
258      * @param columns The columns to be formatted
259      * @return The string representation of the given column names
260      */
261     public static String getColumnNamesAsString(Column[] columns)
262     {
263         logger.debug("getColumnNamesAsString(columns={}) - start", columns);
264 
265         String[] names = new String[columns.length];
266         for (int i = 0; i < columns.length; i++)
267         {
268             Column column = columns[i];
269             names[i] = column.getColumnName();
270         }
271         return Arrays.asList(names).toString();
272     }
273 
274     /**
275      * Merges the two arrays of columns so that all of the columns are available in the result array.
276      * The first array is considered as master and if a column with a specific name is available in 
277      * both arrays the one from the first array is used.
278      * @param referenceColumns reference columns treated as master columns during the merge
279      * @param columnsToMerge potentially new columns to be merged if they do not yet exist in the referenceColumns 
280      * @return Array of merged columns
281      */
282     public static Column[] mergeColumnsByName(Column[] referenceColumns, Column[] columnsToMerge) {
283         logger.debug("mergeColumnsByName(referenceColumns={}, columnsToMerge={}) - start", referenceColumns, columnsToMerge);
284 
285         List resultList = new ArrayList(Arrays.asList(referenceColumns));
286         List columnsToMergeNotInRefList = new ArrayList(Arrays.asList(columnsToMerge));
287         
288         // All columns that exist in the referenceColumns
289         for (int i = 0; i < referenceColumns.length; i++) {
290             Column refColumn = referenceColumns[i];
291             for (int k = 0; k < columnsToMerge.length; k++) {
292                 Column columnToMerge = columnsToMerge[k];
293                 // Check if this colToMerge exists in the refColumn
294                 if(columnToMerge.getColumnName().equals(refColumn.getColumnName())) {
295                     // We found the column in the refColumns - so no candidate for adding to the result list
296                     columnsToMergeNotInRefList.remove(columnToMerge);
297                     break;
298                 }
299             }
300         }
301         
302         // Add all "columnsToMerge" that have not been found in the referenceColumnList
303         resultList.addAll(columnsToMergeNotInRefList);
304         return (Column[]) resultList.toArray(new Column[]{});
305     }
306 
307     
308 	/**
309 	 * Returns the column difference of the two given {@link ITableMetaData} objects
310 	 * @param expectedMetaData
311 	 * @param actualMetaData
312 	 * @return The columns that differ in the both given {@link ITableMetaData} objects
313 	 * @throws DataSetException
314 	 */
315 	public static ColumnDiff getColumnDiff(ITableMetaData expectedMetaData,
316 			ITableMetaData actualMetaData) 
317 	throws DataSetException 
318 	{
319 		return new ColumnDiff(expectedMetaData, actualMetaData);
320 	}
321     
322 
323 	
324     //  ColumnComparator class
325     private static class ColumnComparator implements Comparator
326     {
327         /**
328          * Logger for this class
329          */
330         private static final Logger logger = LoggerFactory.getLogger(ColumnComparator.class);
331 
332         /**
333          * Compare columns by name ignoring case
334          * @see java.util.Comparator#compare(T, T)
335          */
336         public int compare(Object o1, Object o2)
337         {
338             logger.debug("compare(o1={}, o2={}) - start", o1, o2);
339 
340             Column column1 = (Column)o1;
341             Column column2 = (Column)o2;
342 
343             String columnName1 = column1.getColumnName();
344             String columnName2 = column2.getColumnName();
345             return columnName1.compareToIgnoreCase(columnName2);
346         }
347     }
348 
349     /**
350      * Describes the {@link Column}s that are different in two tables.
351      * @author gommma
352      * @version $Revision$
353      * @since 2.3.0
354      */
355     public static class ColumnDiff
356     {
357         /**
358          * Logger for this class
359          */
360         private static final Logger logger = LoggerFactory.getLogger(ColumnDiff.class);
361         /**
362          * String message that is returned when no difference has been found in the compared columns
363          */
364 		private static final String NO_DIFFERENCE = "no difference found";
365     	
366     	/**
367     	 * The columns that exist in the expected result but not in the actual
368     	 */
369     	private Column[] expected; 
370     	/**
371     	 * The columns that exist in the actual result but not in the expected
372     	 */
373     	private Column[] actual;
374     	private ITableMetaData expectedMetaData;
375     	private ITableMetaData actualMetaData;
376     	
377     	/**
378     	 * Creates the difference between the two metadata's columns
379     	 * @param expectedMetaData The metadata of the expected results table
380     	 * @param actualMetaData The metadata of the actual results table
381     	 * @throws DataSetException
382     	 */
383     	public ColumnDiff(ITableMetaData expectedMetaData,
384 				ITableMetaData actualMetaData) 
385     	throws DataSetException 
386 		{
387     		if (expectedMetaData == null) {
388 				throw new NullPointerException(
389 						"The parameter 'expectedMetaData' must not be null");
390 			}
391     		if (actualMetaData == null) {
392 				throw new NullPointerException(
393 						"The parameter 'actualMetaData' must not be null");
394 			}
395     		
396     		this.expectedMetaData = expectedMetaData;
397     		this.actualMetaData = actualMetaData;
398     		
399     		Column[] allExpectedCols = expectedMetaData.getColumns();
400     		Column[] allActualCols = actualMetaData.getColumns();
401     		
402     		// Get the columns that are missing on the actual side (walk through actual 
403     		// columns and look for them in the expected metadata)
404     		this.actual = findMissingColumnsIn(expectedMetaData, allActualCols);
405     		// Get the columns that are missing on the expected side (walk through expected 
406     		// columns and look for them in the actual metadata)
407     		this.expected = findMissingColumnsIn(actualMetaData, allExpectedCols);
408 		}
409     	
410     	/**
411     	 * Searches and returns all columns that are missing in the given {@link ITableMetaData} object
412     	 * @param metaDataToCheck The {@link ITableMetaData} in which the given columns should be searched
413     	 * @param columnsToSearch The columns to be searched in the given {@link ITableMetaData}
414     	 * @return Those {@link Column}s out of the columnsToSearch that have not been found in metaDataToCheck
415     	 * @throws DataSetException 
416     	 */
417     	private Column[] findMissingColumnsIn(ITableMetaData metaDataToCheck,
418 				Column[] columnsToSearch) throws DataSetException 
419     	{
420     		logger.debug("findMissingColumnsIn(metaDataToCheck={}, columnsToSearch={})", metaDataToCheck, columnsToSearch);
421     		
422     		List columnsNotFound = new ArrayList();
423     		for (int i = 0; i < columnsToSearch.length; i++) {
424     			try {
425     				metaDataToCheck.getColumnIndex(columnsToSearch[i].getColumnName());
426     			}
427     			catch(NoSuchColumnException e) {
428     				columnsNotFound.add(columnsToSearch[i]);
429     			}
430 			}
431     		
432     		Column[] result = (Column[]) columnsNotFound.toArray(new Column[]{});
433     		return result;
434 		}
435 
436     	/**
437     	 * @return <code>true</code> if there is a difference in the columns given in the constructor
438     	 */
439     	public boolean hasDifference()
440     	{
441     		return this.expected.length > 0 || this.actual.length > 0;
442     	}
443 
444 		/**
445     	 * @return The columns that exist in the expected result but not in the actual
446     	 */
447     	public Column[] getExpected() {
448 			return expected;
449 		}
450 
451 		/**
452 		 * @return The columns that exist in the actual result but not in the expected
453 		 */
454 		public Column[] getActual() {
455 			return actual;
456 		}
457 
458 		/**
459 		 * @return The value of {@link #getExpected()} as formatted string
460 		 * @see #getExpected()
461 		 */
462 		public String getExpectedAsString() {
463 			return Columns.getColumnNamesAsString(expected);
464 		}
465 
466 		/**
467 		 * @return The value of {@link #getActual()} as formatted string
468 		 * @see #getActual()
469 		 */
470 		public String getActualAsString() {
471 			return Columns.getColumnNamesAsString(actual);
472 		}
473 
474 		/**
475 		 * @return A pretty formatted message that can be used for user information
476 		 * @throws DataSetException
477 		 */
478 		public String getMessage() throws DataSetException 
479 		{
480 	        logger.debug("getMessage() - start");
481 
482 			if(!this.hasDifference())
483 			{
484 				return NO_DIFFERENCE;
485 			}
486 			else
487 			{
488 	    		Column[] allExpectedCols = expectedMetaData.getColumns();
489 	    		Column[] allActualCols = actualMetaData.getColumns();
490 	    		String expectedTableName = expectedMetaData.getTableName();
491 	
492 	    		String message;
493 	    		if(allExpectedCols.length != allActualCols.length) 
494 	    		{
495 	    			message = "column count (table=" + expectedTableName + ", " +
496 	    					"expectedColCount=" + allExpectedCols.length + ", actualColCount=" + allActualCols.length + ")";
497 	    		}
498 	    		else 
499 	    		{
500 	    			message = "column mismatch (table=" + expectedTableName + ")";
501 	    		}
502 	    		return message;
503 			}
504 		}
505 
506 //		/**
507 //		 * @return A pretty formatted message that shows up the difference
508 //		 */
509 //		private String toMessage()
510 //		{
511 //			StringBuffer sb = new StringBuffer();
512 //			sb.append("column-diffs (expected <-> actual): ");
513 //			if(this.hasDifference()) 
514 //			{
515 //				sb.append(getExpectedAsString());
516 //				sb.append(" <-> ");
517 //				sb.append(getActualAsString());
518 //			}
519 //			else
520 //			{
521 //				sb.append(NO_DIFFERENCE);
522 //			}
523 //			return sb.toString();
524 //		}
525 		
526 		public String toString()
527     	{
528     		StringBuffer sb = new StringBuffer();
529     		sb.append(getClass().getName()).append("[");
530     		sb.append("expected=").append(Arrays.asList(expected).toString());
531     		sb.append(", actual=").append(Arrays.asList(actual).toString());
532     		sb.append("]");
533     		return sb.toString();
534     	}
535 
536     }
537 
538 
539 }