/** 
 * Copyright 2007 Dr. Matthias Laux 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
 
import java.util.ArrayList; 
import java.util.HashMap; 
import java.util.List; 
import java.util.Map; 
import java.util.Set; 
import java.util.TreeSet; 
 
/** 
 * This is the central class for handling data for HTML tables. Effectively, instance of 
 * this class are Java object representations of the HTML table structure, and the goal 
 * is that instances of this class hold all the data that is required for the expected 
 * HTML table visual layout once the table instance is merged with a Velocity template. 
 * 
 * A table has a logical row and column numbering that starts at row <code>row0</code> and column 
 * <code>col0</code> in the upper left corner. Indices run from <code>row0</code> 
 * to <code>rowNumber - 1</code> and from 
 * <code>col0</code> to <code>colNumber - 1</code>, respectively. 
 */ 
 
public class Table { 
   
  private static Cell defaultCell = new Cell("", 1, 1); 
   
  private Cell[][]    cells      = null; 
  private int         rowNumber  = 0; 
  private int         row0       = 0; 
  private int         rowEnd     = 0; 
  private int         colNumber  = 0; 
  private int         col0       = 0; 
  private int         colEnd     = 0; 
  private boolean[][] visible    = null; 
  private boolean[][] def        = null;  // Mark whether a cell contains the default cell 
   
  private Map<BoundaryLocation, BoundaryCondition> boundaryConditions = 
    new HashMap<BoundaryLocation, BoundaryCondition>(); 
   
  /** 
   * Constructor for a table where the logical indexes for rows and columns start at 0 
   * 
   * @param rowNumber Number of rows for the table 
   * @param colNumber Number of columns for the table 
   */ 
   
  public Table(int rowNumber, int colNumber) { 
    this(0, 0, rowNumber, colNumber); 
  } 
   
  /** 
   * Constructor for a table 
   * 
   * @param row0      First logical index at upper edge of the table 
   * @param col0      First logical index at left edge of the table 
   * @param rowNumber Number of rows for the table 
   * @param colNumber Number of columns for the table 
   */ 
   
  public Table(int row0, int col0, int rowNumber, int colNumber) { 
    if (rowNumber < 1) { 
      throw new IllegalArgumentException("rowNumber must be larger than 0"); 
    } 
    if (colNumber < 1) { 
      throw new IllegalArgumentException("colNumber must be larger than 0"); 
    } 
     
    this.rowNumber = rowNumber; 
    this.colNumber = colNumber; 
    this.row0      = row0; 
    this.col0      = col0; 
     
    rowEnd = row0 + rowNumber - 1;  // Helper 
    colEnd = col0 + colNumber - 1; 
     
    cells   = new Cell[rowNumber][colNumber]; 
    visible = new boolean[rowNumber][colNumber]; 
    def     = new boolean[rowNumber][colNumber]; 
     
    for (int r = 0; r < rowNumber; r++) { 
      for (int c = 0; c < colNumber; c++) { 
        visible[r][c] = true; 
        def[r][c]     = true; 
        cells[r][c]   = defaultCell; 
      } 
    } 
     
    //.... The default boundary conditions 
     
    boundaryConditions.put(ColumnLocation.LEFT, BoundaryCondition.FIXED); 
    boundaryConditions.put(ColumnLocation.RIGHT, BoundaryCondition.FIXED); 
    boundaryConditions.put(RowLocation.TOP, BoundaryCondition.FIXED); 
    boundaryConditions.put(RowLocation.BOTTOM, BoundaryCondition.FIXED); 
     
  } 
   
  /** 
   * Coalesce cells containing the default cell into one common cell. This is useful to simplify the HTML table 
   * structure e. g. after all relevant data has been added to a table. Coalescing can either be along rows 
   * or along columns. For example, when coalescing along rows, each row of the table will be checked for 
   * consecutive blocks of cells containing the default cell. These blocks will be replaced by one cell covering 
   * them all. 
   * <p> 
   * This method creates new cell instances using the <code>name</code> and 
   * <code>types</code> arguments provided which can then be used in the renderer 
   * to react accordingly. 
   * 
   * @param internalLocation The location along which to coalesce. Can either be along rows or along columns 
   * @param name             The name to assign for the cell(s) created 
   * @param types            The types to assign to the cell(s) created 
   * 
   * @return <code>true</code> if cells were coalesced 
   */ 
   
  public boolean coalese(InternalLocation internalLocation, String name, String... types) { 
    if (name == null) 
      throw new IllegalArgumentException("name may not be null"); 
    if (types == null) 
      throw new IllegalArgumentException("types may not be null"); 
    if (internalLocation == null) 
      throw new IllegalArgumentException("internalLocation may not be null"); 
     
    Cell    cell      = null; 
    boolean coalesced = false; 
     
    switch (internalLocation) { 
       
      case ROW: 
         
        for (int r = 0; r < rowNumber; r++) { 
          int     cstart   = 0; 
          int     c        = 0; 
          boolean scanning = false; 
          while (c < colNumber) { 
            if (isDefaultCell(r + row0, c + col0)) { 
              if (!scanning) { 
                cstart   = c; 
                scanning = true; 
              } 
            } else if (scanning) { 
               
              cell = new Cell(name, 1, c - cstart); 
              for (String type : types) 
                cell.setType(type); 
              setCell(cell, r + row0, cstart + col0); 
              scanning  = false; 
              coalesced = true; 
               
            } 
            c++; 
          } 
           
          //.... Final column 
           
          if (scanning) { 
             
            cell = new Cell(name, 1, c - cstart); 
            for (String type : types) 
              cell.setType(type); 
            setCell(cell, r + row0, cstart + col0); 
            coalesced = true; 
             
          } 
           
        } 
         
        break; 
         
      case COLUMN: 
         
        for (int c = 0; c < colNumber; c++) { 
          int     rstart   = 0; 
          int     r        = 0; 
          boolean scanning = false; 
          while (r < rowNumber) { 
            if (isDefaultCell(r + row0, c + col0)) { 
              if (!scanning) { 
                rstart   = r; 
                scanning = true; 
              } 
            } else if (scanning) { 
               
              cell = new Cell(name, r - rstart, 1); 
              for (String type : types) 
                cell.setType(type); 
              setCell(cell, rstart + row0, c + col0); 
              scanning  = false; 
              coalesced = true; 
               
            } 
            r++; 
          } 
           
          //.... Final column 
           
          if (scanning) { 
             
            cell = new Cell(name, r - rstart, 1); 
            for (String type : types) 
              cell.setType(type); 
            setCell(cell, rstart + row0, c + col0); 
            coalesced = true; 
             
          } 
           
        } 
         
    } 
     
    return coalesced; 
     
  } 
   
  /** 
   * Create a shallow copy of the current instance. The clone is identical 
   * to the original cell in terms of dimensions, logical indices, 
   * cell visibility, default cells, boundary conditions and cells 
   * as such, but the cell references in the clone are the same as in the original 
   * table. 
   * 
   * @return A cloned table instance 
   */ 
   
  public Table clone() { 
    Table clone = new Table(row0, col0, rowNumber, colNumber); 
     
    clone.setBoundaryCondition(ColumnLocation.LEFT, getBoundaryCondition(ColumnLocation.LEFT)); 
    clone.setBoundaryCondition(ColumnLocation.RIGHT, getBoundaryCondition(ColumnLocation.RIGHT)); 
    clone.setBoundaryCondition(RowLocation.BOTTOM, getBoundaryCondition(RowLocation.BOTTOM)); 
    clone.setBoundaryCondition(RowLocation.TOP, getBoundaryCondition(RowLocation.TOP)); 
     
    for (int r = 0; r < rowNumber; r++) { 
      for (int c = 0; c < colNumber; c++) { 
        int row = r + row0; 
        int col = c + col0; 
        clone.setVisible(r, c, visible[r][c]); 
        clone.setDefault(r, c, def[r][c]); 
        clone.setCell(r, c, cells[r][c]); 
      } 
    } 
    return clone; 
  } 
   
