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;
*/
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
}
* @param fc - the first column index (0-based)
*/
- public void setFirstColumn(short fc)
+ public void setFirstColumn(int fc)
{
field_1_first_col = fc;
}
* @param lc - the last column index (0-based)
*/
- public void setLastColumn(short lc)
+ public void setLastColumn(int lc)
{
field_2_last_col = lc;
}
* @param cw - column width
*/
- public void setColumnWidth(short cw)
+ public void setColumnWidth(int cw)
{
field_3_col_width = cw;
}
* @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
public void setHidden(boolean ishidden)
{
- field_5_options = hidden.setShortBoolean(field_5_options, ishidden);
+ field_5_options = hidden.setBoolean(field_5_options, ishidden);
}
/**
* @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);
}
/**
public void setCollapsed(boolean iscollapsed)
{
- field_5_options = collapsed.setShortBoolean(field_5_options,
+ field_5_options = collapsed.setBoolean(field_5_options,
iscollapsed);
}
* @return the first column index (0-based)
*/
- public short getFirstColumn()
+ public int getFirstColumn()
{
return field_1_first_col;
}
* @return the last column index (0-based)
*/
- public short getLastColumn()
+ public int getLastColumn()
{
return field_2_last_col;
}
* @return column width
*/
- public short getColumnWidth()
+ public int getColumnWidth()
{
return field_3_col_width;
}
* @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
/**
* @return outline level for the cells
*/
- public short getOutlineLevel()
+ public int getOutlineLevel()
{
- return outlevel.getShortValue(field_5_options);
+ return outlevel.getValue(field_5_options);
}
/**
}
// 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;
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();
}
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() {
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
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;
}
-
-
}
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
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);
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());
+ }
+}