From f736644496248f93b487ba004af07670c31d5f12 Mon Sep 17 00:00:00 2001 From: Josh Micich Date: Thu, 11 Sep 2008 23:18:50 +0000 Subject: [PATCH] Fix for bug 45639 - cleaned up index logic inside ColumnInfoRecordsAggregate git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@694534 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/changes.xml | 1 + src/documentation/content/xdocs/status.xml | 1 + src/java/org/apache/poi/hssf/model/Sheet.java | 27 +- .../poi/hssf/record/ColumnInfoRecord.java | 138 +-- .../ColumnInfoRecordsAggregate.java | 875 +++++++++--------- .../apache/poi/hssf/usermodel/HSSFSheet.java | 36 +- .../org/apache/poi/hssf/model/TestSheet.java | 5 +- .../poi/hssf/model/TestSheetAdditional.java | 4 +- .../TestColumnInfoRecordsAggregate.java | 100 +- 9 files changed, 650 insertions(+), 537 deletions(-) diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index 95cab0e781..02d75184a9 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -37,6 +37,7 @@ + 45639 - Fixed AIOOBE due to bad index logic in ColumnInfoRecordsAggregate Fixed special cases of INDEX function (single column/single row, errors) 45761 - Support for Very Hidden excel sheets in HSSF 45738 - Initial HWPF support for Office Art Shapes diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index e7fb447da7..dab203e98d 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 45639 - Fixed AIOOBE due to bad index logic in ColumnInfoRecordsAggregate Fixed special cases of INDEX function (single column/single row, errors) 45761 - Support for Very Hidden excel sheets in HSSF 45738 - Initial HWPF support for Office Art Shapes diff --git a/src/java/org/apache/poi/hssf/model/Sheet.java b/src/java/org/apache/poi/hssf/model/Sheet.java index 388f1a6ee4..8bbc30435a 100644 --- a/src/java/org/apache/poi/hssf/model/Sheet.java +++ b/src/java/org/apache/poi/hssf/model/Sheet.java @@ -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 diff --git a/src/java/org/apache/poi/hssf/record/ColumnInfoRecord.java b/src/java/org/apache/poi/hssf/record/ColumnInfoRecord.java index b77dca3e17..32aef3a6c3 100644 --- a/src/java/org/apache/poi/hssf/record/ColumnInfoRecord.java +++ b/src/java/org/apache/poi/hssf/record/ColumnInfoRecord.java @@ -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 true 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(); + StringBuffer sb = 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(); + 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() { diff --git a/src/java/org/apache/poi/hssf/record/aggregates/ColumnInfoRecordsAggregate.java b/src/java/org/apache/poi/hssf/record/aggregates/ColumnInfoRecordsAggregate.java index b24d8c5b45..0f405bd111 100644 --- a/src/java/org/apache/poi/hssf/record/aggregates/ColumnInfoRecordsAggregate.java +++ b/src/java/org/apache/poi/hssf/record/aggregates/ColumnInfoRecordsAggregate.java @@ -18,18 +18,35 @@ 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"); - } - } + public ColumnInfoRecordsAggregate(RecordStream rs) { + this(); - /** - * 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; - } + 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); + } + } - /** - * Inserts a column into the aggregate (at the end of the list). - */ - public void insertColumn( ColumnInfoRecord col ) - { - records.add( col ); - } + /** + * 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 position specified - * by idx. - */ - public void insertColumn( int idx, ColumnInfoRecord col ) - { - records.add( idx, col ); - } + /** + * 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); + } - public int getNumColumns( ) - { - return records.size(); - } + /** + * Inserts a column into the aggregate (at the position specified by + * idx. + */ + private void insertColumn(int idx, ColumnInfoRecord col) { + records.add(idx, col); + } - public void visitContainedRecords(RecordVisitor rv) { - int nItems = records.size(); - if (nItems < 1) { - return; - } - for(int i=0; i 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; + } + } - return idx; - } + 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; + } - 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; + } - 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 ); - } + 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(); - } + /** + * '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(); + } - 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(); - } + 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; + } - // 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(); - } + public void collapseColumn(int columnIndex) { + int colInfoIx = findColInfoIdx(columnIndex, 0); + if (colInfoIx == -1) { + return; + } - if (endLevel > startLevel) - { - return endHidden; - } - else - { - return startHidden; - } - } + // Find the start of the group. + int groupStartColInfoIx = findStartOfColumnOutlineGroup(colInfoIx); + ColumnInfoRecord columnInfo = getColInfo(groupStartColInfoIx); - public void collapseColumn( short columnNumber ) - { - int idx = findColumnIdx( columnNumber, 0 ); - if (idx == -1) - return; + // Hide all the columns until the end of the group + int lastColIx = setGroupHidden(groupStartColInfoIx, columnInfo.getOutlineLevel(), true); - // 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; - } + // 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 setColumn(short column, Short xfIndex, Short width, Integer level, Boolean hidden, Boolean collapsed) - { - ColumnInfoRecord ci = null; - int k = 0; + public void expandColumn(int columnIndex) { + int idx = findColInfoIdx(columnIndex, 0); + if (idx == -1) { + return; + } - for (k = 0; k < records.size(); k++) - { - ci = ( ColumnInfoRecord ) records.get(k); - if ((ci.getFirstColumn() <= column) - && (column <= ci.getLastColumn())) - { - break; - } - ci = null; - } + // If it is already expanded do nothing. + if (!isColumnGroupCollapsed(idx)) { + return; + } - 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(); + // Find the start/end of the group. + int startIdx = findStartOfColumnOutlineGroup(idx); + int endIdx = findEndOfColumnOutlineGroup(idx); - nci.setFirstColumn(column); - nci.setLastColumn(column); - nci.setOptions(ci.getOptions()); - nci.setXFIndex(ci.getXFIndex()); - setColumnInfoFields( nci, xfIndex, width, level, hidden, collapsed ); + // 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); + } + } - insertColumn(k, nci); - } - else - { - //split to 3 records - short lastcolumn = ci.getLastColumn(); - ci.setLastColumn(( short ) (column - 1)); + // Write collapse flag (stored in a single col info record after this outline group) + setColumn(columnInfo.getLastColumn() + 1, null, null, null, null, Boolean.FALSE); + } - 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); + private static ColumnInfoRecord copyColInfo(ColumnInfoRecord ci) { + return (ColumnInfoRecord) ci.clone(); + } - 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(); + public void setColumn(int targetColumnIx, Short xfIndex, Short width, + Integer level, Boolean hidden, Boolean collapsed) { + ColumnInfoRecord ci = null; + int k = 0; - nci.setFirstColumn(column); - nci.setLastColumn(column); - setColumnInfoFields( nci, xfIndex, width, level, hidden, collapsed ); - insertColumn(k, nci); - } - } + 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 + } + } - /** - * Sets all non null fields into the ci 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() ); - } + if (ci == null) { + // okay so there ISN'T a column info record that covers this column so lets create one! + ColumnInfoRecord nci = new ColumnInfoRecord(); - 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 ); + nci.setFirstColumn(targetColumnIx); + nci.setLastColumn(targetColumnIx); + setColumnInfoFields( nci, xfIndex, width, level, hidden, collapsed ); + insertColumn(k, nci); + attemptMergeColInfoRecords(k); + return; + } - 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; - } + 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(); - 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 columnChanged = styleChanged || widthChanged || levelChanged || hiddenChanged || collapsedChanged; + if (!columnChanged) { + // do nothing...nothing changed. + return; + } - boolean columnsMatch = - previousCol.getXFIndex() == currentCol.getXFIndex() && - previousCol.getOptions() == currentCol.getOptions() && - previousCol.getColumnWidth() == currentCol.getColumnWidth(); + 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 (columnsMatch) - { - previousCol.setLastColumn( currentCol.getLastColumn() ); - records.remove( columnIdx ); - } - } + 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); - /** - * 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) - { + nci.setFirstColumn(targetColumnIx); + nci.setLastColumn(targetColumnIx); + setColumnInfoFields( nci, xfIndex, width, level, hidden, collapsed ); - // 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 ); - } + 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); - } - /** - * Finds the ColumnInfoRecord which contains the specified columnIndex - * @param columnIndex index of the column (not the index of the ColumnInfoRecord) - * @return null if no column info found for the specified column - */ + 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 ci 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 false 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 true the group will be indented by one + * level, if false 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 ColumnInfoRecord which contains the specified columnIndex + * @param columnIndex index of the column (not the index of the ColumnInfoRecord) + * @return null 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