  /** 
   * Internal helper for cloning 
   */ 
   
  private void setVisible(int r, int c, boolean v) { 
    visible[r][c] = v; 
  } 
   
  /** 
   * Internal helper for cloning 
   */ 
   
  private void setDefault(int r, int c, boolean d) { 
    def[r][c] = d; 
  } 
   
  /** 
   * Internal helper for cloning 
   */ 
   
  private void setCell(int r, int c, Cell cell) { 
    cells[r][c] = cell; 
  } 
   
  /** 
   * Retrieve the boundary condition at the given boundary location 
   * 
   * @param boundaryLocation The boundary location where the information is to be retrieved 
   * 
   * @return The boundary condition at the desired boundary location 
   */ 
   
  public BoundaryCondition getBoundaryCondition(BoundaryLocation boundaryLocation) { 
    if (boundaryLocation == null) { 
      throw new IllegalArgumentException("location may not be null"); 
    } 
    return boundaryConditions.get(boundaryLocation); 
  } 
   
  /** 
   * Add columns to the table either at the left or at the right end. 
   * 
   * If columns are inserted at the left edge of the table, the logical start index 
   * for the columns is reduced by <code>count</code>. If columns are inserted at the 
   * right edge of the table, the logical end index of the columns is increased by 
   * <code>count</code>. 
   * 
   * @param location Whether to add the columns at the left or the right edge 
   * @param count    The number of columns to add 
   */ 
   
  public void addColumns(ColumnLocation location, int count) { 
    if (location == null) { 
      throw new IllegalArgumentException("location may not be null"); 
    } 
    if (count <= 0) { 
      throw new IllegalArgumentException("count must be greater than 0"); 
    } 
     
    Cell[][]    cells_new   = new Cell[rowNumber][colNumber + count]; 
    boolean[][] visible_new = new boolean[rowNumber][colNumber + count]; 
    boolean[][] def_new     = new boolean[rowNumber][colNumber + count]; 
     
    switch (location) { 
       
      case LEFT: 
        for (int r = 0; r < rowNumber; r++) { 
          for (int c = 0; c < count; c++) { 
            visible_new[r][c] = true; 
            def_new[r][c]     = true; 
            cells_new[r][c]   = defaultCell; 
          } 
          for (int c = 0; c < colNumber; c++) { 
            visible_new[r][c + count] = visible[r][c]; 
            def_new[r][c + count]     = def[r][c]; 
            cells_new[r][c + count]   = cells[r][c]; 
          } 
        } 
        col0 -= count; 
        break; 
         
      case RIGHT: 
        for (int r = 0; r < rowNumber; r++) { 
          for (int c = 0; c < colNumber; c++) { 
            visible_new[r][c] = visible[r][c]; 
            def_new[r][c]     = def[r][c]; 
            cells_new[r][c]   = cells[r][c]; 
          } 
          for (int c = colNumber; c < count + colNumber; c++) { 
            visible_new[r][c] = true; 
            def_new[r][c]     = true; 
            cells_new[r][c]   = defaultCell; 
          } 
        } 
        colEnd += count; 
        break; 
         
    } 
     
    visible = visible_new; 
    def     = def_new; 
    cells   = cells_new; 
     
    colNumber += count; 
     
  } 
   
  /** 
   * Add one column to the table either at the left or at the right end. 
   * 
   * This is a convenience method for adding just one column. See 
   * {@link #addColumns(ColumnLocation, int)} for more details. 
   * 
   * @param location Whether to add the column at the left or the right edge 
   */ 
   
  public void addColumn(ColumnLocation location) { 
    addColumns(location, 1); 
  } 
   
  /** 
   * Add one row to the table either at the top or at the bottom end. 
   * 
   * This is a convenience method for adding just one row. See 
   * {@link #addRows(RowLocation, int)} for more details. 
   * 
   * @param location Whether to add the row at the top or the bottom edge 
   */ 
   
  public void addRow(RowLocation location) { 
    addRows(location, 1); 
  } 
   
  /** 
   * Add rows to the table either at the top or at the bottom end. 
   * 
   * If rows are inserted at the top edge of the table, the logical start index 
   * for the rows is reduced by <code>count</code>. If rows are inserted at the 
   * bottom edge of the table, the logical end index of the rows is increased by 
   * <code>count</code>. 
   * 
   * @param location Whether to add the rows at the top or the bottom edge 
   * @param count    The number of rows to add 
   */ 
   
  public void addRows(RowLocation location, int count) { 
    if (location == null) { 
      throw new IllegalArgumentException("location may not be null"); 
    } 
    if (count <= 0) { 
      throw new IllegalArgumentException("count must be greater than 0"); 
    } 
     
    Cell[][]    cells_new   = new Cell[rowNumber + count][colNumber]; 
    boolean[][] visible_new = new boolean[rowNumber + count][colNumber]; 
    boolean[][] def_new     = new boolean[rowNumber + count][colNumber]; 
     
    switch (location) { 
       
      case TOP: 
        for (int c = 0; c < colNumber; c++) { 
          for (int r = 0; r < count; r++) { 
            visible_new[r][c] = true; 
            def_new[r][c]     = true; 
            cells_new[r][c]   = defaultCell; 
          } 
          for (int r = 0; r < rowNumber; r++) { 
            visible_new[r + count][c] = visible[r][c]; 
            def_new[r + count][c]     = def[r][c]; 
            cells_new[r + count][c]   = cells[r][c]; 
          } 
        } 
        row0 -= count; 
        break; 
         
      case BOTTOM: 
        for (int c = 0; c < colNumber; c++) { 
          for (int r = 0; r < rowNumber; r++) { 
            visible_new[r][c] = visible[r][c]; 
            def_new[r][c]     = def[r][c]; 
            cells_new[r][c]   = cells[r][c]; 
          } 
          for (int r = rowNumber; r < count + rowNumber; r++) { 
            visible_new[r][c] = true; 
            def_new[r][c]     = true; 
            cells_new[r][c]   = defaultCell; 
          } 
        } 
        rowEnd += count; 
        break; 
         
    } 
     
    visible = visible_new; 
    def     = def_new; 
    cells   = cells_new; 
     
    rowNumber += count; 
     
  } 
   
  /** 
   * Removes empty cells at all four boundary locations. 
   * 
   * This is a convenience method comprising four individual 
   * method calls. 
   * 
   * @return <code>true</code> if some cells removed 
   */ 
   
  public boolean compact() { 
    return compact(ColumnLocation.LEFT) && compact(ColumnLocation.RIGHT) 
    && compact(RowLocation.TOP) && compact(RowLocation.BOTTOM); 
  } 
   
  /** 
   * Removes empty cells at the given locations. 
   * 
   * This is a convenience method simplifying individual calls to the methods 
   * {@link #compact(RowLocation)}, 
   * {@link #compact(ColumnLocation)}, and 
   * {@link #compact(InternalLocation)}. See these methods for additional details. 
   * 
   * @param locations The desired locations where to compact the table 
   * 
   * @return <code>true</code> if some cells removed 
   */ 
   
  public boolean compact(Location... locations) { 
    if (locations == null) { 
      throw new IllegalArgumentException("locations may not be null"); 
    } 
     
    boolean ret = false; 
     
    for (Location location : locations) { 
      if (location instanceof ColumnLocation) { 
        ret = ret || compact((ColumnLocation)location); 
      } else if (location instanceof RowLocation) { 
        ret = ret || compact((RowLocation)location); 
      } else if (location instanceof InternalLocation) { 
        ret = ret || compact((InternalLocation)location); 
      } 
    } 
     
    return ret; 
     
  } 
   
