]> source.dussan.org Git - poi.git/commitdiff
Fix for bug 45639 - cleaned up index logic inside ColumnInfoRecordsAggregate
authorJosh Micich <josh@apache.org>
Thu, 11 Sep 2008 23:18:50 +0000 (23:18 +0000)
committerJosh Micich <josh@apache.org>
Thu, 11 Sep 2008 23:18:50 +0000 (23:18 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@694534 13f79535-47bb-0310-9956-ffa450edef68

src/documentation/content/xdocs/changes.xml
src/documentation/content/xdocs/status.xml
src/java/org/apache/poi/hssf/model/Sheet.java
src/java/org/apache/poi/hssf/record/ColumnInfoRecord.java
src/java/org/apache/poi/hssf/record/aggregates/ColumnInfoRecordsAggregate.java
src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java
src/testcases/org/apache/poi/hssf/model/TestSheet.java
src/testcases/org/apache/poi/hssf/model/TestSheetAdditional.java
src/testcases/org/apache/poi/hssf/record/aggregates/TestColumnInfoRecordsAggregate.java

index 95cab0e78187ba9beec03c7f56a9baf87b8753b1..02d75184a9d0eee44a668ca84bb697241dc5bbf6 100644 (file)
@@ -37,6 +37,7 @@
 
                <!-- Don't forget to update status.xml too! -->
         <release version="3.1.1-alpha1" date="2008-??-??">
+           <action dev="POI-DEVELOPERS" type="fix">45639 - Fixed AIOOBE due to bad index logic in ColumnInfoRecordsAggregate</action>
            <action dev="POI-DEVELOPERS" type="fix">Fixed special cases of INDEX function (single column/single row, errors)</action>
            <action dev="POI-DEVELOPERS" type="add">45761 - Support for Very Hidden excel sheets in HSSF</action>
            <action dev="POI-DEVELOPERS" type="add">45738 - Initial HWPF support for Office Art Shapes</action>
index e7fb447da7dfa2f3d7b15d584f0ac4ae6c09e26a..dab203e98d36b0898a881efdfcdab93a8e05cafc 100644 (file)
@@ -34,6 +34,7 @@
        <!-- Don't forget to update changes.xml too! -->
     <changes>
         <release version="3.1.1-alpha1" date="2008-??-??">
+           <action dev="POI-DEVELOPERS" type="fix">45639 - Fixed AIOOBE due to bad index logic in ColumnInfoRecordsAggregate</action>
            <action dev="POI-DEVELOPERS" type="fix">Fixed special cases of INDEX function (single column/single row, errors)</action>
            <action dev="POI-DEVELOPERS" type="add">45761 - Support for Very Hidden excel sheets in HSSF</action>
            <action dev="POI-DEVELOPERS" type="add">45738 - Initial HWPF support for Office Art Shapes</action>
index 388f1a6ee4f0225e5463bccfc2b9005a1118e678..8bbc30435a0e5e1797a5a39664bbcfaecceec8fa 100644 (file)
@@ -1055,7 +1055,7 @@ public final class Sheet implements Model {
 
         ColumnInfoRecord ci = _columnInfos.findColumnInfo(columnIndex);
         if (ci != null) {
-            return ci.getColumnWidth();
+            return (short)ci.getColumnWidth();
         }
         //default column width is measured in characters
         //multiply
@@ -1079,8 +1079,8 @@ public final class Sheet implements Model {
     public short getXFIndexForColAt(short columnIndex) {
         ColumnInfoRecord ci = _columnInfos.findColumnInfo(columnIndex);
         if (ci != null) {
-            return ci.getXFIndex();
-         }
+            return (short)ci.getXFIndex();
+        }
         return 0xF;
     }
 
@@ -1138,8 +1138,7 @@ public final class Sheet implements Model {
      * @param indent        if true the group will be indented by one level,
      *                      if false indenting will be removed by one level.
      */
-    public void groupColumnRange(short fromColumn, short toColumn, boolean indent)
-    {
+    public void groupColumnRange(int fromColumn, int toColumn, boolean indent) {
 
         // Set the level for each column
         _columnInfos.groupColumnRange( fromColumn, toColumn, indent);
@@ -1709,17 +1708,13 @@ public final class Sheet implements Model {
     }
 
 
-    public void setColumnGroupCollapsed( short columnNumber, boolean collapsed )
-    {
-        if (collapsed)
-        {
-            _columnInfos.collapseColumn( columnNumber );
-        }
-        else
-        {
-            _columnInfos.expandColumn( columnNumber );
-        }
-    }
+    public void setColumnGroupCollapsed(int columnNumber, boolean collapsed) {
+               if (collapsed) {
+                       _columnInfos.collapseColumn(columnNumber);
+               } else {
+                       _columnInfos.expandColumn(columnNumber);
+               }
+       }
 
     /**
      * protect a spreadsheet with a password (not encypted, just sets protect
index b77dca3e174663e1ecc363ec9ce3a773c84863b8..32aef3a6c32e0dc39c4ba7002427131b20d1ae0f 100644 (file)
@@ -17,6 +17,7 @@
 
 package org.apache.poi.hssf.record;
 
+import org.apache.poi.util.HexDump;
 import org.apache.poi.util.LittleEndian;
 import org.apache.poi.util.BitField;
 import org.apache.poi.util.BitFieldFactory;
@@ -30,19 +31,24 @@ import org.apache.poi.util.BitFieldFactory;
  */
 public final class ColumnInfoRecord extends Record {
     public static final short     sid = 0x7d;
-    private short                 field_1_first_col;
-    private short                 field_2_last_col;
-    private short                 field_3_col_width;
-    private short                 field_4_xf_index;
-    private short                 field_5_options;
+    private int field_1_first_col;
+    private int field_2_last_col;
+    private int field_3_col_width;
+    private int field_4_xf_index;
+    private int field_5_options;
     private static final BitField hidden    = BitFieldFactory.getInstance(0x01);
     private static final BitField outlevel  = BitFieldFactory.getInstance(0x0700);
     private static final BitField collapsed = BitFieldFactory.getInstance(0x1000);
     // Excel seems write values 2, 10, and 260, even though spec says "must be zero"
     private short                 field_6_reserved;
 
-    public ColumnInfoRecord()
-    {
+    /**
+     * Creates a column info record with default width and format
+     */
+    public ColumnInfoRecord() {
+        setColumnWidth(2275);
+        field_5_options = 2; 
+        field_4_xf_index = 0x0f;
         field_6_reserved = 2; // seems to be the most common value
     }
 
@@ -90,7 +96,7 @@ public final class ColumnInfoRecord extends Record {
      * @param fc - the first column index (0-based)
      */
 
-    public void setFirstColumn(short fc)
+    public void setFirstColumn(int fc)
     {
         field_1_first_col = fc;
     }
@@ -100,7 +106,7 @@ public final class ColumnInfoRecord extends Record {
      * @param lc - the last column index (0-based)
      */
 
-    public void setLastColumn(short lc)
+    public void setLastColumn(int lc)
     {
         field_2_last_col = lc;
     }
@@ -110,7 +116,7 @@ public final class ColumnInfoRecord extends Record {
      * @param cw - column width
      */
 
-    public void setColumnWidth(short cw)
+    public void setColumnWidth(int cw)
     {
         field_3_col_width = cw;
     }
@@ -121,20 +127,11 @@ public final class ColumnInfoRecord extends Record {
      * @see org.apache.poi.hssf.record.ExtendedFormatRecord
      */
 
-    public void setXFIndex(short xfi)
+    public void setXFIndex(int xfi)
     {
         field_4_xf_index = xfi;
     }
 
-    /**
-     * set the options bitfield - use the bitsetters instead
-     * @param options - the bitfield raw value
-     */
-
-    public void setOptions(short options)
-    {
-        field_5_options = options;
-    }
 
     // start options bitfield
 
@@ -146,7 +143,7 @@ public final class ColumnInfoRecord extends Record {
 
     public void setHidden(boolean ishidden)
     {
-        field_5_options = hidden.setShortBoolean(field_5_options, ishidden);
+        field_5_options = hidden.setBoolean(field_5_options, ishidden);
     }
 
     /**
@@ -155,9 +152,9 @@ public final class ColumnInfoRecord extends Record {
      * @param olevel -outline level for the cells
      */
 
-    public void setOutlineLevel(short olevel)
+    public void setOutlineLevel(int olevel)
     {
-        field_5_options = outlevel.setShortValue(field_5_options, olevel);
+        field_5_options = outlevel.setValue(field_5_options, olevel);
     }
 
     /**
@@ -168,7 +165,7 @@ public final class ColumnInfoRecord extends Record {
 
     public void setCollapsed(boolean iscollapsed)
     {
-        field_5_options = collapsed.setShortBoolean(field_5_options,
+        field_5_options = collapsed.setBoolean(field_5_options,
                                                     iscollapsed);
     }
 
@@ -179,7 +176,7 @@ public final class ColumnInfoRecord extends Record {
      * @return the first column index (0-based)
      */
 
-    public short getFirstColumn()
+    public int getFirstColumn()
     {
         return field_1_first_col;
     }
@@ -189,7 +186,7 @@ public final class ColumnInfoRecord extends Record {
      * @return the last column index (0-based)
      */
 
-    public short getLastColumn()
+    public int getLastColumn()
     {
         return field_2_last_col;
     }
@@ -199,7 +196,7 @@ public final class ColumnInfoRecord extends Record {
      * @return column width
      */
 
-    public short getColumnWidth()
+    public int getColumnWidth()
     {
         return field_3_col_width;
     }
@@ -210,21 +207,18 @@ public final class ColumnInfoRecord extends Record {
      * @see org.apache.poi.hssf.record.ExtendedFormatRecord
      */
 
-    public short getXFIndex()
+    public int getXFIndex()
     {
         return field_4_xf_index;
     }
 
-    /**
-     * get the options bitfield - use the bitsetters instead
-     * @return the bitfield raw value
-     */
-
-    public short getOptions()
-    {
+    public int getOptions() {
         return field_5_options;
     }
-
+    public void setOptions(int field_5_options) {
+        this.field_5_options = field_5_options;
+    }
+    
     // start options bitfield
 
     /**
@@ -244,9 +238,9 @@ public final class ColumnInfoRecord extends Record {
      * @return outline level for the cells
      */
 
-    public short getOutlineLevel()
+    public int getOutlineLevel()
     {
-        return outlevel.getShortValue(field_5_options);
+        return outlevel.getValue(field_5_options);
     }
 
     /**
@@ -261,6 +255,31 @@ public final class ColumnInfoRecord extends Record {
     }
 
     // end options bitfield
+    
+    public boolean containsColumn(int columnIndex) {
+        return field_1_first_col <= columnIndex && columnIndex <= field_2_last_col; 
+    }
+    public boolean isAdjacentBefore(ColumnInfoRecord other) {
+        return field_2_last_col == other.field_1_first_col - 1;
+    }
+   
+    /**
+     * @return <code>true</code> if the format, options and column width match
+     */
+    public boolean formatMatches(ColumnInfoRecord other) {
+        if (field_4_xf_index != other.field_4_xf_index) {
+            return false;
+        }
+        if (field_5_options != other.field_5_options) {
+            return false;
+        }
+        if (field_3_col_width != other.field_3_col_width) {
+            return false;
+        }
+        return true;
+    }
+    
+    
     public short getSid()
     {
         return sid;
@@ -269,13 +288,13 @@ public final class ColumnInfoRecord extends Record {
     public int serialize(int offset, byte [] data)
     {
         LittleEndian.putShort(data, 0 + offset, sid);
-        LittleEndian.putShort(data, 2 + offset, ( short ) 12);
-        LittleEndian.putShort(data, 4 + offset, getFirstColumn());
-        LittleEndian.putShort(data, 6 + offset, getLastColumn());
-        LittleEndian.putShort(data, 8 + offset, getColumnWidth());
-        LittleEndian.putShort(data, 10 + offset, getXFIndex());
-        LittleEndian.putShort(data, 12 + offset, getOptions());
-        LittleEndian.putShort(data, 14 + offset, field_6_reserved);   
+        LittleEndian.putUShort(data, 2 + offset, 12);
+        LittleEndian.putUShort(data, 4 + offset, getFirstColumn());
+        LittleEndian.putUShort(data, 6 + offset, getLastColumn());
+        LittleEndian.putUShort(data, 8 + offset, getColumnWidth());
+        LittleEndian.putUShort(data, 10 + offset, getXFIndex());
+        LittleEndian.putUShort(data, 12 + offset, field_5_options);
+        LittleEndian.putUShort(data, 14 + offset, field_6_reserved);   
         return getRecordSize();
     }
 
@@ -286,24 +305,19 @@ public final class ColumnInfoRecord extends Record {
 
     public String toString()
     {
-        StringBuffer buffer = new StringBuffer();
-
-        buffer.append("[COLINFO]\n");
-        buffer.append("colfirst       = ").append(getFirstColumn())
-            .append("\n");
-        buffer.append("collast        = ").append(getLastColumn())
-            .append("\n");
-        buffer.append("colwidth       = ").append(getColumnWidth())
-            .append("\n");
-        buffer.append("xfindex        = ").append(getXFIndex()).append("\n");
-        buffer.append("options        = ").append(getOptions()).append("\n");
-        buffer.append("  hidden       = ").append(getHidden()).append("\n");
-        buffer.append("  olevel       = ").append(getOutlineLevel())
-            .append("\n");
-        buffer.append("  collapsed    = ").append(getCollapsed())
-            .append("\n");
-        buffer.append("[/COLINFO]\n");
-        return buffer.toString();
+        StringBuffer sb = new StringBuffer();
+
+        sb.append("[COLINFO]\n");
+        sb.append("  colfirst = ").append(getFirstColumn()).append("\n");
+        sb.append("  collast  = ").append(getLastColumn()).append("\n");
+        sb.append("  colwidth = ").append(getColumnWidth()).append("\n");
+        sb.append("  xfindex  = ").append(getXFIndex()).append("\n");
+        sb.append("  options  = ").append(HexDump.shortToHex(field_5_options)).append("\n");
+        sb.append("    hidden   = ").append(getHidden()).append("\n");
+        sb.append("    olevel   = ").append(getOutlineLevel()).append("\n");
+        sb.append("    collapsed= ").append(getCollapsed()).append("\n");
+        sb.append("[/COLINFO]\n");
+        return sb.toString();
     }
 
     public Object clone() {
index b24d8c5b451a6236d0635aa1781862ed052f04e5..0f405bd1115b041551af5b186e708968a0d70ee5 100644 (file)
 package org.apache.poi.hssf.record.aggregates;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 import org.apache.poi.hssf.model.RecordStream;
 import org.apache.poi.hssf.record.ColumnInfoRecord;
-import org.apache.poi.hssf.record.Record;
 
 /**
  * @author Glen Stampoultzis
- * @version $Id$
  */
 public final class ColumnInfoRecordsAggregate extends RecordAggregate {
+       /**
+        * List of {@link ColumnInfoRecord}s assumed to be in order 
+        */
        private final List records;
+       
+       
+       private static final class CIRComparator implements Comparator {
+               public static final Comparator instance = new CIRComparator();
+               private CIRComparator() {
+                       // enforce singleton
+               }
+               public int compare(Object a, Object b) {
+                       return compareColInfos((ColumnInfoRecord)a, (ColumnInfoRecord)b);
+               }
+               public static int compareColInfos(ColumnInfoRecord a, ColumnInfoRecord b) {
+                       return a.getFirstColumn()-b.getFirstColumn();
+               }
+       }
 
        /**
         * Creates an empty aggregate
@@ -37,486 +54,470 @@ public final class ColumnInfoRecordsAggregate extends RecordAggregate {
        public ColumnInfoRecordsAggregate() {
                records = new ArrayList();
        }
-    public ColumnInfoRecordsAggregate(RecordStream rs) {
-        this();
-        
-        while(rs.peekNextClass() == ColumnInfoRecord.class) {
-               records.add(rs.getNext());
-        }
-        if (records.size() < 1) {
-               throw new RuntimeException("No column info records found");
-        }
-   }
-
-    /**
-     * Performs a deep clone of the record
-     */
-    public Object clone()
-    {
-        ColumnInfoRecordsAggregate rec = new ColumnInfoRecordsAggregate();
-        for (int k = 0; k < records.size(); k++)
-        {
-            ColumnInfoRecord ci = ( ColumnInfoRecord ) records.get(k);
-            ci=(ColumnInfoRecord) ci.clone();
-            rec.insertColumn( ci );
-        }
-        return rec;
-    }
-
-    /**
-     * Inserts a column into the aggregate (at the end of the list).
-     */
-    public void insertColumn( ColumnInfoRecord col )
-    {
-        records.add( col );
-    }
-
-    /**
-     * Inserts a column into the aggregate (at the position specified
-     * by <code>idx</code>.
-     */
-    public void insertColumn( int idx, ColumnInfoRecord col )
-    {
-        records.add( idx, col );
-    }
-
-    public int getNumColumns( )
-    {
-        return records.size();
-    }
-
-    public void visitContainedRecords(RecordVisitor rv) {
-       int nItems = records.size();
-       if (nItems < 1) {
-               return;
-       }
-       for(int i=0; i<nItems; i++) {
-               rv.visitRecord((Record)records.get(i));
-       }
-    }
-
-    public int findStartOfColumnOutlineGroup(int idx)
-    {
-        // Find the start of the group.
-        ColumnInfoRecord columnInfo = (ColumnInfoRecord) records.get( idx );
-        int level = columnInfo.getOutlineLevel();
-        while (idx != 0)
-        {
-            ColumnInfoRecord prevColumnInfo = (ColumnInfoRecord) records.get( idx - 1 );
-            if (columnInfo.getFirstColumn() - 1 == prevColumnInfo.getLastColumn())
-            {
-                if (prevColumnInfo.getOutlineLevel() < level)
-                {
-                    break;
-                }
-                idx--;
-                columnInfo = prevColumnInfo;
-            }
-            else
-            {
-                break;
-            }
-        }
-
-        return idx;
-    }
-
-    public int findEndOfColumnOutlineGroup(int idx)
-    {
-        // Find the end of the group.
-        ColumnInfoRecord columnInfo = (ColumnInfoRecord) records.get( idx );
-        int level = columnInfo.getOutlineLevel();
-        while (idx < records.size() - 1)
-        {
-            ColumnInfoRecord nextColumnInfo = (ColumnInfoRecord) records.get( idx + 1 );
-            if (columnInfo.getLastColumn() + 1 == nextColumnInfo.getFirstColumn())
-            {
-                if (nextColumnInfo.getOutlineLevel() < level)
-                {
-                    break;
-                }
-                idx++;
-                columnInfo = nextColumnInfo;
-            }
-            else
-            {
-                break;
-            }
-        }
-
-        return idx;
-    }
-
-    private ColumnInfoRecord getColInfo(int idx) {
-        return (ColumnInfoRecord) records.get( idx );
-    }
-
-    public ColumnInfoRecord writeHidden( ColumnInfoRecord columnInfo, int idx, boolean hidden )
-    {
-        int level = columnInfo.getOutlineLevel();
-        while (idx < records.size())
-        {
-            columnInfo.setHidden( hidden );
-            if (idx + 1 < records.size())
-            {
-                ColumnInfoRecord nextColumnInfo = getColInfo(idx + 1);
-                if (columnInfo.getLastColumn() + 1 == nextColumnInfo.getFirstColumn())
-                {
-                    if (nextColumnInfo.getOutlineLevel() < level)
-                        break;
-                    columnInfo = nextColumnInfo;
-                }
-                else
-                {
-                    break;
-                }
-            }
-            idx++;
-        }
-        return columnInfo;
-    }
-
-    public boolean isColumnGroupCollapsed( int idx )
-    {
-        int endOfOutlineGroupIdx = findEndOfColumnOutlineGroup( idx );
-        if (endOfOutlineGroupIdx >= records.size())
-            return false;
-        if (getColInfo(endOfOutlineGroupIdx).getLastColumn() + 1 != getColInfo(endOfOutlineGroupIdx + 1).getFirstColumn())
-            return false;
-        else
-            return getColInfo(endOfOutlineGroupIdx+1).getCollapsed();
-    }
-
-
-    public boolean isColumnGroupHiddenByParent( int idx )
-    {
-        // Look out outline details of end
-        int endLevel;
-        boolean endHidden;
-        int endOfOutlineGroupIdx = findEndOfColumnOutlineGroup( idx );
-        if (endOfOutlineGroupIdx >= records.size())
-        {
-            endLevel = 0;
-            endHidden = false;
-        }
-        else if (getColInfo(endOfOutlineGroupIdx).getLastColumn() + 1 != getColInfo(endOfOutlineGroupIdx + 1).getFirstColumn())
-        {
-            endLevel = 0;
-            endHidden = false;
-        }
-        else
-        {
-            endLevel = getColInfo( endOfOutlineGroupIdx + 1).getOutlineLevel();
-            endHidden = getColInfo( endOfOutlineGroupIdx + 1).getHidden();
-        }
-
-        // Look out outline details of start
-        int startLevel;
-        boolean startHidden;
-        int startOfOutlineGroupIdx = findStartOfColumnOutlineGroup( idx );
-        if (startOfOutlineGroupIdx <= 0)
-        {
-            startLevel = 0;
-            startHidden = false;
-        }
-        else if (getColInfo(startOfOutlineGroupIdx).getFirstColumn() - 1 != getColInfo(startOfOutlineGroupIdx - 1).getLastColumn())
-        {
-            startLevel = 0;
-            startHidden = false;
-        }
-        else
-        {
-            startLevel = getColInfo( startOfOutlineGroupIdx - 1).getOutlineLevel();
-            startHidden = getColInfo( startOfOutlineGroupIdx - 1 ).getHidden();
-        }
-
-        if (endLevel > startLevel)
-        {
-            return endHidden;
-        }
-        else
-        {
-            return startHidden;
-        }
-    }
-
-    public void collapseColumn( short columnNumber )
-    {
-        int idx = findColumnIdx( columnNumber, 0 );
-        if (idx == -1)
-            return;
-
-        // Find the start of the group.
-        ColumnInfoRecord columnInfo = getColInfo( findStartOfColumnOutlineGroup( idx ) );
-
-        // Hide all the columns until the end of the group
-        columnInfo = writeHidden( columnInfo, idx, true );
-
-        // Write collapse field
-        setColumn( (short) ( columnInfo.getLastColumn() + 1 ), null, null, null, null, Boolean.TRUE);
-    }
-
-    public void expandColumn( short columnNumber )
-    {
-        int idx = findColumnIdx( columnNumber, 0 );
-        if (idx == -1)
-            return;
-
-        // If it is already exapanded do nothing.
-        if (!isColumnGroupCollapsed(idx))
-            return;
-
-        // Find the start of the group.
-        int startIdx = findStartOfColumnOutlineGroup( idx );
-        ColumnInfoRecord columnInfo = getColInfo( startIdx );
-
-        // Find the end of the group.
-        int endIdx = findEndOfColumnOutlineGroup( idx );
-        ColumnInfoRecord endColumnInfo = getColInfo( endIdx );
-
-        // expand:
-        // colapsed bit must be unset
-        // hidden bit gets unset _if_ surrounding groups are expanded you can determine
-        //   this by looking at the hidden bit of the enclosing group.  You will have
-        //   to look at the start and the end of the current group to determine which
-        //   is the enclosing group
-        // hidden bit only is altered for this outline level.  ie.  don't uncollapse contained groups
-        if (!isColumnGroupHiddenByParent( idx ))
-        {
-            for (int i = startIdx; i <= endIdx; i++)
-            {
-                if (columnInfo.getOutlineLevel() == getColInfo(i).getOutlineLevel())
-                    getColInfo(i).setHidden( false );
-            }
-        }
-
-        // Write collapse field
-        setColumn( (short) ( columnInfo.getLastColumn() + 1 ), null, null, null, null, Boolean.FALSE);
-    }
-
-    /**
-     * creates the ColumnInfo Record and sets it to a default column/width
-     * @see org.apache.poi.hssf.record.ColumnInfoRecord
-     * @return record containing a ColumnInfoRecord
-     */
-    public static ColumnInfoRecord createColInfo()
-    {
-        ColumnInfoRecord retval = new ColumnInfoRecord();
-
-        retval.setColumnWidth(( short ) 2275);
-        // was:       retval.setOptions(( short ) 6);
-        retval.setOptions(( short ) 2);
-        retval.setXFIndex(( short ) 0x0f);
-        return retval;
-    }
-
-
-    public void setColumn(short column, Short xfIndex, Short width, Integer level, Boolean hidden, Boolean collapsed)
-    {
-        ColumnInfoRecord ci = null;
-        int              k  = 0;
-
-        for (k = 0; k < records.size(); k++)
-        {
-            ci = ( ColumnInfoRecord ) records.get(k);
-            if ((ci.getFirstColumn() <= column)
-                    && (column <= ci.getLastColumn()))
-            {
-                break;
-            }
-            ci = null;
-        }
-
-        if (ci != null)
-        {
-           boolean styleChanged = xfIndex != null && ci.getXFIndex() != xfIndex.shortValue();
-            boolean widthChanged = width != null && ci.getColumnWidth() != width.shortValue();
-            boolean levelChanged = level != null && ci.getOutlineLevel() != level.intValue();
-            boolean hiddenChanged = hidden != null && ci.getHidden() != hidden.booleanValue();
-            boolean collapsedChanged = collapsed != null && ci.getCollapsed() != collapsed.booleanValue();
-            boolean columnChanged = styleChanged || widthChanged || levelChanged || hiddenChanged || collapsedChanged;
-            if (!columnChanged)
-            {
-                // do nothing...nothing changed.
-            }
-            else if ((ci.getFirstColumn() == column)
-                     && (ci.getLastColumn() == column))
-            {                               // if its only for this cell then
-                setColumnInfoFields( ci, xfIndex, width, level, hidden, collapsed );
-            }
-            else if ((ci.getFirstColumn() == column)
-                     || (ci.getLastColumn() == column))
-            {
-                // okay so the width is different but the first or last column == the column we'return setting
-                // we'll just divide the info and create a new one
-                if (ci.getFirstColumn() == column)
-                {
-                    ci.setFirstColumn(( short ) (column + 1));
-                }
-                else
-                {
-                    ci.setLastColumn(( short ) (column - 1));
-                }
-                ColumnInfoRecord nci = ( ColumnInfoRecord ) createColInfo();
-
-                nci.setFirstColumn(column);
-                nci.setLastColumn(column);
-                nci.setOptions(ci.getOptions());
-                nci.setXFIndex(ci.getXFIndex());
-                setColumnInfoFields( nci, xfIndex, width, level, hidden, collapsed );
-
-                insertColumn(k, nci);
-            }
-            else
-            {
-                //split to 3 records
-                short lastcolumn = ci.getLastColumn();
-                ci.setLastColumn(( short ) (column - 1));
-
-                ColumnInfoRecord nci = ( ColumnInfoRecord ) createColInfo();
-                nci.setFirstColumn(column);
-                nci.setLastColumn(column);
-                nci.setOptions(ci.getOptions());
-                nci.setXFIndex(ci.getXFIndex());
-                setColumnInfoFields( nci, xfIndex, width, level, hidden, collapsed );
-                insertColumn(++k, nci);
-
-                nci = ( ColumnInfoRecord ) createColInfo();
-                nci.setFirstColumn((short)(column+1));
-                nci.setLastColumn(lastcolumn);
-                nci.setOptions(ci.getOptions());
-                nci.setXFIndex(ci.getXFIndex());
-                nci.setColumnWidth(ci.getColumnWidth());
-                insertColumn(++k, nci);
-            }
-        }
-        else
-        {
-
-            // okay so there ISN'T a column info record that cover's this column so lets create one!
-            ColumnInfoRecord nci = ( ColumnInfoRecord ) createColInfo();
-
-            nci.setFirstColumn(column);
-            nci.setLastColumn(column);
-            setColumnInfoFields( nci, xfIndex, width, level, hidden, collapsed );
-            insertColumn(k, nci);
-        }
-    }
-
-    /**
-     * Sets all non null fields into the <code>ci</code> parameter.
-     */
-    private void setColumnInfoFields( ColumnInfoRecord ci, Short xfStyle, Short width, Integer level, Boolean hidden, Boolean collapsed )
-    {
-       if (xfStyle != null)
-           ci.setXFIndex(xfStyle.shortValue());
-        if (width != null)
-            ci.setColumnWidth(width.shortValue());
-        if (level != null)
-            ci.setOutlineLevel( level.shortValue() );
-        if (hidden != null)
-            ci.setHidden( hidden.booleanValue() );
-        if (collapsed != null)
-            ci.setCollapsed( collapsed.booleanValue() );
-    }
-
-    private int findColumnIdx(int column, int fromIdx)
-    {
-        if (column < 0)
-            throw new IllegalArgumentException( "column parameter out of range: " + column );
-        if (fromIdx < 0)
-            throw new IllegalArgumentException( "fromIdx parameter out of range: " + fromIdx );
-
-        ColumnInfoRecord ci;
-        for (int k = fromIdx; k < records.size(); k++)
-        {
-            ci = getColInfo(k);
-            if ((ci.getFirstColumn() <= column)
-                    && (column <= ci.getLastColumn()))
-            {
-                return k;
-            }
-            ci = null;
-        }
-        return -1;
-    }
-
-    public void collapseColInfoRecords( int columnIdx )
-    {
-        if (columnIdx == 0)
-            return;
-        ColumnInfoRecord previousCol = getColInfo( columnIdx - 1);
-        ColumnInfoRecord currentCol = getColInfo( columnIdx );
-        boolean adjacentColumns = previousCol.getLastColumn() == currentCol.getFirstColumn() - 1;
-        if (!adjacentColumns)
-            return;
-
-        boolean columnsMatch =
-                previousCol.getXFIndex() == currentCol.getXFIndex() &&
-                previousCol.getOptions() == currentCol.getOptions() &&
-                previousCol.getColumnWidth() == currentCol.getColumnWidth();
-
-        if (columnsMatch)
-        {
-            previousCol.setLastColumn( currentCol.getLastColumn() );
-            records.remove( columnIdx );
-        }
-    }
-
-    /**
-     * Creates an outline group for the specified columns.
-     * @param fromColumn    group from this column (inclusive)
-     * @param toColumn      group to this column (inclusive)
-     * @param indent        if true the group will be indented by one level,
-     *                      if false indenting will be removed by one level.
-     */
-    public void groupColumnRange(short fromColumn, short toColumn, boolean indent)
-    {
-
-        // Set the level for each column
-        int fromIdx = 0;
-        for (int i = fromColumn; i <= toColumn; i++)
-        {
-            int level = 1;
-            int columnIdx = findColumnIdx( i, Math.max(0,fromIdx) );
-            if (columnIdx != -1)
-            {
-                level = getColInfo(columnIdx).getOutlineLevel();
-                if (indent) level++; else level--;
-                level = Math.max(0, level);
-                level = Math.min(7, level);
-                fromIdx = columnIdx - 1; // subtract 1 just in case this column is collapsed later.
-            }
-            setColumn((short)i, null, null, new Integer(level), null, null);
-            columnIdx = findColumnIdx( i, Math.max(0, fromIdx ) );
-            collapseColInfoRecords( columnIdx );
-        }
-
-    }
-    /**
-     * Finds the <tt>ColumnInfoRecord</tt> which contains the specified columnIndex
-     * @param columnIndex index of the column (not the index of the ColumnInfoRecord)
-     * @return <code>null</code> if no column info found for the specified column
-     */
+       public ColumnInfoRecordsAggregate(RecordStream rs) {
+               this();
+
+               boolean isInOrder = true;
+               ColumnInfoRecord cirPrev = null;
+               while(rs.peekNextClass() == ColumnInfoRecord.class) {
+                       ColumnInfoRecord cir = (ColumnInfoRecord) rs.getNext();
+                       records.add(cir);
+                       if (cirPrev != null && CIRComparator.compareColInfos(cirPrev, cir) > 0) {
+                               isInOrder = false;
+                       }
+                       cirPrev = cir;
+               }
+               if (records.size() < 1) {
+                       throw new RuntimeException("No column info records found");
+               }
+               if (!isInOrder) {
+                       Collections.sort(records, CIRComparator.instance);
+               }
+       }
+
+       /**
+        * Performs a deep clone of the record
+        */
+       public Object clone() {
+               ColumnInfoRecordsAggregate rec = new ColumnInfoRecordsAggregate();
+               for (int k = 0; k < records.size(); k++) {
+                       ColumnInfoRecord ci = ( ColumnInfoRecord ) records.get(k);
+                       rec.records.add(ci.clone());
+               }
+               return rec;
+       }
+
+       /**
+        * Inserts a column into the aggregate (at the end of the list).
+        */
+       public void insertColumn(ColumnInfoRecord col) {
+               records.add(col);
+               Collections.sort(records, CIRComparator.instance);
+       }
+
+       /**
+        * Inserts a column into the aggregate (at the position specified by
+        * <code>idx</code>.
+        */
+       private void insertColumn(int idx, ColumnInfoRecord col) {
+               records.add(idx, col);
+       }
+
+       /* package */ int getNumColumns() {
+               return records.size();
+       }
+
+       public void visitContainedRecords(RecordVisitor rv) {
+               int nItems = records.size();
+               if (nItems < 1) {
+                       return;
+               }
+               ColumnInfoRecord cirPrev = null;
+               for(int i=0; i<nItems; i++) {
+                       ColumnInfoRecord cir = (ColumnInfoRecord)records.get(i);
+                       rv.visitRecord(cir);
+                       if (cirPrev != null && CIRComparator.compareColInfos(cirPrev, cir) > 0) {
+                               // Excel probably wouldn't mind, but there is much logic in this class
+                               // that assumes the column info records are kept in order
+                               throw new RuntimeException("Column info records are out of order");
+                       }
+                       cirPrev = cir;
+               }
+       }
+
+       private int findStartOfColumnOutlineGroup(int pIdx) {
+               // Find the start of the group.
+               ColumnInfoRecord columnInfo = (ColumnInfoRecord) records.get(pIdx);
+               int level = columnInfo.getOutlineLevel();
+               int idx = pIdx;
+               while (idx != 0) {
+                       ColumnInfoRecord prevColumnInfo = (ColumnInfoRecord) records.get(idx - 1);
+                       if (!prevColumnInfo.isAdjacentBefore(columnInfo)) {
+                               break;
+                       }
+                       if (prevColumnInfo.getOutlineLevel() < level) {
+                               break;
+                       }
+                       idx--;
+                       columnInfo = prevColumnInfo;
+               }
+
+               return idx;
+       }
+
+       private int findEndOfColumnOutlineGroup(int colInfoIndex) {
+               // Find the end of the group.
+               ColumnInfoRecord columnInfo = (ColumnInfoRecord) records.get(colInfoIndex);
+               int level = columnInfo.getOutlineLevel();
+               int idx = colInfoIndex;
+               while (idx < records.size() - 1) {
+                       ColumnInfoRecord nextColumnInfo = (ColumnInfoRecord) records.get(idx + 1);
+                       if (!columnInfo.isAdjacentBefore(nextColumnInfo)) {
+                               break;
+                       }
+                       if (nextColumnInfo.getOutlineLevel() < level) {
+                               break;
+                       }
+                       idx++;
+                       columnInfo = nextColumnInfo;
+               }
+               return idx;
+       }
+
+       private ColumnInfoRecord getColInfo(int idx) {
+               return (ColumnInfoRecord) records.get( idx );
+       }
+
+       /**
+        * 'Collapsed' state is stored in a single column col info record immediately after the outline group
+        * @param idx
+        * @return
+        */
+       private boolean isColumnGroupCollapsed(int idx) {
+               int endOfOutlineGroupIdx = findEndOfColumnOutlineGroup(idx);
+               int nextColInfoIx = endOfOutlineGroupIdx+1;
+               if (nextColInfoIx >= records.size()) {
+                       return false;
+               }
+               ColumnInfoRecord nextColInfo = getColInfo(nextColInfoIx);
+               if (!getColInfo(endOfOutlineGroupIdx).isAdjacentBefore(nextColInfo)) {
+                       return false;
+               }
+               return nextColInfo.getCollapsed();
+       }
+
+
+       private boolean isColumnGroupHiddenByParent(int idx) {
+               // Look out outline details of end
+               int endLevel = 0;
+               boolean endHidden = false;
+               int endOfOutlineGroupIdx = findEndOfColumnOutlineGroup( idx );
+               if (endOfOutlineGroupIdx < records.size()) {
+                       ColumnInfoRecord nextInfo = getColInfo(endOfOutlineGroupIdx + 1);
+                       if (getColInfo(endOfOutlineGroupIdx).isAdjacentBefore(nextInfo)) {
+                               endLevel = nextInfo.getOutlineLevel();
+                               endHidden = nextInfo.getHidden();
+                       }
+               }
+               // Look out outline details of start
+               int startLevel = 0;
+               boolean startHidden = false;
+               int startOfOutlineGroupIdx = findStartOfColumnOutlineGroup( idx );
+               if (startOfOutlineGroupIdx > 0) {
+                       ColumnInfoRecord prevInfo = getColInfo(startOfOutlineGroupIdx - 1);
+                       if (prevInfo.isAdjacentBefore(getColInfo(startOfOutlineGroupIdx))) {
+                               startLevel = prevInfo.getOutlineLevel();
+                               startHidden = prevInfo.getHidden();
+                       }
+               }
+               if (endLevel > startLevel) {
+                       return endHidden;
+               }
+               return startHidden;
+       }
+
+       public void collapseColumn(int columnIndex) {
+               int colInfoIx = findColInfoIdx(columnIndex, 0);
+               if (colInfoIx == -1) {
+                       return;
+               }
+
+               // Find the start of the group.
+               int groupStartColInfoIx = findStartOfColumnOutlineGroup(colInfoIx);
+               ColumnInfoRecord columnInfo = getColInfo(groupStartColInfoIx);
+
+               // Hide all the columns until the end of the group
+               int lastColIx = setGroupHidden(groupStartColInfoIx, columnInfo.getOutlineLevel(), true);
+
+               // Write collapse field
+               setColumn(lastColIx + 1, null, null, null, null, Boolean.TRUE);
+       }
+       /**
+        * Sets all adjacent columns of the same outline level to the specified hidden status.
+        * @param pIdx the col info index of the start of the outline group
+        * @return the column index of the last column in the outline group
+        */
+       private int setGroupHidden(int pIdx, int level, boolean hidden) {
+               int idx = pIdx;
+               ColumnInfoRecord columnInfo = getColInfo(idx);
+               while (idx < records.size()) {
+                       columnInfo.setHidden(hidden);
+                       if (idx + 1 < records.size()) {
+                               ColumnInfoRecord nextColumnInfo = getColInfo(idx + 1);
+                               if (!columnInfo.isAdjacentBefore(nextColumnInfo)) {
+                                       break;
+                               }
+                               if (nextColumnInfo.getOutlineLevel() < level) {
+                                       break;
+                               }
+                               columnInfo = nextColumnInfo;
+                       }
+                       idx++;
+               }
+               return columnInfo.getLastColumn();
+       }
+
+
+       public void expandColumn(int columnIndex) {
+               int idx = findColInfoIdx(columnIndex, 0);
+               if (idx == -1) {
+                       return;
+               }
+
+               // If it is already expanded do nothing.
+               if (!isColumnGroupCollapsed(idx)) {
+                       return;
+               }
+
+               // Find the start/end of the group.
+               int startIdx = findStartOfColumnOutlineGroup(idx);
+               int endIdx = findEndOfColumnOutlineGroup(idx);
+
+               // expand:
+               // colapsed bit must be unset
+               // hidden bit gets unset _if_ surrounding groups are expanded you can determine
+               //   this by looking at the hidden bit of the enclosing group.  You will have
+               //   to look at the start and the end of the current group to determine which
+               //   is the enclosing group
+               // hidden bit only is altered for this outline level.  ie.  don't uncollapse contained groups
+               ColumnInfoRecord columnInfo = getColInfo(endIdx);
+               if (!isColumnGroupHiddenByParent(idx)) {
+                       int outlineLevel = columnInfo.getOutlineLevel();
+                       for (int i = startIdx; i <= endIdx; i++) {
+                               ColumnInfoRecord ci = getColInfo(i);
+                               if (outlineLevel == ci.getOutlineLevel())
+                                       ci.setHidden(false);
+                       }
+               }
+
+               // Write collapse flag (stored in a single col info record after this outline group)
+               setColumn(columnInfo.getLastColumn() + 1, null, null, null, null, Boolean.FALSE);
+       }
+
+       private static ColumnInfoRecord copyColInfo(ColumnInfoRecord ci) {
+               return (ColumnInfoRecord) ci.clone();
+       }
+
+
+       public void setColumn(int targetColumnIx, Short xfIndex, Short width, 
+                                       Integer level, Boolean hidden, Boolean collapsed) {
+               ColumnInfoRecord ci = null;
+               int k  = 0;
+
+               for (k = 0; k < records.size(); k++) {
+                       ColumnInfoRecord tci = (ColumnInfoRecord) records.get(k);
+                       if (tci.containsColumn(targetColumnIx)) {
+                               ci = tci;
+                               break;
+                       }
+                       if (tci.getFirstColumn() > targetColumnIx) {
+                               // call column infos after k are for later columns
+                               break; // exit now so k will be the correct insert pos
+                       }
+               }
+
+               if (ci == null) {
+                       // okay so there ISN'T a column info record that covers this column so lets create one!
+                       ColumnInfoRecord nci = new ColumnInfoRecord();
+
+                       nci.setFirstColumn(targetColumnIx);
+                       nci.setLastColumn(targetColumnIx);
+                       setColumnInfoFields( nci, xfIndex, width, level, hidden, collapsed );
+                       insertColumn(k, nci);
+                       attemptMergeColInfoRecords(k);
+                       return;
+               }
+
+               boolean styleChanged = xfIndex != null && ci.getXFIndex() != xfIndex.shortValue();
+               boolean widthChanged = width != null && ci.getColumnWidth() != width.shortValue();
+               boolean levelChanged = level != null && ci.getOutlineLevel() != level.intValue();
+               boolean hiddenChanged = hidden != null && ci.getHidden() != hidden.booleanValue();
+               boolean collapsedChanged = collapsed != null && ci.getCollapsed() != collapsed.booleanValue();
+
+               boolean columnChanged = styleChanged || widthChanged || levelChanged || hiddenChanged || collapsedChanged;
+               if (!columnChanged) {
+                       // do nothing...nothing changed.
+                       return;
+               }
+
+               if (ci.getFirstColumn() == targetColumnIx && ci.getLastColumn() == targetColumnIx) {
+                       // ColumnInfo ci for a single column, the target column
+                       setColumnInfoFields(ci, xfIndex, width, level, hidden, collapsed);
+                       attemptMergeColInfoRecords(k);
+                       return;
+               }
+
+               if (ci.getFirstColumn() == targetColumnIx || ci.getLastColumn() == targetColumnIx) {
+                       // The target column is at either end of the multi-column ColumnInfo ci
+                       // we'll just divide the info and create a new one
+                       if (ci.getFirstColumn() == targetColumnIx) {
+                               ci.setFirstColumn(targetColumnIx + 1);
+                       } else {
+                               ci.setLastColumn(targetColumnIx - 1);
+                               k++; // adjust insert pos to insert after
+                       }
+                       ColumnInfoRecord nci = copyColInfo(ci);
+
+                       nci.setFirstColumn(targetColumnIx);
+                       nci.setLastColumn(targetColumnIx);
+                       setColumnInfoFields( nci, xfIndex, width, level, hidden, collapsed );
+
+                       insertColumn(k, nci);
+                       attemptMergeColInfoRecords(k);
+               } else {
+                       //split to 3 records
+                       ColumnInfoRecord ciStart = ci;
+                       ColumnInfoRecord ciMid = copyColInfo(ci);
+                       ColumnInfoRecord ciEnd = copyColInfo(ci);
+                       int lastcolumn = ci.getLastColumn();
+                       
+                       ciStart.setLastColumn(targetColumnIx - 1);
+
+                       ciMid.setFirstColumn(targetColumnIx);
+                       ciMid.setLastColumn(targetColumnIx);
+                       setColumnInfoFields(ciMid, xfIndex, width, level, hidden, collapsed);
+                       insertColumn(++k, ciMid);
+                       
+                       ciEnd.setFirstColumn(targetColumnIx+1);
+                       ciEnd.setLastColumn(lastcolumn);
+                       insertColumn(++k, ciEnd);
+                       // no need to attemptMergeColInfoRecords because we 
+                       // know both on each side are different
+               }
+       }
+
+       /**
+        * Sets all non null fields into the <code>ci</code> parameter.
+        */
+       private static void setColumnInfoFields( ColumnInfoRecord ci, Short xfStyle, Short width, 
+                               Integer level, Boolean hidden, Boolean collapsed ) {
+               if (xfStyle != null) {
+                       ci.setXFIndex(xfStyle.shortValue());
+               }
+               if (width != null) {
+                       ci.setColumnWidth(width.shortValue());
+               }
+               if (level != null) {
+                       ci.setOutlineLevel( level.shortValue() );
+               }
+               if (hidden != null) {
+                       ci.setHidden( hidden.booleanValue() );
+               }
+               if (collapsed != null) {
+                       ci.setCollapsed( collapsed.booleanValue() );
+               }
+       }
+
+       private int findColInfoIdx(int columnIx, int fromColInfoIdx) {
+               if (columnIx < 0) {
+                       throw new IllegalArgumentException( "column parameter out of range: " + columnIx );
+               }
+               if (fromColInfoIdx < 0) {
+                       throw new IllegalArgumentException( "fromIdx parameter out of range: " + fromColInfoIdx );
+               }
+
+               for (int k = fromColInfoIdx; k < records.size(); k++) {
+                       ColumnInfoRecord ci = getColInfo(k);
+                       if (ci.containsColumn(columnIx)) {
+                               return k;
+                       }
+                       if (ci.getFirstColumn() > columnIx) {
+                               break;
+                       }
+               }
+               return -1;
+       }
+
+       /**
+        * Attempts to merge the col info record at the specified index 
+        * with either or both of its neighbours
+        */
+       private void attemptMergeColInfoRecords(int colInfoIx) {
+               int nRecords = records.size();
+               if (colInfoIx < 0 || colInfoIx >= nRecords) {
+                       throw new IllegalArgumentException("colInfoIx " + colInfoIx 
+                                       + " is out of range (0.." + (nRecords-1) + ")");
+               }
+               ColumnInfoRecord currentCol = getColInfo(colInfoIx);
+               int nextIx = colInfoIx+1;
+               if (nextIx < nRecords) {
+                       if (mergeColInfoRecords(currentCol, getColInfo(nextIx))) {
+                       records.remove(nextIx);
+                       }
+               }
+               if (colInfoIx > 0) {
+                       if (mergeColInfoRecords(getColInfo(colInfoIx - 1), currentCol)) {
+                       records.remove(colInfoIx);
+               }
+               }
+       }
+       /**
+        * merges two column info records (if they are adjacent and have the same formatting, etc)
+        * @return <code>false</code> if the two column records could not be merged
+        */
+       private static boolean mergeColInfoRecords(ColumnInfoRecord ciA, ColumnInfoRecord ciB) {
+               if (ciA.isAdjacentBefore(ciB) && ciA.formatMatches(ciB)) {
+                       ciA.setLastColumn(ciB.getLastColumn());
+                       return true;
+               }
+               return false;
+       }
+       /**
+        * Creates an outline group for the specified columns, by setting the level
+        * field for each col info record in the range. {@link ColumnInfoRecord}s
+        * may be created, split or merged as a result of this operation.
+        * 
+        * @param fromColumnIx
+        *            group from this column (inclusive)
+        * @param toColumnIx
+        *            group to this column (inclusive)
+        * @param indent
+        *            if <code>true</code> the group will be indented by one
+        *            level, if <code>false</code> indenting will be decreased by
+        *            one level.
+        */
+       public void groupColumnRange(int fromColumnIx, int toColumnIx, boolean indent) {
+
+               int colInfoSearchStartIdx = 0; // optimization to speed up the search for col infos
+               for (int i = fromColumnIx; i <= toColumnIx; i++) {
+                       int level = 1;
+                       int colInfoIdx = findColInfoIdx(i, colInfoSearchStartIdx);
+                       if (colInfoIdx != -1) {
+                               level = getColInfo(colInfoIdx).getOutlineLevel();
+                               if (indent) {
+                                       level++;
+                               } else {
+                                       level--;
+                               }
+                               level = Math.max(0, level);
+                               level = Math.min(7, level);
+                               colInfoSearchStartIdx = Math.max(0, colInfoIdx - 1); // -1 just in case this column is collapsed later.
+                       }
+                       setColumn(i, null, null, new Integer(level), null, null);
+               }
+       }
+       /**
+        * Finds the <tt>ColumnInfoRecord</tt> which contains the specified columnIndex
+        * @param columnIndex index of the column (not the index of the ColumnInfoRecord)
+        * @return <code>null</code> if no column info found for the specified column
+        */
        public ColumnInfoRecord findColumnInfo(int columnIndex) {
                int nInfos = records.size();
                for(int i=0; i< nInfos; i++) {
                        ColumnInfoRecord ci = getColInfo(i);
-                       if (ci.getFirstColumn() <= columnIndex && columnIndex <= ci.getLastColumn()) {
+                       if (ci.containsColumn(columnIndex)) {
                                return ci;
                        }
                }
                return null;
        }
        public int getMaxOutlineLevel() {
-        int result = 0;
-        int count=records.size();
-        for (int i=0; i<count; i++) {
-            ColumnInfoRecord columnInfoRecord = getColInfo(i);
-            result = Math.max(columnInfoRecord.getOutlineLevel(), result);
-        }
-        return result;
+               int result = 0;
+               int count=records.size();
+               for (int i=0; i<count; i++) {
+                       ColumnInfoRecord columnInfoRecord = getColInfo(i);
+                       result = Math.max(columnInfoRecord.getOutlineLevel(), result);
+               }
+               return result;
        }
-
-
 }
index 807afae1606b3f4c652749def89962db4b346d77..3480dbe6a38fe224a7ccda6280bc21188525889c 100644 (file)
@@ -384,7 +384,7 @@ public final class HSSFSheet {
         
         for(int index=0; index<records.size(); index++) {
            if(records.get(index) instanceof DVRecord) {
-                  dvRecords.add(records.get(index));
+               dvRecords.add(records.get(index));
            }
         }
         return dvRecords;
@@ -1595,15 +1595,33 @@ public final class HSSFSheet {
         return patriarch;
     }
 
+    /**
+     * @deprecated (Sep 2008) use {@link #setColumnGroupCollapsed(int, boolean)}
+     */
+    public void setColumnGroupCollapsed(short columnNumber, boolean collapsed) {
+        setColumnGroupCollapsed(columnNumber & 0xFFFF, collapsed);
+    }
+    /**
+     * @deprecated (Sep 2008) use {@link #groupColumn(int, int)}
+     */
+    public void groupColumn(short fromColumn, short toColumn) {
+        groupColumn(fromColumn & 0xFFFF, toColumn & 0xFFFF);
+    }
+    /**
+     * @deprecated (Sep 2008) use {@link #ungroupColumn(int, int)}
+     */
+    public void ungroupColumn(short fromColumn, short toColumn) {
+        ungroupColumn(fromColumn & 0xFFFF, toColumn & 0xFFFF);
+    }
+
     /**
      * Expands or collapses a column group.
      *
      * @param columnNumber      One of the columns in the group.
      * @param collapsed         true = collapse group, false = expand group.
      */
-    public void setColumnGroupCollapsed( short columnNumber, boolean collapsed )
-    {
-        sheet.setColumnGroupCollapsed( columnNumber, collapsed );
+    public void setColumnGroupCollapsed(int columnNumber, boolean collapsed) {
+        sheet.setColumnGroupCollapsed(columnNumber, collapsed);
     }
 
     /**
@@ -1612,14 +1630,12 @@ public final class HSSFSheet {
      * @param fromColumn        beginning of the column range.
      * @param toColumn          end of the column range.
      */
-    public void groupColumn(short fromColumn, short toColumn)
-    {
-        sheet.groupColumnRange( fromColumn, toColumn, true );
+    public void groupColumn(int fromColumn, int toColumn) {
+        sheet.groupColumnRange(fromColumn, toColumn, true);
     }
 
-    public void ungroupColumn( short fromColumn, short toColumn )
-    {
-        sheet.groupColumnRange( fromColumn, toColumn, false );
+    public void ungroupColumn(int fromColumn, int toColumn) {
+        sheet.groupColumnRange(fromColumn, toColumn, false);
     }
 
     public void groupRow(int fromRow, int toRow)
index ca6a10f45e8ddb9835f8de24b01948e71ce4cb13..8321774f3265c0f01086004254044ce8cba657c4 100644 (file)
@@ -357,7 +357,7 @@ public final class TestSheet extends TestCase {
         xfindex = sheet.getXFIndexForColAt((short) 1);
         assertEquals(DEFAULT_IDX, xfindex);
 
-        ColumnInfoRecord nci = ColumnInfoRecordsAggregate.createColInfo();
+        ColumnInfoRecord nci = new ColumnInfoRecord();
         sheet._columnInfos.insertColumn(nci);
 
         // single column ColumnInfoRecord
@@ -567,7 +567,7 @@ public final class TestSheet extends TestCase {
 
         sheet.setMargin(HSSFSheet.LeftMargin, 0.3);
         try {
-            row.createCell((short) 0);
+            row.createCell(0);
         } catch (IllegalStateException e) {
             if (e.getMessage().equals("Cannot create value records before row records exist")) {
                 throw new AssertionFailedError("Identified bug 45717");
@@ -576,4 +576,3 @@ public final class TestSheet extends TestCase {
         }
     }
 }
-
index 0962b9148aaf3feee84298a62cbda598b8d007c4..477908dc7295deb685a37410f9c961f4dac5dc2f 100644 (file)
@@ -20,7 +20,6 @@ package org.apache.poi.hssf.model;
 import junit.framework.TestCase;
 
 import org.apache.poi.hssf.record.ColumnInfoRecord;
-import org.apache.poi.hssf.record.aggregates.ColumnInfoRecordsAggregate;
 
 /**
  * @author Tony Poppleton
@@ -29,7 +28,7 @@ public final class TestSheetAdditional extends TestCase {
        
        public void testGetCellWidth() {
                Sheet sheet = Sheet.createSheet();
-               ColumnInfoRecord nci = ColumnInfoRecordsAggregate.createColInfo();
+               ColumnInfoRecord nci = new ColumnInfoRecord();
 
                // Prepare test model
                nci.setFirstColumn((short)5);
@@ -55,5 +54,4 @@ public final class TestSheetAdditional extends TestCase {
                assertEquals((short)100,sheet.getColumnWidth((short)9));
                assertEquals((short)100,sheet.getColumnWidth((short)10));
        }
-
 }
index 2988290616d29cbda560e9d2fec9a902af0d520b..6c981cab5e74f3d4a7a0ca162ec55b49b897e19b 100644 (file)
 
 package org.apache.poi.hssf.record.aggregates;
 
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.AssertionFailedError;
 import junit.framework.TestCase;
+
 import org.apache.poi.hssf.record.ColumnInfoRecord;
+import org.apache.poi.hssf.record.Record;
 import org.apache.poi.hssf.record.RecordBase;
+import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor;
 
 /**
  * @author Glen Stampoultzis
@@ -28,11 +35,11 @@ public final class TestColumnInfoRecordsAggregate extends TestCase {
 
        public void testGetRecordSize() {
                ColumnInfoRecordsAggregate agg = new ColumnInfoRecordsAggregate();
-               agg.insertColumn(createColumn(1, 3));
-               agg.insertColumn(createColumn(4, 7));
-               agg.insertColumn(createColumn(8, 8));
+               agg.insertColumn(createColInfo(1, 3));
+               agg.insertColumn(createColInfo(4, 7));
+               agg.insertColumn(createColInfo(8, 8));
                agg.groupColumnRange((short) 2, (short) 5, true);
-               assertEquals(6, agg.getNumColumns());
+               assertEquals(4, agg.getNumColumns());
 
                confirmSerializedSize(agg);
 
@@ -48,10 +55,91 @@ public final class TestColumnInfoRecordsAggregate extends TestCase {
                assertEquals(estimatedSize, serializedSize);
        }
 
-       private static ColumnInfoRecord createColumn(int firstCol, int lastCol) {
+       private static ColumnInfoRecord createColInfo(int firstCol, int lastCol) {
                ColumnInfoRecord columnInfoRecord = new ColumnInfoRecord();
                columnInfoRecord.setFirstColumn((short) firstCol);
                columnInfoRecord.setLastColumn((short) lastCol);
                return columnInfoRecord;
        }
-}
\ No newline at end of file
+
+       private static final class CIRCollector implements RecordVisitor {
+
+               private List _list;
+               public CIRCollector() {
+                       _list = new ArrayList();
+               }
+               public void visitRecord(Record r) {
+                       _list.add(r);
+               }
+               public static ColumnInfoRecord[] getRecords(ColumnInfoRecordsAggregate agg) {
+                       CIRCollector circ = new CIRCollector();
+                       agg.visitContainedRecords(circ);
+                       List list = circ._list;
+                       ColumnInfoRecord[] result = new ColumnInfoRecord[list.size()];
+                       list.toArray(result);
+                       return result;
+               }
+       }
+
+       public void testGroupColumns_bug45639() {
+               ColumnInfoRecordsAggregate agg = new ColumnInfoRecordsAggregate();
+               agg.groupColumnRange( 7, 9, true);
+               agg.groupColumnRange( 4, 12, true);
+               try {
+                       agg.groupColumnRange( 1, 15, true);
+               } catch (ArrayIndexOutOfBoundsException e) {
+                       throw new AssertionFailedError("Identified bug 45639");
+               }
+               ColumnInfoRecord[] cirs = CIRCollector.getRecords(agg);
+               assertEquals(5, cirs.length);
+               confirmCIR(cirs, 0,  1,  3, 1, false, false);
+               confirmCIR(cirs, 1,  4,  6, 2, false, false);
+               confirmCIR(cirs, 2,  7,  9, 3, false, false);
+               confirmCIR(cirs, 3, 10, 12, 2, false, false);
+               confirmCIR(cirs, 4, 13, 15, 1, false, false);
+       }
+
+       /**
+        * Check that an inner group remains hidden
+        */
+       public void testHiddenAfterExpanding() {
+               ColumnInfoRecordsAggregate agg = new ColumnInfoRecordsAggregate();
+               agg.groupColumnRange(1, 15, true);
+               agg.groupColumnRange(4, 12, true);
+
+               ColumnInfoRecord[] cirs;
+
+               // collapse both inner and outer groups
+               agg.collapseColumn(6);
+               agg.collapseColumn(3);
+
+               cirs = CIRCollector.getRecords(agg);
+               assertEquals(5, cirs.length);
+               confirmCIR(cirs, 0,  1,  3, 1, true, false);
+               confirmCIR(cirs, 1,  4, 12, 2, true, false);
+               confirmCIR(cirs, 2, 13, 13, 1, true, true);
+               confirmCIR(cirs, 3, 14, 15, 1, true, false);
+               confirmCIR(cirs, 4, 16, 16, 0, false, true);
+
+               // just expand the inner group
+               agg.expandColumn(6);
+
+               cirs = CIRCollector.getRecords(agg);
+               assertEquals(4, cirs.length);
+               if (!cirs[1].getHidden()) {
+                       throw new AssertionFailedError("Inner group should still be hidden");
+               }
+               confirmCIR(cirs, 0,  1,  3, 1, true, false);
+               confirmCIR(cirs, 1,  4, 12, 2, true, false);
+               confirmCIR(cirs, 2, 13, 15, 1, true, false);
+               confirmCIR(cirs, 3, 16, 16, 0, false, true);
+       }
+       private static void confirmCIR(ColumnInfoRecord[] cirs, int ix, int startColIx, int endColIx, int level, boolean isHidden, boolean isCollapsed) {
+               ColumnInfoRecord cir = cirs[ix];
+               assertEquals("startColIx", startColIx, cir.getFirstColumn());
+               assertEquals("endColIx", endColIx, cir.getLastColumn());
+               assertEquals("level", level, cir.getOutlineLevel());
+               assertEquals("hidden", isHidden, cir.getHidden());
+               assertEquals("collapsed", isCollapsed, cir.getCollapsed());
+       }
+}