001    /*
002     * Created on Feb 2, 2008
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
005     * the License. You may obtain a copy of the License at
006     *
007     * http://www.apache.org/licenses/LICENSE-2.0
008     *
009     * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
010     * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
011     * specific language governing permissions and limitations under the License.
012     *
013     * Copyright @2008-2010 the original author or authors.
014     */
015    package org.fest.swing.driver;
016    
017    import static org.fest.assertions.Assertions.assertThat;
018    import static org.fest.assertions.Fail.fail;
019    import static org.fest.swing.core.MouseButton.LEFT_BUTTON;
020    import static org.fest.swing.driver.CommonValidations.validateCellReader;
021    import static org.fest.swing.driver.CommonValidations.validateCellWriter;
022    import static org.fest.swing.driver.ComponentStateValidator.validateIsEnabledAndShowing;
023    import static org.fest.swing.driver.JTableCellEditableQuery.isCellEditable;
024    import static org.fest.swing.driver.JTableCellValidator.validateCellIndices;
025    import static org.fest.swing.driver.JTableCellValidator.validateIndices;
026    import static org.fest.swing.driver.JTableCellValidator.validateNotNull;
027    import static org.fest.swing.driver.JTableColumnCountQuery.columnCountOf;
028    import static org.fest.swing.driver.JTableContentsQuery.tableContents;
029    import static org.fest.swing.driver.JTableHasSelectionQuery.hasSelection;
030    import static org.fest.swing.driver.JTableHeaderQuery.tableHeader;
031    import static org.fest.swing.driver.JTableMatchingCellQuery.cellWithValue;
032    import static org.fest.swing.driver.JTableSingleRowCellSelectedQuery.isCellSelected;
033    import static org.fest.swing.driver.TextAssert.verifyThat;
034    import static org.fest.swing.edt.GuiActionRunner.execute;
035    import static org.fest.swing.exception.ActionFailedException.actionFailure;
036    import static org.fest.swing.query.JTableColumnByIdentifierQuery.columnIndexByIdentifier;
037    import static org.fest.swing.util.Arrays.equal;
038    import static org.fest.swing.util.Arrays.isEmptyIntArray;
039    import static org.fest.util.Arrays.format;
040    import static org.fest.util.Arrays.isEmpty;
041    import static org.fest.util.Strings.concat;
042    import static org.fest.util.Strings.quote;
043    
044    import java.awt.Color;
045    import java.awt.Component;
046    import java.awt.Font;
047    import java.awt.Point;
048    import java.util.regex.Pattern;
049    
050    import javax.swing.JPopupMenu;
051    import javax.swing.JTable;
052    import javax.swing.table.JTableHeader;
053    
054    import org.fest.assertions.Description;
055    import org.fest.swing.annotation.RunsInCurrentThread;
056    import org.fest.swing.annotation.RunsInEDT;
057    import org.fest.swing.cell.JTableCellReader;
058    import org.fest.swing.cell.JTableCellWriter;
059    import org.fest.swing.core.MouseButton;
060    import org.fest.swing.core.Robot;
061    import org.fest.swing.data.TableCell;
062    import org.fest.swing.data.TableCellFinder;
063    import org.fest.swing.edt.GuiQuery;
064    import org.fest.swing.edt.GuiTask;
065    import org.fest.swing.exception.ActionFailedException;
066    import org.fest.swing.exception.ComponentLookupException;
067    import org.fest.swing.util.Arrays;
068    import org.fest.swing.util.Pair;
069    import org.fest.swing.util.PatternTextMatcher;
070    import org.fest.swing.util.StringTextMatcher;
071    import org.fest.util.VisibleForTesting;
072    
073    /**
074     * Understands functional testing of <code>{@link JTable}</code>s:
075     * <ul>
076     * <li>user input simulation</li>
077     * <li>state verification</li>
078     * <li>property value query</li>
079     * </ul>
080     * This class is intended for internal use only. Please use the classes in the package
081     * <code>{@link org.fest.swing.fixture}</code> in your tests.
082     *
083     * @author Yvonne Wang
084     * @author Alex Ruiz
085     */
086    public class JTableDriver extends JComponentDriver {
087    
088      private static final String CONTENTS_PROPERTY = "contents";
089      private static final String EDITABLE_PROPERTY = "editable";
090      private static final String SELECTED_ROWS_PROPERTY = "selectedRows";
091      private static final String SELECTION_PROPERTY = "selection";
092      private static final String VALUE_PROPERTY = "value";
093    
094      private final JTableLocation location = new JTableLocation();
095      private JTableCellReader cellReader;
096      private JTableCellWriter cellWriter;
097    
098      /**
099       * Creates a new </code>{@link JTableDriver}</code>.
100       * @param robot the robot to use to simulate user events.
101       */
102      public JTableDriver(Robot robot) {
103        super(robot);
104        cellReader(new BasicJTableCellReader());
105        cellWriter(new BasicJTableCellWriter(robot));
106      }
107    
108      /**
109       * Returns the <code>{@link JTableHeader}</code> of the given <code>{@link JTable}</code>.
110       * @param table the given <code>JTable</code>.
111       * @return the <code>JTableHeader</code> of the given <code>JTable</code>.
112       */
113      @RunsInEDT
114      public JTableHeader tableHeaderOf(JTable table) {
115        return tableHeader(table);
116      }
117    
118      /**
119       * Returns the <code>String</code> representation of the value of the selected cell, using this driver's
120       * <code>{@link JTableCellReader}</code>.
121       * @param table the target <code>JTable</code>.
122       * @return the <code>String</code> representation of the value of the selected cell.
123       * @see #cellReader(JTableCellReader)
124       */
125      @RunsInEDT
126      public String selectionValue(JTable table) {
127        return selectionValue(table, cellReader);
128      }
129    
130      @RunsInEDT
131      private static String selectionValue(final JTable table, final JTableCellReader cellReader) {
132        return execute(new GuiQuery<String>() {
133          protected String executeInEDT() {
134            if (table.getSelectedRowCount() == 0) return null;
135            return cellReader.valueAt(table, table.getSelectedRow(), table.getSelectedColumn());
136          }
137        });
138      }
139    
140      /**
141       * Returns a cell from the given <code>{@link JTable}</code> using the given cell finder.
142       * @param table the target <code>JTable</code>.
143       * @param cellFinder knows how to find a cell.
144       * @return the found cell, if any.
145       * @throws NullPointerException if <code>cellFinder</code> is <code>null</code>.
146       * @throws IndexOutOfBoundsException if the row or column indices in the found cell are out of bounds.
147       * @throws ActionFailedException if a matching cell could not be found.
148       */
149      @RunsInEDT
150      public TableCell cell(JTable table, TableCellFinder cellFinder) {
151        if (cellFinder == null) throw new NullPointerException("The cell finder to use should not be null");
152        TableCell cell = cellFinder.findCell(table, cellReader);
153        validateCellIndices(table, cell);
154        return cell;
155      }
156    
157      /**
158       * Returns a cell from the given <code>{@link JTable}</code> whose value matches the given one.
159       * @param table the target <code>JTable</code>.
160       * @param value the value of the cell to look for. It can be a regular expression.
161       * @return a cell from the given <code>JTable</code> whose value matches the given one.
162       * @throws ActionFailedException if a cell with a matching value cannot be found.
163       */
164      @RunsInEDT
165      public TableCell cell(JTable table, String value) {
166        return cellWithValue(table, new StringTextMatcher(value), cellReader);
167      }
168    
169      /**
170       * Returns a cell from the given <code>{@link JTable}</code> whose value matches the given regular expression pattern.
171       * @param table the target <code>JTable</code>.
172       * @param pattern the regular expression pattern to match
173       * @return a cell from the given <code>JTable</code> whose value matches the given one.
174       * @throws NullPointerException if the given regular expression is <code>null</code>.
175       * @throws ActionFailedException if a cell with a matching value cannot be found.
176       * @since 1.2
177       */
178      @RunsInEDT
179      public TableCell cell(JTable table, Pattern pattern) {
180        return cellWithValue(table, new PatternTextMatcher(pattern), cellReader);
181      }
182    
183      /**
184       * Returns the <code>String</code> representation of the value at the given cell, using this driver's
185       * <code>{@link JTableCellReader}</code>.
186       * @param table the target <code>JTable</code>.
187       * @param cell the table cell.
188       * @return the <code>String</code> representation of the value at the given cell.
189       * @throws NullPointerException if the cell is <code>null</code>.
190       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
191       * @see #cellReader(JTableCellReader)
192       */
193      @RunsInEDT
194      public String value(JTable table, TableCell cell) {
195        validateNotNull(cell);
196        return cellValue(table, cell, cellReader);
197      }
198    
199      @RunsInEDT
200      private static String cellValue(final JTable table, final TableCell cell, final JTableCellReader cellReader) {
201        return execute(new GuiQuery<String>() {
202          protected String executeInEDT() {
203            validateCellIndices(table, cell);
204            return cellReader.valueAt(table, cell.row, cell.column);
205          }
206        });
207      }
208    
209      /**
210       * Returns the <code>String</code> representation of the value at the given row and column, using this driver's
211       * <code>{@link JTableCellReader}</code>.
212       * @param table the target <code>JTable</code>.
213       * @param row the given row.
214       * @param column the given column.
215       * @return the <code>String</code> representation of the value at the given row and column.
216       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
217       * @see #cellReader(JTableCellReader)
218       */
219      @RunsInEDT
220      public String value(JTable table, int row, int column) {
221        return cellValue(table, row, column, cellReader);
222      }
223    
224      @RunsInEDT
225      private static String cellValue(final JTable table, final int row, final int column,
226          final JTableCellReader cellReader) {
227        return execute(new GuiQuery<String>() {
228          protected String executeInEDT() {
229            validateIndices(table, row, column);
230            return cellReader.valueAt(table, row, column);
231          }
232        });
233      }
234    
235      /**
236       * Returns the font of the given table cell.
237       * @param table the target <code>JTable</code>.
238       * @param cell the table cell.
239       * @return the font of the given table cell.
240       * @throws NullPointerException if the cell is <code>null</code>.
241       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
242       */
243      @RunsInEDT
244      public Font font(JTable table, TableCell cell) {
245        validateNotNull(cell);
246        return cellFont(table, cell, cellReader);
247      }
248    
249      @RunsInEDT
250      private static Font cellFont(final JTable table, final TableCell cell, final JTableCellReader cellReader) {
251        return execute(new GuiQuery<Font>() {
252          protected Font executeInEDT() {
253            validateCellIndices(table, cell);
254            return cellReader.fontAt(table, cell.row, cell.column);
255          }
256        });
257      }
258    
259      /**
260       * Returns the background color of the given table cell.
261       * @param table the target <code>JTable</code>.
262       * @param cell the table cell.
263       * @return the background color of the given table cell.
264       * @throws ActionFailedException if the cell is <code>null</code>.
265       * @throws ActionFailedException if any of the indices (row and column) is out of bounds.
266       */
267      @RunsInEDT
268      public Color background(JTable table, TableCell cell) {
269        validateNotNull(cell);
270        return cellBackground(table, cell, cellReader);
271      }
272    
273      @RunsInEDT
274      private static Color cellBackground(final JTable table, final TableCell cell, final JTableCellReader cellReader) {
275        return execute(new GuiQuery<Color>() {
276          protected Color executeInEDT() {
277            validateCellIndices(table, cell);
278            return cellReader.backgroundAt(table, cell.row, cell.column);
279          }
280        });
281      }
282    
283      /**
284       * Returns the foreground color of the given table cell.
285       * @param table the target <code>JTable</code>.
286       * @param cell the table cell.
287       * @return the foreground color of the given table cell.
288       * @throws NullPointerException if the cell is <code>null</code>.
289       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
290       */
291      @RunsInEDT
292      public Color foreground(JTable table, TableCell cell) {
293        validateNotNull(cell);
294        return cellForeground(table, cell, cellReader);
295      }
296    
297      @RunsInEDT
298      private static Color cellForeground(final JTable table, final TableCell cell, final JTableCellReader cellReader) {
299        return execute(new GuiQuery<Color>() {
300          protected Color executeInEDT() {
301            validateCellIndices(table, cell);
302            return cellReader.foregroundAt(table, cell.row, cell.column);
303          }
304        });
305      }
306    
307      /**
308       * Selects the given cells of the <code>{@link JTable}</code>.
309       * @param table the target <code>JTable</code>.
310       * @param cells the cells to select.
311       * @throws NullPointerException if <code>cells</code> is <code>null</code> or empty.
312       * @throws IllegalArgumentException if <code>cells</code> is <code>null</code> or empty.
313       * @throws IllegalStateException if the <code>JTable</code> is disabled.
314       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
315       * @throws NullPointerException if any element in <code>cells</code> is <code>null</code>.
316       * @throws IndexOutOfBoundsException if any of the indices of any of the <code>cells</code> are out of bounds.
317       */
318      public void selectCells(final JTable table, final TableCell[] cells) {
319        validateCellsToSelect(cells);
320        new MultipleSelectionTemplate(robot) {
321          int elementCount() {
322            return cells.length;
323          }
324    
325          void selectElement(int index) {
326            selectCell(table, cells[index]);
327          }
328        }.multiSelect();
329      }
330    
331      private void validateCellsToSelect(final TableCell[] cells) {
332        if (cells == null)  throw new NullPointerException("Array of table cells to select should not be null");
333        if (isEmpty(cells)) throw new IllegalArgumentException("Array of table cells to select should not be empty");
334      }
335    
336      /**
337       * Verifies that the <code>{@link JTable}</code> does not have any selection.
338       * @param table the target <code>JTable</code>.
339       * @throws AssertionError is the <code>JTable</code> has a selection.
340       */
341      @RunsInEDT
342      public void requireNoSelection(JTable table) {
343        assertNoSelection(table);
344      }
345    
346      @RunsInEDT
347      private static void assertNoSelection(final JTable table) {
348        execute(new GuiTask() {
349          protected void executeInEDT() {
350            if (!hasSelection(table)) return;
351            String message = concat("[", propertyName(table, SELECTION_PROPERTY).value(),
352                "] expected no selection but was:<rows=", format(selectedRowsOf(table)), ", columns=",
353                format(table.getSelectedColumns()), ">");
354            fail(message);
355          }
356        });
357      }
358    
359      /**
360       * Selects the given cell, if it is not selected already.
361       * @param table the target <code>JTable</code>.
362       * @param cell the cell to select.
363       * @throws NullPointerException if the cell is <code>null</code>.
364       * @throws IllegalStateException if the <code>JTable</code> is disabled.
365       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
366       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
367       */
368      @RunsInEDT
369      public void selectCell(JTable table, TableCell cell) {
370        validateNotNull(cell);
371        selectCell(table, cell.row, cell.column);
372      }
373    
374      /**
375       * Clicks the given cell, using the specified mouse button, the given number of times.
376       * @param table the target <code>JTable</code>.
377       * @param cell the table cell.
378       * @param mouseButton the mouse button to use.
379       * @param times the number of times to click the cell.
380       * @throws NullPointerException if the cell is <code>null</code>.
381       * @throws IllegalStateException if the <code>JTable</code> is disabled.
382       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
383       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
384       */
385      @RunsInEDT
386      public void click(JTable table, TableCell cell, MouseButton mouseButton, int times) {
387        if (times <= 0)
388          throw new IllegalArgumentException("The number of times to click a cell should be greater than zero");
389        Point pointAtCell = scrollToPointAtCell(table, cell, location);
390        robot.click(table, pointAtCell, mouseButton, times);
391      }
392    
393      /**
394       * Starts a drag operation at the location of the given table cell.
395       * @param table the target <code>JTable</code>.
396       * @param cell the table cell.
397       * @throws NullPointerException if the cell is <code>null</code>.
398       * @throws IllegalStateException if the <code>JTable</code> is disabled.
399       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
400       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
401       */
402      @RunsInEDT
403      public void drag(JTable table, TableCell cell) {
404        Point pointAtCell = scrollToPointAtCell(table, cell, location);
405        drag(table, pointAtCell);
406      }
407    
408      /**
409       * Starts a drop operation at the location of the given table cell.
410       * @param table the target <code>JTable</code>.
411       * @param cell the table cell.
412       * @throws NullPointerException if the cell is <code>null</code>.
413       * @throws IllegalStateException if the <code>JTable</code> is disabled.
414       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
415       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
416       */
417      @RunsInEDT
418      public void drop(JTable table, TableCell cell) {
419        Point pointAtCell = scrollToPointAtCell(table, cell, location);
420        drop(table, pointAtCell);
421      }
422    
423      /**
424       * Shows a pop-up menu at the given table cell.
425       * @param table the target <code>JTable</code>.
426       * @param cell the table cell.
427       * @return the displayed pop-up menu.
428       * @throws NullPointerException if the cell is <code>null</code>.
429       * @throws IllegalStateException if the <code>JTable</code> is disabled.
430       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
431       * @throws ComponentLookupException if a pop-up menu cannot be found.
432       */
433      @RunsInEDT
434      public JPopupMenu showPopupMenuAt(JTable table, TableCell cell) {
435        Point pointAtCell = scrollToPointAtCell(table, cell, location);
436        return robot.showPopupMenu(table, pointAtCell);
437      }
438    
439      @RunsInEDT
440      private static Point scrollToPointAtCell(final JTable table, final TableCell cell, final JTableLocation location) {
441        validateNotNull(cell);
442        return execute(new GuiQuery<Point>() {
443          protected Point executeInEDT() {
444            scrollToCell(table, cell, location);
445            return location.pointAt(table, cell.row, cell.column);
446          }
447        });
448      }
449    
450      @RunsInCurrentThread
451      private static void scrollToCell(final JTable table, final TableCell cell, final JTableLocation location) {
452        validateIsEnabledAndShowing(table);
453        validateCellIndices(table, cell);
454        table.scrollRectToVisible(location.cellBounds(table, cell));
455      }
456    
457      /**
458       * Converts the given table cell into a coordinate pair.
459       * @param table the target <code>JTable</code>.
460       * @param cell the table cell.
461       * @return the coordinates of the given row and column.
462       * @throws NullPointerException if the cell is <code>null</code>.
463       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
464       */
465      @RunsInEDT
466      public Point pointAt(JTable table, TableCell cell) {
467        return pointAtCell(table, cell, location);
468      }
469    
470      @RunsInEDT
471      private static Point pointAtCell(final JTable table, final TableCell cell, final JTableLocation location) {
472        return execute(new GuiQuery<Point>() {
473          protected Point executeInEDT() {
474            validateCellIndices(table, cell);
475            return location.pointAt(table, cell.row, cell.column);
476          }
477        });
478      }
479    
480      /**
481       * Asserts that the <code>String</code> representation of the cell values in the <code>{@link JTable}</code> is
482       * equal to the given <code>String</code> array. This method uses this driver's
483       * <code>{@link JTableCellReader}</code> to read the values of the table cells as <code>String</code>s.
484       * @param table the target <code>JTable</code>.
485       * @param contents the expected <code>String</code> representation of the cell values in the <code>JTable</code>.
486       * @see #cellReader(JTableCellReader)
487       */
488      @RunsInEDT
489      public void requireContents(JTable table, String[][] contents) {
490        String[][] actual = contents(table);
491    
492        if (!equal(actual, contents))
493          failNotEqual(actual, contents, propertyName(table, CONTENTS_PROPERTY));
494      }
495    
496      private static void failNotEqual(String[][] actual, String[][] expected, Description description) {
497        String descriptionValue = description != null ? description.value() : null;
498        String message = descriptionValue == null ? "" : concat("[", descriptionValue, "]");
499        fail(concat(message, " expected:<", Arrays.format(expected), "> but was:<", Arrays.format(actual), ">"));
500      }
501    
502      /**
503       * Returns the <code>String</code> representation of the cells in the <code>{@link JTable}</code>, using this
504       * driver's <code>{@link JTableCellReader}</code>.
505       * @param table the target <code>JTable</code>.
506       * @return the <code>String</code> representation of the cells in the <code>JTable</code>.
507       * @see #cellReader(JTableCellReader)
508       */
509      @RunsInEDT
510      public String[][] contents(JTable table) {
511        return tableContents(table, cellReader);
512      }
513    
514      /**
515       * Asserts that the value of the given cell matches the given value.
516       * @param table the target <code>JTable</code>.
517       * @param cell the given table cell.
518       * @param value the expected value. It can be a regular expression.
519       * @throws NullPointerException if the cell is <code>null</code>.
520       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
521       * @throws AssertionError if the value of the given cell does not match the given value.
522       */
523      @RunsInEDT
524      public void requireCellValue(JTable table, TableCell cell, String value) {
525        verifyThat(value(table, cell)).as(cellValueProperty(table, cell)).isEqualOrMatches(value);
526      }
527    
528      /**
529       * Asserts that the value of the given cell matches the given regular expression pattern.
530       * @param table the target <code>JTable</code>.
531       * @param cell the given table cell.
532       * @param pattern the regular expression pattern to match.
533       * @throws NullPointerException if the cell is <code>null</code>.
534       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
535       * @throws NullPointerException if the given regular expression pattern is <code>null</code>.
536       * @throws AssertionError if the value of the given cell does not match the given regular expression pattern.
537       * @since 1.2
538       */
539      @RunsInEDT
540      public void requireCellValue(JTable table, TableCell cell, Pattern pattern) {
541        verifyThat(value(table, cell)).as(cellValueProperty(table, cell)).matches(pattern);
542      }
543    
544      @RunsInEDT
545      private Description cellValueProperty(JTable table, TableCell cell) {
546        return cellProperty(table, concat(VALUE_PROPERTY, " ", cell));
547      }
548    
549      /**
550       * Enters the given value in the given cell of the <code>{@link JTable}</code>, using this driver's
551       * <code>{@link JTableCellWriter}</code>.
552       * @param table the target <code>JTable</code>.
553       * @param cell the given cell.
554       * @param value the given value.
555       * @throws NullPointerException if the cell is <code>null</code>.
556       * @throws IllegalStateException if the <code>JTable</code> is disabled.
557       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
558       * @throws IllegalStateException if the <code>JTable</code> cell is not editable.
559       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
560       * @throws ActionFailedException if this driver's <code>JTableCellValueReader</code> is unable to enter the given
561       * value.
562       * @see #cellWriter(JTableCellWriter)
563       */
564      @RunsInEDT
565      public void enterValueInCell(JTable table, TableCell cell, String value) {
566        validateNotNull(cell);
567        cellWriter.enterValue(table, cell.row, cell.column, value);
568      }
569    
570      /**
571       * Asserts that the given table cell is editable.
572       * @param table the target <code>JTable</code>.
573       * @param cell the given table cell.
574       * @throws NullPointerException if the cell is <code>null</code>.
575       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
576       * @throws AssertionError if the given table cell is not editable.
577       */
578      @RunsInEDT
579      public void requireEditable(JTable table, TableCell cell) {
580        requireEditableEqualTo(table, cell, true);
581      }
582    
583      /**
584       * Asserts that the given table cell is not editable.
585       * @param table the target <code>JTable</code>.
586       * @param cell the given table cell.
587       * @throws NullPointerException if the cell is <code>null</code>.
588       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
589       * @throws AssertionError if the given table cell is editable.
590       */
591      @RunsInEDT
592      public void requireNotEditable(JTable table, TableCell cell) {
593        requireEditableEqualTo(table, cell, false);
594      }
595    
596      @RunsInEDT
597      private static void requireEditableEqualTo(final JTable table, final TableCell cell, boolean editable) {
598        validateNotNull(cell);
599        boolean cellEditable = execute(new GuiQuery<Boolean>() {
600          protected Boolean executeInEDT() {
601            return isCellEditable(table, cell);
602          }
603        });
604        assertThat(cellEditable).as(cellProperty(table, concat(EDITABLE_PROPERTY, " ", cell))).isEqualTo(editable);
605      }
606    
607      @RunsInEDT
608      private static Description cellProperty(JTable table, String propertyName) {
609        return propertyName(table, propertyName);
610      }
611    
612      /**
613       * Returns the editor in the given cell of the <code>{@link JTable}</code>, using this driver's
614       * <code>{@link JTableCellWriter}</code>.
615       * @param table the target <code>JTable</code>.
616       * @param cell the given cell.
617       * @return the editor in the given cell of the <code>JTable</code>.
618       * @throws NullPointerException if the cell is <code>null</code>.
619       * @throws IllegalStateException if the <code>JTable</code> cell is not editable.
620       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
621       * @see #cellWriter(JTableCellWriter)
622       */
623      @RunsInEDT
624      public Component cellEditor(JTable table, TableCell cell) {
625        validateNotNull(cell);
626        return cellWriter.editorForCell(table, cell.row, cell.column);
627      }
628    
629      /**
630       * Starts editing the given cell of the <code>{@link JTable}</code>, using this driver's
631       * <code>{@link JTableCellWriter}</code>. This method should be called before manipulating the
632       * <code>{@link Component}</code> returned by <code>{@link #cellEditor(JTable, TableCell)}</code>.
633       * @param table the target <code>JTable</code>.
634       * @param cell the given cell.
635       * @throws NullPointerException if the cell is <code>null</code>.
636       * @throws IllegalStateException if the <code>JTable</code> is disabled.
637       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
638       * @throws IllegalStateException if the <code>JTable</code> cell is not editable.
639       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
640       * @throws ActionFailedException if this writer is unable to handle the underlying cell editor.
641       * @see #cellWriter(JTableCellWriter)
642       */
643      @RunsInEDT
644      public void startCellEditing(JTable table, TableCell cell) {
645        validateNotNull(cell);
646        cellWriter.startCellEditing(table, cell.row, cell.column);
647      }
648    
649      /**
650       * Stops editing the given cell of the <code>{@link JTable}</code>, using this driver's
651       * <code>{@link JTableCellWriter}</code>. This method should be called after manipulating the
652       * <code>{@link Component}</code> returned by <code>{@link #cellEditor(JTable, TableCell)}</code>.
653       * @param table the target <code>JTable</code>.
654       * @param cell the given cell.
655       * @throws NullPointerException if the cell is <code>null</code>.
656       * @throws IllegalStateException if the <code>JTable</code> is disabled.
657       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
658       * @throws IllegalStateException if the <code>JTable</code> cell is not editable.
659       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
660       * @throws ActionFailedException if this writer is unable to handle the underlying cell editor.
661       * @see #cellWriter(JTableCellWriter)
662       */
663      @RunsInEDT
664      public void stopCellEditing(JTable table, TableCell cell) {
665        validateNotNull(cell);
666        cellWriter.stopCellEditing(table, cell.row, cell.column);
667      }
668    
669      /**
670       * Cancels editing the given cell of the <code>{@link JTable}</code>, using this driver's
671       * <code>{@link JTableCellWriter}</code>. This method should be called after manipulating the
672       * <code>{@link Component}</code> returned by <code>{@link #cellEditor(JTable, TableCell)}</code>.
673       * @param table the target <code>JTable</code>.
674       * @param cell the given cell.
675       * @throws NullPointerException if the cell is <code>null</code>.
676       * @throws IllegalStateException if the <code>JTable</code> is disabled.
677       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
678       * @throws IllegalStateException if the <code>JTable</code> cell is not editable.
679       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
680       * @throws ActionFailedException if this writer is unable to handle the underlying cell editor.
681       * @see #cellWriter(JTableCellWriter)
682       */
683      @RunsInEDT
684      public void cancelCellEditing(JTable table, TableCell cell) {
685        validateNotNull(cell);
686        cellWriter.cancelCellEditing(table, cell.row, cell.column);
687      }
688    
689      /**
690       * Validates that the given table cell is non <code>null</code> and its indices are not out of bounds.
691       * @param table the target <code>JTable</code>.
692       * @param cell to validate.
693       * @throws NullPointerException if the cell is <code>null</code>.
694       * @throws IndexOutOfBoundsException if any of the indices (row and column) is out of bounds.
695       */
696      @RunsInEDT
697      public void validate(JTable table, TableCell cell) {
698        validateCellIndexBounds(table, cell);
699      }
700    
701      private static void validateCellIndexBounds(final JTable table, final TableCell cell) {
702        execute(new GuiTask() {
703          protected void executeInEDT() {
704            validateCellIndices(table, cell);
705          }
706        });
707      }
708    
709      /**
710       * Updates the implementation of <code>{@link JTableCellReader}</code> to use when comparing internal values of a
711       * <code>{@link JTable}</code> and the values expected in a test.
712       * @param newCellReader the new <code>JTableCellValueReader</code> to use.
713       * @throws NullPointerException if <code>newCellReader</code> is <code>null</code>.
714       */
715      public void cellReader(JTableCellReader newCellReader) {
716        validateCellReader(newCellReader);
717        cellReader = newCellReader;
718      }
719    
720      /**
721       * Updates the implementation of <code>{@link JTableCellWriter}</code> to use to edit cell values in a
722       * <code>{@link JTable}</code>.
723       * @param newCellWriter the new <code>JTableCellWriter</code> to use.
724       * @throws NullPointerException if <code>newCellWriter</code> is <code>null</code>.
725       */
726      public void cellWriter(JTableCellWriter newCellWriter) {
727        validateCellWriter(newCellWriter);
728        cellWriter = newCellWriter;
729      }
730    
731      /**
732       * Returns the number of rows that can be shown in the given <code>{@link JTable}</code>, given unlimited space.
733       * @param table the target <code>JTable</code>.
734       * @return the number of rows shown in the given <code>JTable</code>.
735       * @see JTable#getRowCount()
736       */
737      @RunsInEDT
738      public int rowCountOf(JTable table) {
739        return JTableRowCountQuery.rowCountOf(table);
740      }
741    
742      /**
743       * Returns the index of the column in the given <code>{@link JTable}</code> whose id matches the given one.
744       * @param table the target <code>JTable</code>.
745       * @param columnId the id of the column to look for.
746       * @return the index of the column whose id matches the given one.
747       * @throws ActionFailedException if a column with a matching id could not be found.
748       */
749      @RunsInEDT
750      public int columnIndex(JTable table, Object columnId) {
751        return findColumnIndex(table, columnId);
752      }
753    
754      @RunsInEDT
755      private static int findColumnIndex(final JTable table, final Object columnId) {
756        return execute(new GuiQuery<Integer>() {
757          protected Integer executeInEDT() {
758            int index = columnIndexByIdentifier(table, columnId);
759            if (index < 0) failColumnIndexNotFound(columnId);
760            return index;
761          }
762        });
763      }
764    
765      private static ActionFailedException failColumnIndexNotFound(Object columnId) {
766        throw actionFailure(concat("Unable to find a column with id ", quote(columnId)));
767      }
768    
769      /**
770       * Asserts that the given <code>{@link JTable}</code> has the given number of rows.
771       * @param table the target <code>JTable</code>.
772       * @param rowCount the expected number of rows.
773       * @throws AssertionError if the given <code>JTable</code> does not have the given number of rows.
774       */
775      @RunsInEDT
776      public void requireRowCount(JTable table, int rowCount) {
777        assertThat(rowCountOf(table)).as(propertyName(table, "rowCount")).isEqualTo(rowCount);
778      }
779    
780      /**
781       * Asserts that the given <code>{@link JTable}</code> has the given number of columns.
782       * @param table the target <code>JTable</code>.
783       * @param columnCount the expected number of columns.
784       * @throws AssertionError if the given <code>JTable</code> does not have the given number of columns.
785       */
786      @RunsInEDT
787      public void requireColumnCount(JTable table, int columnCount) {
788        assertThat(columnCountOf(table)).as(propertyName(table, "columnCount")).isEqualTo(columnCount);
789      }
790    
791      /**
792       * Simulates a user selecting the given rows in the given <code>{@link JTable}</code>.
793       * @param table the target <code>JTable</code>.
794       * @param rows the indices of the row to select.
795       * @throws NullPointerException if the given array of indices is <code>null</code>.
796       * @throws IllegalArgumentException if the given array of indices is empty.
797       * @throws IllegalStateException if the <code>JTable</code> is disabled.
798       * @throws IllegalStateException if the <code>JTable</code> is not showing on the screen.
799       * @throws IndexOutOfBoundsException if any of the given indices is negative, or equal to or greater than the number
800       * of rows in the <code>JTable</code>.
801       * @since 1.2
802       */
803      @RunsInEDT
804      public void selectRows(final JTable table, final int... rows) {
805        if (rows == null) throw new NullPointerException("The array of row indices should not be null");
806        if (isEmptyIntArray(rows)) throw new IllegalArgumentException("The array of row indices should not be empty");
807        new MultipleSelectionTemplate(robot) {
808          int elementCount() {
809            return rows.length;
810          }
811    
812          void selectElement(int index) {
813            selectCell(table, rows[index], 0);
814          }
815        }.multiSelect();
816      }
817    
818      @RunsInEDT
819      private void selectCell(JTable table, int row, int column) {
820        Pair<Boolean, Point> cellSelectionInfo = cellSelectionInfo(table, row, column, location);
821        if (cellSelectionInfo.i) return; // cell already selected
822        robot.click(table, cellSelectionInfo.ii, LEFT_BUTTON, 1);
823      }
824    
825      @RunsInEDT
826      private static Pair<Boolean, Point> cellSelectionInfo(final JTable table, final int row, final int column,
827          final JTableLocation location) {
828        return execute(new GuiQuery<Pair<Boolean, Point>>() {
829          protected Pair<Boolean, Point> executeInEDT() {
830            if (isCellSelected(table, row, column)) return new Pair<Boolean, Point>(true, null);
831            scrollToCell(table, row, column, location);
832            Point pointAtCell = location.pointAt(table, row, column);
833            return new Pair<Boolean, Point>(false, pointAtCell);
834          }
835        });
836      }
837    
838      @RunsInCurrentThread
839      private static void scrollToCell(final JTable table, final int row, final int column, final JTableLocation location) {
840        validateIsEnabledAndShowing(table);
841        validateIndices(table, row, column);
842        table.scrollRectToVisible(location.cellBounds(table, row, column));
843      }
844    
845      /**
846       * Asserts that the set of selected rows in the given <code>{@link JTable}</code> contains to the given row indices.
847       * @param table the target <code>JTable</code>.
848       * @param rows the indices of the rows expected to be selected.
849       * @throws AssertionError if the sets of selected rows in the given <code>JTable</code> (if any) do not contain the
850       * given row indices.
851       * @since 1.2
852       */
853      @RunsInEDT
854      public void requireSelectedRows(JTable table, int... rows) {
855        int[] selectedRows = selectedRowsOf(table);
856        assertThat(selectedRows).as(propertyName(table, SELECTED_ROWS_PROPERTY)).contains(rows);
857      }
858    
859      @RunsInEDT
860      private static int[] selectedRowsOf(final JTable table) {
861        return execute(new GuiQuery<int[]>() {
862          protected int[] executeInEDT() {
863            return table.getSelectedRows();
864          }
865        });
866      }
867    
868      @VisibleForTesting
869      JTableCellReader cellReader() { return cellReader; }
870    }