  /** 
   * Removes empty cells at the given boundary location. 
   * 
   * Empty cells are cells which contain the default cell, i. e. they have not been 
   * touched as part of a {@link #setCell(Cell, int, int)} method call. This method 
   * checks for complete columns with default cells at the given boundary location 
   * and removes them from the table. 
   * 
   * @param columnLocation The desired location where to compact the table 
   * 
   * @return <code>true</code> if some cells were cut off 
   */ 
   
  public boolean compact(ColumnLocation columnLocation) { 
    if (columnLocation == null) { 
      throw new IllegalArgumentException("columnLocation may not be null"); 
    } 
     
    int         count       = 0; 
    Cell[][]    cells_new   = null; 
    boolean[][] visible_new = null; 
    boolean[][] def_new     = null; 
     
    //.... Save this for later check for changes 
     
    int old_row0      = row0; 
    int old_col0      = col0; 
    int old_rowNumber = rowNumber; 
    int old_colNumber = colNumber; 
     
    //.... Left edge 
     
    if (columnLocation.equals(ColumnLocation.LEFT)) { 
       
      int     c         = 0; 
      boolean removable = true; 
       
      do { 
        for (int r = 0; r < rowNumber; r++) { 
          if (!def[r][c]) { 
            removable = false; 
          } 
        } 
        if (removable) { 
          count++; 
          c++; 
        } 
      } while (removable); 
       
      if (count > 0) { 
         
        cells_new   = new Cell[rowNumber][colNumber - count]; 
        visible_new = new boolean[rowNumber][colNumber - count]; 
        def_new     = new boolean[rowNumber][colNumber - count]; 
         
        for (int c2 = 0; c2 < colNumber - count; c2++) { 
          for (int r = 0; r < rowNumber; r++) { 
            visible_new[r][c2] = visible[r][c2 + count]; 
            def_new[r][c2]     = def[r][c2 + count]; 
            cells_new[r][c2]   = cells[r][c2 + count]; 
          } 
        } 
         
        visible = visible_new; 
        def     = def_new; 
        cells   = cells_new; 
         
        col0 += count; 
         
      } 
       
    } else { 
       
      //.... Right edge 
       
      int     c         = colNumber - 1; 
      boolean removable = true; 
       
      do { 
        for (int r = 0; r < rowNumber; r++) { 
          if (!def[r][c]) { 
            removable = false; 
          } 
        } 
        if (removable) { 
          c--; 
          count++; 
        } 
      } while (removable); 
       
      if (count > 0) { 
         
        cells_new   = new Cell[rowNumber][colNumber - count]; 
        visible_new = new boolean[rowNumber][colNumber - count]; 
        def_new     = new boolean[rowNumber][colNumber - count]; 
         
        for (int c2 = 0; c2 < colNumber - count; c2++) { 
          for (int r = 0; r < rowNumber; r++) { 
            visible_new[r][c2] = visible[r][c2]; 
            def_new[r][c2]     = def[r][c2]; 
            cells_new[r][c2]   = cells[r][c2]; 
          } 
        } 
         
        visible = visible_new; 
        def     = def_new; 
        cells   = cells_new; 
         
        colEnd -= count; 
         
      } 
       
    } 
     
    colNumber -= count; 
     
    //.... Check whether the dimensions of the table have changed 
     
    if (row0 != old_row0 || col0 != old_col0 
      || rowNumber != old_rowNumber 
      || colNumber != old_colNumber) { 
      return true; 
    } else { 
      return false; 
    } 
     
  } 
   
  /** 
   * Removes empty cells at the given boundary location. 
   * 
   * Empty cells are cells which contain the default cell, i. e. they have not been 
   * touched as part of a {@link #setCell(Cell, int, int)} method call. This method 
   * checks for complete rows with default cells at the given boundary location 
   * and removes them from the table. 
   * 
   * @param rowLocation The desired location where to compact the table 
   * 
   * @return <code>true</code> if some cells were cut off 
   */ 
   
  public boolean compact(RowLocation rowLocation) { 
    if (rowLocation == null) { 
      throw new IllegalArgumentException("rowLocation may not be null"); 
    } 
     
    int         count       = 0; 
    Cell[][]    cells_new   = null; 
    boolean[][] visible_new = null; 
    boolean[][] def_new     = null; 
     
    //.... Save this for later check for changes 
     
    int old_row0      = row0; 
    int old_col0      = col0; 
    int old_rowNumber = rowNumber; 
    int old_colNumber = colNumber; 
     
    //.... Top edge 
     
    if (rowLocation.equals(RowLocation.TOP)) { 
       
      int     r         = 0; 
      boolean removable = true; 
       
      do { 
        for (int c = 0; c < colNumber; c++) { 
          if (!def[r][c]) { 
            removable = false; 
          } 
        } 
        if (removable) { 
          count++; 
          r++; 
        } 
      } while (removable); 
       
      if (count > 0) { 
         
        cells_new   = new Cell[rowNumber - count][colNumber]; 
        visible_new = new boolean[rowNumber - count][colNumber]; 
        def_new     = new boolean[rowNumber - count][colNumber]; 
         
        for (int c = 0; c < colNumber; c++) { 
          for (int r2 = 0; r2 < rowNumber - count; r2++) { 
            visible_new[r2][c] = visible[r2 + count][c]; 
            def_new[r2][c]     = def[r2 + count][c]; 
            cells_new[r2][c]   = cells[r2 + count][c]; 
          } 
        } 
         
        visible = visible_new; 
        def     = def_new; 
        cells   = cells_new; 
         
        row0 += count; 
         
      } 
       
    } else { 
       
      //.... Bottom edge 
       
      int     r         = rowNumber - 1; 
      boolean removable = true; 
       
      do { 
        for (int c = 0; c < colNumber; c++) { 
          if (!def[r][c]) { 
            removable = false; 
          } 
        } 
        if (removable) { 
          count++; 
          r--; 
        } 
      } while (removable); 
       
      if (count > 0) { 
         
        cells_new   = new Cell[rowNumber - count][colNumber]; 
        visible_new = new boolean[rowNumber - count][colNumber]; 
        def_new     = new boolean[rowNumber - count][colNumber]; 
         
        for (int c = 0; c < colNumber; c++) { 
          for (int r2 = 0; r2 < rowNumber - count; r2++) { 
            visible_new[r2][c] = visible[r2][c]; 
            def_new[r2][c]     = def[r2][c]; 
            cells_new[r2][c]   = cells[r2][c]; 
          } 
        } 
         
        visible = visible_new; 
        def     = def_new; 
        cells   = cells_new; 
         
        rowEnd -= count; 
         
      } 
       
    } 
     
    rowNumber -= count; 
     
    //.... Check whether the dimensions of the table have changed 
     
    if (row0 != old_row0 || col0 != old_col0 
      || rowNumber != old_rowNumber 
      || colNumber != old_colNumber) { 
      return true; 
    } else { 
      return false; 
    } 
     
  } 
   
  /** 
   * Removes empty cells at the given internal location. 
   * 
   * Empty cells are cells which contain the default cell, i. e. they have not been 
   * touched as part of a {@link #setCell(Cell, int, int)} method call. This method 
   * checks for complete rows or columns in the table (depending on the 
   * <code>internalLocation</code> parameter) with default cells 
   * and removes them from the table. 
   * <p> 
   * Note that calls to this method also remove such rows or columns ate the 
   * table boundaries, and thus a call to this method is a superset to calls 
   * to {@link #compact(RowLocation)} and {@link #compact(ColumnLocation)}. 
   * 
   * @param internalLocation The desired internal location where to compact the table 
   *                         (effectively by rows or by columns) 
   * 
   * @return <code>true</code> if some cells were removed 
   */ 
   
  public boolean compact(InternalLocation internalLocation) { 
    if (internalLocation == null) { 
      throw new IllegalArgumentException("internalLocation may not be null"); 
    } 
     
    int         count       = 0; 
    Cell[][]    cells_new   = null; 
    boolean[][] visible_new = null; 
    boolean[][] def_new     = null; 
     
    //.... Save this for later check for changes 
     
    int old_row0      = row0; 
    int old_col0      = col0; 
    int old_rowNumber = rowNumber; 
    int old_colNumber = colNumber; 
     
    if (internalLocation.equals(InternalLocation.COLUMN)) { 
       
      //.... Create an index of columns to retain 
       
      List<Integer> columnList = new ArrayList<Integer>(); 
       
      for (int c = 0; c < colNumber; c++) { 
        boolean removable = true; 
        for (int r = 0; r < rowNumber; r++) { 
          if (!def[r][c]) { 
            removable = false; 
          } 
        } 
        if (!removable) { 
          columnList.add(c); 
        } 
      } 
       
      //.... Remove the columns 
       
      count = columnList.size(); 
       
      if (count > 0) { 
         
        cells_new   = new Cell[rowNumber][count]; 
        visible_new = new boolean[rowNumber][count]; 
        def_new     = new boolean[rowNumber][count]; 
         
        int c2 = 0; 
        for (int c = 0; c < count; c++) { 
          for (int r = 0; r < rowNumber; r++) { 
            c2                = columnList.get(c); 
            visible_new[r][c] = visible[r][c2]; 
            def_new[r][c]     = def[r][c2]; 
            cells_new[r][c]   = cells[r][c2]; 
          } 
        } 
         
        visible = visible_new; 
        def     = def_new; 
        cells   = cells_new; 
         
        col0      += columnList.get(0); 
        colNumber  = count; 
        colEnd     = col0 + colNumber - 1; 
         
      } 
       
      //.... Remove all empty rows (this includes the TOP and BOTTOM cases) 
       
    } else if (internalLocation.equals(InternalLocation.ROW)) { 
       
      //.... Create an index of rows to retain 
       
      List<Integer> rowList = new ArrayList<Integer>(); 
       
      for (int r = 0; r < rowNumber; r++) { 
        boolean removable = true; 
        for (int c = 0; c < colNumber; c++) { 
          if (!def[r][c]) { 
            removable = false; 
          } 
        } 
        if (!removable) { 
          rowList.add(r); 
        } 
      } 
       
      //.... Remove the rows 
       
      count = rowList.size(); 
       
      if (count > 0) { 
         
        cells_new   = new Cell[count][colNumber]; 
        visible_new = new boolean[count][colNumber]; 
        def_new     = new boolean[count][colNumber]; 
         
        int r2 = 0; 
        for (int c = 0; c < colNumber; c++) { 
          for (int r = 0; r < count; r++) { 
            r2                = rowList.get(r); 
            visible_new[r][c] = visible[r2][c]; 
            def_new[r][c]     = def[r2][c]; 
            cells_new[r][c]   = cells[r2][c]; 
          } 
        } 
         
        visible = visible_new; 
        def     = def_new; 
        cells   = cells_new; 
         
        row0      += rowList.get(0); 
        rowNumber  = count; 
        rowEnd     = row0 + rowNumber - 1; 
         
      } 
       
    } 
     
    //.... Check whether the dimensions of the table have changed 
     
    if (row0 != old_row0 || col0 != old_col0 
      || rowNumber != old_rowNumber 
      || colNumber != old_colNumber) { 
      return true; 
    } else { 
      return false; 
    } 
     
  } 
   
  /** 
   * A convenience method to enable clipping at all four table boundaries. 
   * 
   * @see BoundaryCondition 
   */ 
   
  public void setClipping() { 
    boundaryConditions.put(ColumnLocation.LEFT, BoundaryCondition.CLIPPING); 
    boundaryConditions.put(ColumnLocation.RIGHT, BoundaryCondition.CLIPPING); 
    boundaryConditions.put(RowLocation.TOP, BoundaryCondition.CLIPPING); 
    boundaryConditions.put(RowLocation.BOTTOM, BoundaryCondition.CLIPPING); 
  } 
   
  /** 
   * A convenience method to enable auto-grow at all four table boundaries. 
   * 
   * @see BoundaryCondition 
   */ 
   
  public void setGrow() { 
    boundaryConditions.put(ColumnLocation.LEFT, BoundaryCondition.GROW); 
    boundaryConditions.put(ColumnLocation.RIGHT, BoundaryCondition.GROW); 
    boundaryConditions.put(RowLocation.TOP, BoundaryCondition.GROW); 
    boundaryConditions.put(RowLocation.BOTTOM, BoundaryCondition.GROW); 
  } 
   
  /** 
   * A convenience method to enable fixed boundaries at all four table boundaries. 
   * 
   * @see BoundaryCondition 
   */ 
   
  public void setFixed() { 
    boundaryConditions.put(ColumnLocation.LEFT, BoundaryCondition.FIXED); 
    boundaryConditions.put(ColumnLocation.RIGHT, BoundaryCondition.FIXED); 
    boundaryConditions.put(RowLocation.TOP, BoundaryCondition.FIXED); 
    boundaryConditions.put(RowLocation.BOTTOM, BoundaryCondition.FIXED); 
  } 
   
  /** 
   * Retrieve the logical start index for rows in the table. 
   * 
   * @return The logical start index for rows 
   */ 
   
  public int getRow0() { 
    return row0; 
  } 
   
  /** 
   * Retrieve the logical start index for columns in the table. 
   * 
   * @return The logical start index for columns 
   */ 
   
  public int getCol0() { 
    return col0; 
  } 
   
  /** 
   * Retrieve the number of rows in the table. 
   * 
   * @return The number of rows in the table 
   */ 
   
  public int getRowNumber() { 
    return rowNumber; 
  } 
   
  /** 
   * Retrieve the number of columns in the table. 
   * 
   * @return The number of columns in the table 
   */ 
   
  public int getColNumber() { 
    return colNumber; 
  } 
   
  /** 
   * Retrieve the cell at the given table location. 
   * 
   * @param row The logical row index 
   * @param col The logical column index 
   * 
   * @return The cell at the given location 
   */ 
   
  public Cell getCell(int row, int col) { 
    int r = row - row0; 
    int c = col - col0; 
     
    if (r >= rowNumber || r < 0) { 
      throw new IllegalArgumentException("row must be between " + row0 + " and " + getRowEnd()); 
    } 
    if (c >= colNumber || c < 0) { 
      throw new IllegalArgumentException("col must be between " + col0 + " and " + getColEnd()); 
    } 
    return cells[r][c]; 
  } 
   
  /** 
   * Check whether the cell at the given table location is visible. 
   * 
   * Cells can become invisible when other cells spanning more than one row and/or 
   * column cover the particular location in the table. This is important for the 
   * rendering of tables since cells which are invisible are not part of the rendering. 
   * 
   * @param row The logical row index 
   * @param col The logical column index 
   * 
   * @return <code>true</code> if the cell at the given location is visible 
   */ 
   
  public boolean isVisible(int row, int col) { 
    int r = row - row0; 
    int c = col - col0; 
     
    if (r >= rowNumber || r < 0) { 
      throw new IllegalArgumentException("row must be between " + row0 + " and " + getRowEnd()); 
    } 
    if (c >= colNumber || c < 0) { 
      throw new IllegalArgumentException("col must be between " + col0 + " and " + getColEnd()); 
    } 
    return visible[r][c]; 
  } 
   
  /** 
   * Check whether the cell at the given table location is the default cell. 
   * 
   * At table instance creation time, all cells in the table refer to the default cell. 
   * This may change over time as cells are added to the table. 
   * 
   * @param row The logical row index 
   * @param col The logical column index 
   * 
   * @return <code>true</code> if the cell at the given location is the default cell 
   */ 
   
  public boolean isDefaultCell(int row, int col) { 
    int r = row - row0; 
    int c = col - col0; 
     
    if (r >= rowNumber || r < 0) { 
      throw new IllegalArgumentException("row must be between " + row0 + " and " + getRowEnd()); 
    } 
    if (c >= colNumber || c < 0) { 
      throw new IllegalArgumentException("col must be between " + col0 + " and " + getColEnd()); 
    } 
    return def[r][c]; 
  } 
   
  /** 
   * Insert a cell into the table at the given location. 
   * 
   * Several cases need to be differentiated when adding a cell to the table. This HTML table shows 
   * the different cases that can occur when inserting a cell (orange) into a table (grey). Note that 
   * these cases apply both to rows and columns: 
   * <p> 
   * 
   * <table style="text-align: left; width: 500px;" border="1" 
   * cellpadding="2" cellspacing="2"> 
   * <tbody> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">Case<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td colspan="3" rowspan="1" 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); text-align: center; font-family: Helvetica,Arial,sans-serif;">Table 
   * Extent<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">1<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(255, 204, 51); font-family: Helvetica,Arial,sans-serif; text-align: center;">Cell</td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"><br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;"><br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;"><br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;"><br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">2<br> 
   * </td> 
   * <td colspan="4" rowspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 204, 51); font-family: Helvetica,Arial,sans-serif; text-align: center;">Cell<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;"><br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">3<br> 
   * </td> 
   * <td colspan="7" rowspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 204, 51); font-family: Helvetica,Arial,sans-serif; text-align: center;">Cell</td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">4<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;"><br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(255, 204, 51); font-family: Helvetica,Arial,sans-serif; text-align: center;">Cell</td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;"><br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">5<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;"><br> 
   * </td> 
   * <td colspan="4" rowspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 204, 51); font-family: Helvetica,Arial,sans-serif; text-align: center;">Cell<br> 
   * </td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">6<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;"> <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">      
   * <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;">     
   * <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;"><br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(192, 192, 192); font-family: Helvetica,Arial,sans-serif;">     
   * <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">     
   *  <br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(255, 204, 51); font-family: Helvetica,Arial,sans-serif; text-align: center;">Cell</td> 
   * </tr> 
   * </tbody> 
   * </table> 
   * <p> 
   * Depending on the chosen boundary conditions at the boundary locations, the following results occur: 
   * <p> 
   * <table style="text-align: left;" border="1" cellpadding="2" 
   * cellspacing="2"> 
   * <tbody> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">Case<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif; background-color: rgb(255, 255, 255);">FIXED</td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif; background-color: rgb(255, 255, 255);">CLIPPING<br> 
   * </td> 
   * <td colspan="1" rowspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif;">GROW<br> 
   * </td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">1<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif; text-align: center;">IllegalArgumentException<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif; background-color: rgb(255, 255, 255);">null<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif;">SetResult, 
   * table expanded<br> 
   * </td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">3<br> 
   * </td> 
   * <td colspan="1" rowspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif; text-align: center;">IllegalArgumentException</td> 
   * <td colspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif;">SetResult, 
   * cell clipped<br> 
   * </td> 
   * <td colspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif;">SetResult, 
   * table expanded</td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">2<br> 
   * </td> 
   * <td colspan="1" rowspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif; text-align: center;">IllegalArgumentException</td> 
   * <td colspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif;">SetResult, 
   * cell clipped</td> 
   * <td colspan="1" 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif;">SetResult, 
   * table expanded</td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">4<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif; background-color: rgb(255, 255, 255);">SetResult<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif; background-color: rgb(255, 255, 255);">SetResult<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif;">SetResult</td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">5<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif; background-color: rgb(255, 255, 255);">IllegalArgumentException</td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif; background-color: rgb(255, 255, 255);">SetResult, 
   * cell clipped</td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif;">SetResult, 
   * table expanded</td> 
   * </tr> 
   * <tr> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif;">6<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif; background-color: rgb(255, 255, 255);">IllegalArgumentException</td> 
   * <td 
   * style="vertical-align: top; font-family: Helvetica,Arial,sans-serif; background-color: rgb(255, 255, 255);">null<br> 
   * </td> 
   * <td 
   * style="vertical-align: top; background-color: rgb(255, 255, 255); font-family: Helvetica,Arial,sans-serif;">SetResult, 
   * table expanded</td> 
   * </tr> 
   * </tbody> 
   * </table> 
   * 
   * @param row The logical row index 
   * @param col The logical column index 
   * 
   * @return A {@link SetResult} instance (or <code>null</code>, see above) 
   * 
   * @see BoundaryCondition 
   */ 
   
  public SetResult setCell(Cell cell, int row, int col) { 
    if (cell == null) 
      throw new IllegalArgumentException("cell may not be null"); 
     
    int r        = row - row0;                  // Absolute index  (row, row0 are logical) 
    int c        = col - col0;                  // Absolute index  (col, col0 are logical) 
    int rEnd     = r + cell.getRowSpan() - 1;   // Absolute index 
    int cEnd     = c + cell.getColSpan() - 1;   // Absolute index 
    int rowLimit = row0 + rowNumber - cell.getRowSpan(); 
    int colLimit = col0 + colNumber - cell.getColSpan(); 
     
    SetResult result = new SetResult(row, col);  // The default 
     
    //.... Row: Case 1 
     
    if (rEnd < 0) { 
       
      switch (boundaryConditions.get(RowLocation.TOP)) { 
        case FIXED: 
          throw new IllegalArgumentException("Cell lies completely outside of the table"); 
        case CLIPPING: 
          return null;             // Entire contents are clipped 
        case GROW: 
          addRows(RowLocation.TOP, -r); 
          r    = 0; 
          rEnd = r + cell.getRowSpan() - 1; 
      } 
       
    } else if (r < 0) { 
       
      //.... Row: Case 2 
       
      if (rEnd < rowNumber) { 
         
        switch (boundaryConditions.get(RowLocation.TOP)) { 
          case FIXED: 
            if (cell.getRowSpan() > rowNumber) 
              throw new IllegalArgumentException("Cell has too many rows. Maximum row number is " + rowNumber); 
            throw new IllegalArgumentException("row must be between " + row0 + " and " + rowLimit); 
          case CLIPPING: 
            r = 0; 
            result.setModified(true); 
            break; 
          case GROW: 
            addRows(RowLocation.TOP, -r); 
            r    = 0; 
            rEnd = r + cell.getRowSpan() - 1; 
        } 
         
      } else { 
         
        //.... Row: Case 3 
         
        switch (boundaryConditions.get(RowLocation.TOP)) { 
          case FIXED: 
            throw new IllegalArgumentException("Cell has too many rows. Maximum row number is " + rowNumber); 
          case CLIPPING: 
            r = 0; 
            result.setModified(true); 
            break; 
          case GROW: 
            addRows(RowLocation.TOP, -r); 
            r    = 0; 
            rEnd = r + cell.getRowSpan() - 1; 
        } 
         
        switch (boundaryConditions.get(RowLocation.BOTTOM)) { 
          case FIXED: 
            throw new IllegalArgumentException("Cell has too many rows. Maximum row number is " + rowNumber); 
          case CLIPPING: 
            rEnd = rowNumber - 1; 
            result.setModified(true); 
            break; 
          case GROW: 
            addRows(RowLocation.BOTTOM, rEnd - getRowEnd() + row0); 
            rEnd = rowNumber - 1; 
        } 
         
      } 
       
    } else if (r < rowNumber) { 
       
      //.... Row: Case 4 
       
      if (rEnd < rowNumber) { 
         
        //.... Row: Case 5 
         
      } else { 
         
        switch (boundaryConditions.get(RowLocation.BOTTOM)) { 
          case FIXED: 
            if (cell.getRowSpan() > rowNumber) { 
              throw new IllegalArgumentException("Cell has too many rows. Maximum row number is " + rowNumber); 
            } else { 
              throw new IllegalArgumentException("row must be between " + row0 + " and " + rowLimit); 
            } 
          case CLIPPING: 
            rEnd = rowNumber - 1; 
            result.setModified(true); 
            break; 
          case GROW: 
            addRows(RowLocation.BOTTOM, rEnd - getRowEnd() + row0); 
            rEnd = rowNumber - 1; 
        } 
         
      } 
       
      //.... Row: Case 6 
       
    } else { 
       
      switch (boundaryConditions.get(RowLocation.BOTTOM)) { 
        case FIXED: 
          throw new IllegalArgumentException("Cell lies completely outside of the table"); 
        case CLIPPING: 
          return null; 
        case GROW: 
          addRows(RowLocation.BOTTOM, rEnd - getRowEnd() + row0); 
          rEnd = rowNumber - 1; 
      } 
       
    } 
     
    //.... Column: Case 1 
     
    if (cEnd < 0) { 
       
      switch (boundaryConditions.get(ColumnLocation.LEFT)) { 
        case FIXED: 
          throw new IllegalArgumentException("Cell lies completely outside of the table"); 
        case CLIPPING: 
          return null;             // Entire contents are clipped 
        case GROW: 
          addColumns(ColumnLocation.LEFT, -c); 
          c    = 0; 
          cEnd = c + cell.getColSpan() - 1; 
      } 
       
    } else if (c < 0) { 
       
      //.... Column: Case 2 
       
      if (cEnd < colNumber) { 
         
        switch (boundaryConditions.get(ColumnLocation.LEFT)) { 
          case FIXED: 
            if (cell.getColSpan() > colNumber) { 
              throw new IllegalArgumentException("Cell has too many columns. Maximum column number is " + colNumber); 
            } else { 
              throw new IllegalArgumentException("col must be between " + col0 + " and " + colLimit); 
            } 
          case CLIPPING: 
            c = 0; 
            result.setModified(true); 
            break; 
          case GROW: 
            addColumns(ColumnLocation.LEFT, -c); 
            c    = 0; 
            cEnd = c + cell.getColSpan() - 1; 
        } 
         
      } else { 
         
        //.... Column: Case 3 
         
        switch (boundaryConditions.get(ColumnLocation.LEFT)) { 
          case FIXED: 
            throw new IllegalArgumentException("Cell has too many columns. Maximum column number is " + colNumber); 
          case CLIPPING: 
            c = 0; 
            result.setModified(true); 
            break; 
          case GROW: 
            addColumns(ColumnLocation.LEFT, -c); 
            c    = 0; 
            cEnd = c + cell.getColSpan() - 1; 
             
        } 
         
        switch (boundaryConditions.get(ColumnLocation.RIGHT)) { 
          case FIXED: 
            throw new IllegalArgumentException("Cell has too many columns. Maximum column number is " + colNumber); 
          case CLIPPING: 
            cEnd = colNumber - 1; 
            result.setModified(true); 
            break; 
          case GROW: 
            addColumns(ColumnLocation.RIGHT, cEnd - getColEnd() + col0); 
            cEnd = colNumber - 1; 
        } 
         
      } 
       
    } else if (c < colNumber) { 
       
      //.... Column: Case 4 
       
      if (cEnd < colNumber) { 
         
        //.... Column: Case 5 
         
      } else { 
         
        switch (boundaryConditions.get(ColumnLocation.RIGHT)) { 
          case FIXED: 
            if (cell.getColSpan() > colNumber) { 
              throw new IllegalArgumentException("Cell has too many columns. Maximum column number is " + colNumber); 
            } else { 
              throw new IllegalArgumentException("col must be between " + col0 + " and " + colLimit); 
            } 
          case CLIPPING: 
            cEnd = colNumber - 1; 
            result.setModified(true); 
            break; 
          case GROW: 
            addColumns(ColumnLocation.RIGHT, cEnd - getColEnd() + col0); 
            cEnd = colNumber - 1; 
        } 
         
      } 
       
      //.... Column: Case 6 
       
    } else { 
       
      switch (boundaryConditions.get(ColumnLocation.RIGHT)) { 
        case FIXED: 
          throw new IllegalArgumentException("Cell lies completely outside of the table"); 
        case CLIPPING: 
          return null; 
        case GROW: 
          addColumns(ColumnLocation.RIGHT, cEnd - getColEnd() + col0); 
          cEnd = colNumber - 1; 
      } 
       
    } 
     
    //.... The cell may have to be modified to be displayed correctly now (CLIPPING only) 
     
    if (result.isModified()) { 
      cell.setRowSpan(rEnd - r + 1); 
      cell.setColSpan(cEnd - c + 1); 
    } 
     
    //.... Now actually fill the table where necessary 
     
    for (int rIndex = r; rIndex <= rEnd; rIndex++) { 
      for (int cIndex = c; cIndex <= cEnd; cIndex++) { 
        if (!def[rIndex][cIndex]) { 
          throw new IllegalArgumentException("Cell conflict when trying to add cell with name '" 
            + cell.getName() + "' at location (" 
            + rIndex + "/" + cIndex + "): already covered by cell '" + cells[rIndex][cIndex].getName() + "'"); 
        } 
        cells[rIndex][cIndex]   = cell; 
        visible[rIndex][cIndex] = false; 
        def[rIndex][cIndex]     = false; 
      } 
    } 
    visible[r][c] = true;    // Only this one remains, all others are now hidden 
     
    result.setRow(r + row0); 
    result.setCol(c + col0); 
    result.setRowEnd(rEnd + row0); 
    result.setColEnd(cEnd + col0); 
     
    return result; 
  } 
   
  /** 
   * A simple HTML debug output. The table is dumped to STDOUT and the resulting file 
   * can directly be opened in a browser to get a rough idea of the internal table layout and 
   * cell structure. 
   */ 
   
  public void dump() { 
    System.out.println("<html><body>\n"); 
    System.out.println("<table border=1>"); 
    for (int r = 0; r < rowNumber; r++) { 
      System.out.println("<tr>"); 
      for (int c = 0; c < colNumber; c++) { 
        if (def[r][c]) { 
          System.out.println("<td> (" + r + "/" + c + ")"); 
        } else { 
          if (visible[r][c]) { 
            System.out.println("<td bgcolor=green> (" + r + "/" + c + ")<br> Cell = " + cells[r][c].getName()); 
          } else { 
            System.out.println("<td bgcolor=yellow> (" + r + "/" + c + ")<br> Cell = " + cells[r][c].getName()); 
          } 
        } 
      } 
    } 
    System.out.println("</table>\n"); 
    System.out.println("</body></html>\n"); 
  } 
   
  /** 
   * Get the index of the last row in the table. 
   * 
   * @return The index of the last row in the table 
   */ 
   
  public int getRowEnd() { 
    return rowEnd; 
  } 
   
  /** 
   * Get the logical index of the last column in the table. 
   * 
   * @return The logical index of the last column in the table 
   */ 
   
  public int getColEnd() { 
    return colEnd; 
  } 
   
  /** 
   * Set the boundary condition for the given boundary location. 
   * 
   * @param boundaryLocation  The location for which the boundary condition is to be set 
   * @param boundaryCondition The boundary condition to establish for this location 
   */ 
   
  public void setBoundaryCondition(BoundaryLocation boundaryLocation, BoundaryCondition boundaryCondition) { 
    if (boundaryLocation == null) { 
      throw new IllegalArgumentException("boundaryLocation may not be null"); 
    } 
    if (boundaryCondition == null) { 
      throw new IllegalArgumentException("boundaryCondition may not be null"); 
    } 
    boundaryConditions.put(boundaryLocation, boundaryCondition); 
  } 
   
} 
 
/** 
 * Copyright 2007 Dr. Matthias Laux 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
 
/** 
 * An enum constant for the two possible boundary locations where rows are of relevance. 
 */ 
 
 enum RowLocation implements BoundaryLocation { 
   
  /** 
   * The top edge of the table 
   */ 
   
  TOP, 
   
  /** 
   * The bottom edge of the table 
   */ 
   
  BOTTOM; 
} 
 
/** 
 * Copyright 2007 Dr. Matthias Laux 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
 
/** 
 * Result information of a {@link Table#setCell(Cell, int, int)} operation. 
 * 
 * The information returned in an instance of this class is useful in cases where 
 * the boundaries of the table are managed dynaically since then the cell as such may 
 * have been clipped and thus, the starting and end indices of rows and columns may 
 * have changed. 
 */ 
 
class SetResult { 
   
  private int     row      = 0;       // The logical row where the actual insert occurred 
  private int     col      = 0;       // The logical col where the actual insert occurred 
  private int     rowEnd   = 0;       // The logical index of the end row for the cell 
  private int     colEnd   = 0;       // The logical index of the end col for the cell 
  private boolean modified = false;   // True if rowSpan and/or colSpan had to be modified 
   
  /** 
   * Create a new instance with the given row and column information. 
   * 
   * @param row The logical row where the actual insert of the cell occurred 
   * @param col The logical column where the actual insert of the cell occurred 
   */ 
   
  public SetResult(int row, int col) { 
    this.setRow(row); 
    this.setCol(col); 
  } 
   
  /** 
   * Retrieve the logical index of the row where the actual insert of the cell occurred 
   * 
   * @return The logical index of the row where the actual insert of the cell occurred 
   */ 
   
  public int getRow() { 
    return row; 
  } 
   
  /** 
   * Set the logical index of the row where the actual insert of the cell occurred. Sometimes 
   * it is necessary to modify the value established in the constructor. 
   * 
   * @param row The logical index of the row where the actual insert of the cell occurred 
   */ 
   
  public void setRow(int row) { 
    this.row = row; 
  } 
   
  /** 
   * Retrieve the logical index of the column where the actual insert of the cell occurred 
   * 
   * @return The logical index of the column where the actual insert of the cell occurred 
   */ 
   
  public int getCol() { 
    return col; 
  } 
   
  /** 
   * Set the logical index of the column where the actual insert of the cell occurred. Sometimes 
   * it is necessary to modify the value established in the constructor. 
   * 
   * @param col The logical index of the column where the actual insert of the cell occurred 
   */ 
   
  public void setCol(int col) { 
    this.col = col; 
  } 
   
  /** 
   * Returns a boolean indicating whether the original values of the cell (row and 
   * column number) and /or the insertion point (the arguments to the 
   * {@link Table#setCell(Cell, int, int)} method) have been modified in the course 
   * of the insertion process. 
   * 
   * @return A boolean indicating whether the original values of the cell have 
   *         been modified in the course of the insertion process 
   */ 
   
  public boolean isModified() { 
    return modified; 
  } 
   
  /** 
   * Set the boolean indicating whether the cell parameters have been changed in the course 
   * of the insertion process into the table 
   * 
   * @param modified The desired boolean value 
   */ 
   
  public void setModified(boolean modified) { 
    this.modified = modified; 
  } 
   
  /** 
   * Retrieve the actual row end index of the cell in the table after the insertion process. 
   * This value may be different from he expected value if clipping is activated at the 
   * boundaries. 
   * 
   * @return The actual row end index of the cell in the table 
   */ 
   
  public int getRowEnd() { 
    return rowEnd; 
  } 
   
  /** 
   * Set the actual logical row end index of the cell in the table after the insertion process. 
   * 
   * @param rowEnd The actual logical row end index of the cell in the table 
   */ 
   
  public void setRowEnd(int rowEnd) { 
    this.rowEnd = rowEnd; 
  } 
   
  /** 
   * Retrieve the actual logical end column index of the cell in the table after the insertion process. 
   * This value may be different from he expected value if clipping is activated at the 
   * boundaries. 
   * 
   * @return The actual logical column end index of the cell in the table 
   */ 
   
  public int getColEnd() { 
    return colEnd; 
  } 
   
  /** 
   * Set the actual logical column end index of the cell in the table after the insertion process. 
   * 
   * @param colEnd The actual logical column end index of the cell in the table 
   */ 
   
  public void setColEnd(int colEnd) { 
    this.colEnd = colEnd; 
  } 
   
  /** 
   * The overridden {@link Object#toString()} method. 
   * 
   * @return A string representation of the instance with all relevant data 
   */ 
   
  public String toString() { 
    StringBuilder sb = new StringBuilder(); 
    sb.append("SetResult: row = "); 
    sb.append(row); 
    sb.append(" / col = "); 
    sb.append(col); 
    sb.append(" / rowEnd = "); 
    sb.append(rowEnd); 
    sb.append(" / colEnd = "); 
    sb.append(colEnd); 
    sb.append(" / modified = "); 
    sb.append(modified); 
    return sb.toString(); 
  } 
} 
 
 
/** 
 * Copyright 2007 Dr. Matthias Laux 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
 
/** 
 * An enum constant for the different supported boundary conditions. 
 */ 
 
enum BoundaryCondition { 
   
  /** 
   * Any cell location outside of the predefined area leads to an exception. 
   * This is the default setting 
   */ 
   
  FIXED, 
   
  /** 
   * Cells are truncated when necessary 
   */ 
   
  CLIPPING, 
   
  /** 
   * The table grows when necessary to accommodate additional columns/rows 
   */ 
   
  GROW; 
} 
 
/** 
 * Copyright 2007 Dr. Matthias Laux 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
 
/** 
 * A marker interface for all locations relating to the outer boundaries of a table. 
 */ 
 
interface BoundaryLocation extends Location { 
  ; 
} 
 
/** 
 * Copyright 2007 Dr. Matthias Laux 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
 
/** 
 * An enum constant for internal locations of a table. This can be used to 
 * identify whether operations on the table should apply to rows and / or 
 * columns. 
 */ 
 
enum InternalLocation implements Location { 
   
  /** 
   * This location relates to all rows of the table 
   */ 
   
  ROW, 
   
  /** 
   * This location relates to all columns of the table 
   */ 
   
  COLUMN; 
} 
/** 
 * Copyright 2007 Dr. Matthias Laux 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
 
/** 
 * A marker interface for locations where operations or conditions apply for a table. 
 */ 
 
interface Location { 
  ; 
} 
 
/** 
 * Copyright 2007 Dr. Matthias Laux 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
 
 
/** 
 * This class represents cells in the table. Cells can span more than one row and column. 
 * Instances of this class are also used to hold all data pertaining to a cell and thus serves 
 * as a vehicle to transport data into a Velocity template. 
 */ 
 
class Cell { 
   
  private String              name       = null; 
  private Map<String, String> properties = null;      // For HTML formatting properties 
  private int                 rowSpan    = 1; 
  private int                 colSpan    = 1; 
  private Map<String, Object> content    = new HashMap<String, Object>(); 
  private Set<String>         types      = new TreeSet<String>(); 
   
  /** 
   * Constructor for a simple cell with 1 row and 1 column. 
   * 
   * @param name The name given to the cell. This can be used as a descriptive text when 
   *             necessary 
   */ 
   
  public Cell(String name) { 
    this(name, 1, 1); 
  } 
   
  /** 
   * Constructor for a cell. 
   * 
   * @param name    The name given to the cell. This can be used as a descriptive text when 
   *                necessary 
   * @param rowSpan The number of rows that this cell spans 
   * @param colSpan The number of columns that this cell spans 
   */ 
   
  public Cell(String name, int rowSpan, int colSpan) { 
    this(new HashMap<String, String>(), name, rowSpan, colSpan); 
  } 
   
  /** 
   * Constructor for a cell. 
   * 
   * @param properties The set of properties for this cell 
   * @param name       The name given to the cell. This can be used as a descriptive text when 
   *                   necessary 
   * @param rowSpan    The number of rows that this cell spans 
   * @param colSpan    The number of columns that this cell spans 
   */ 
   
  public Cell(Map<String, String> properties, String name, int rowSpan, int colSpan) { 
    if (properties == null) { 
      throw new IllegalArgumentException("properties may not be null"); 
    } 
    if (name == null) { 
      throw new IllegalArgumentException("name may not be null"); 
    } 
    if (rowSpan < 1) { 
      throw new IllegalArgumentException("rowSpan must be larger than 0"); 
    } 
    if (colSpan < 1) { 
      throw new IllegalArgumentException("colSpan must be larger than 0"); 
    } 
    this.setColSpan(colSpan); 
    this.setRowSpan(rowSpan); 
    this.properties = properties; 
    this.name       = name; 
  } 
   
  /** 
   * Create a deep copy of the current cell. 
   * 
   * @return A deep copy with all properties, types and content elements. 
   */ 
   
  public Cell clone() { 
    Cell clone = new Cell(getName(), getRowSpan(), getColSpan()); 
    for (String key : getProperties().keySet()) { 
      clone.setProperty(key, getProperty(key)); 
    } 
    for (String key : getContent().keySet()) { 
      clone.setContent(key, getContent(key)); 
    } 
    for (String type : getTypes()) { 
      clone.setType(type); 
    } 
    return clone; 
  } 
   
  /** 
   * Retrieve the properties defined for this cell. 
   * 
   * @return The properties map for this cell 
   */ 
   
  public Map<String, String> getProperties() { 
    return properties; 
  } 
   
  /** 
   * Retrieve the content elements defined for this cell. 
   * 
   * @return The content element map for this cell 
   */ 
   
  public Map<String, Object> getContent() { 
    return content; 
  } 
   
  /** 
   * Retrieve the types defined for this cell. 
   * 
   * @return The type set for this cell 
   */ 
   
  public Set<String> getTypes() { 
    return types; 
  } 
   
  /** 
   * Retrieve the name of the cell. 
   * 
   * @return The name of the cell 
   */ 
   
  public String getName() { 
    return name; 
  } 
   
  /** 
   * Retrieve the number of rows that this cell spans. 
   * 
   * @return The number of rows that this cell spans 
   */ 
   
  public int getRowSpan() { 
    return rowSpan; 
  } 
   
  /** 
   * Retrieve the number of columns that this cell spans. 
   * 
   * @return The number of columns that this cell spans 
   */ 
   
  public int getColSpan() { 
    return colSpan; 
  } 
   
  /** 
   * Set a type for this cell. Types are string-valued markers, and any number of types 
   * can be attached to a cell using this method. Inside the Velocity template, 
   * cells can be checked for types using the {@link #isType(String)} method. This 
   * allows the template to handle cells with different types differently (e. g. in the 
   * layout). 
   * 
   * @param type The type to add for this cell 
   */ 
   
  public void setType(String type) { 
    if (type == null) { 
      throw new IllegalArgumentException("type may not be null"); 
    } 
    types.add(type); 
  } 
   
  /** 
   * Check whether a given type is set for this cell. This is useful inside Velocity 
   * templates to allow for type-specific handling of cell layout. 
   * 
   * @param type The type to check for in this cell 
   * 
   * @return A boolean indicating whether the given type has been set for this cell 
   */ 
   
  public boolean isType(String type) { 
    if (type == null) { 
      throw new IllegalArgumentException("type may not be null"); 
    } 
    return types.contains(type); 
  } 
   
  /** 
   * Retrieve a property value. 
   * 
   * @param key The key for this peoperty 
   * 
   * @return The value for the given key 
   */ 
   
  public String getProperty(String key) { 
    if (key == null) { 
      throw new IllegalArgumentException("key may not be null"); 
    } 
    if (!properties.containsKey(key)) { 
      throw new IllegalArgumentException("Unknown property key: " + key); 
    } 
    return properties.get(key); 
  } 
   
  /** 
   * Set a property value. Properties are another means to equip a cell with 
   * configuration information or content data, and any number of key/value pairs 
   * can be attached to a cell and used in Velocity templates when processing the cell. 
   * 
   * @param key   The property key 
   * @param value The property value 
   */ 
   
  public void setProperty(String key, String value) { 
    if (key == null) { 
      throw new IllegalArgumentException("key may not be null"); 
    } 
    if (value == null) { 
      throw new IllegalArgumentException("value may not be null"); 
    } 
    properties.put(key, value); 
  } 
   
  /** 
   * Retrieve the content object associated with the given key. 
   * 
   * @param key The key identifying the content object 
   * 
   * @return The content object associated with the given key 
   */ 
   
  public Object getContent(String key) { 
    if (key == null) { 
      throw new IllegalArgumentException("key may not be null"); 
    } 
    return content.get(key); 
  } 
   
  /** 
   * Set a content object. Content objects are used to attach data to a cell 
   * which can then be used in the template, for example to attach a picture 
   * or a table with the results of a DB query to an HTML cell. The controller 
   * program which sets up the table/cell structure would add such content objects 
   * to the cells, and the Velocity template would retrieve the data using the 
   * keys and add it to the HTML cell structure. 
   * 
   * @param key   The key by which this content object is identified 
   * @param value The actual content object 
   */ 
   
  public void setContent(String key, Object value) { 
    if (key == null) { 
      throw new IllegalArgumentException("key may not be null"); 
    } 
    if (value == null) { 
      throw new IllegalArgumentException("value may not be null"); 
    } 
    content.put(key, value); 
  } 
   
  /** 
   * Set the number of rows that this cell spans. The original value set in the 
   * constructor my change when cells are clipped during insertion into the table. 
   * 
   * @see BoundaryCondition 
   * 
   * @param rowSpan The number of rows that this cell spans 
   */ 
   
  public void setRowSpan(int rowSpan) { 
    if (rowSpan < 1) { 
      throw new IllegalArgumentException("rowSpan must be greater than 0"); 
    } 
    this.rowSpan = rowSpan; 
  } 
   
  /** 
   * Set the number of columns that this cell spans. The original value set in the 
   * constructor my change when cells are clipped during insertion into the table. 
   * 
   * @see BoundaryCondition 
   * 
   * @param colSpan The number of columns that this cell spans 
   */ 
   
  public void setColSpan(int colSpan) { 
    if (colSpan < 1) { 
      throw new IllegalArgumentException("colSpan must be greater than 0"); 
    } 
    this.colSpan = colSpan; 
  } 
   
  /** 
   * The overridden {@link Object#toString()} method. 
   * 
   * @return A string representation of the instance with all relevant data 
   */ 
   
  public String toString() { 
    StringBuilder sb = new StringBuilder(); 
    sb.append("Cell: name = "); 
    sb.append(name); 
    sb.append(" / rowSpan = "); 
    sb.append(rowSpan); 
    sb.append(" / colSpan = "); 
    sb.append(colSpan); 
    return sb.toString(); 
  } 
   
} 
 
 
 
/** 
 * Copyright 2007 Dr. Matthias Laux 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 
 
/** 
 * An enum constant for the two possible boundary locations where columns are of relevance. 
 */ 
 
 enum ColumnLocation implements BoundaryLocation { 
   
  /** 
   * The left edge of the table 
   */ 
   
  LEFT, 
   
  /** 
   * The right edge of the table 
   */ 
   
  RIGHT; 
} 
 
    
     
     
  
  |