diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml
index d9c063455..01d4a1f70 100644
--- a/src/documentation/content/xdocs/status.xml
+++ b/src/documentation/content/xdocs/status.xml
@@ -35,6 +35,7 @@
+ 47199 - Fixed PageSettingsBlock/Sheet to tolerate margin records after other non-PSB records
47069 - Fixed HSSFSheet#getFirstRowNum and HSSFSheet#getLastRowNum to return correct values after removal of all rows
47278 - Fixed XSSFCell to avoid generating xsi:nil entries in shared string table
47206 - Fixed XSSFCell to properly read inline strings
diff --git a/src/java/org/apache/poi/hssf/model/Sheet.java b/src/java/org/apache/poi/hssf/model/Sheet.java
index 0966d560e..34063aee1 100644
--- a/src/java/org/apache/poi/hssf/model/Sheet.java
+++ b/src/java/org/apache/poi/hssf/model/Sheet.java
@@ -57,7 +57,6 @@ import org.apache.poi.hssf.record.SaveRecalcRecord;
import org.apache.poi.hssf.record.ScenarioProtectRecord;
import org.apache.poi.hssf.record.SelectionRecord;
import org.apache.poi.hssf.record.UncalcedRecord;
-import org.apache.poi.hssf.record.UnknownRecord;
import org.apache.poi.hssf.record.WSBoolRecord;
import org.apache.poi.hssf.record.WindowTwoRecord;
import org.apache.poi.hssf.record.aggregates.ChartSubstreamRecordAggregate;
@@ -207,7 +206,7 @@ public final class Sheet implements Model {
records.add(rra); //only add the aggregate once
continue;
}
-
+
if (CustomViewSettingsRecordAggregate.isBeginRecord(recSid)) {
// This happens three times in test sample file "29982.xls"
// Also several times in bugzilla samples 46840-23373 and 46840-23374
@@ -217,28 +216,13 @@ public final class Sheet implements Model {
if (PageSettingsBlock.isComponentRecord(recSid)) {
if (_psBlock == null) {
- // typical case - just one PSB (so far)
+ // first PSB record encountered - read all of them:
_psBlock = new PageSettingsBlock(rs);
records.add(_psBlock);
- continue;
+ } else {
+ // one or more PSB records found after some intervening non-PSB records
+ _psBlock.addLateRecords(rs);
}
- if (recSid == UnknownRecord.HEADER_FOOTER_089C) {
- // test samples: SharedFormulaTest.xls, ex44921-21902.xls, ex42570-20305.xls
- _psBlock.addLateHeaderFooter(rs.getNext());
- continue;
- }
- // Some apps write PLS, WSBOOL, but PLS is part of
- // This happens in the test sample file "NoGutsRecords.xls" and "WORKBOOK_in_capitals.xls"
- // In this case the first PSB is two records back
- int prevPsbIx = records.size()-2;
- if (_psBlock != records.get(prevPsbIx) || !(records.get(prevPsbIx+1) instanceof WSBoolRecord)) {
- // not quite the expected situation
- throw new RuntimeException("two Page Settings Blocks found in the same sheet");
- }
- records.remove(prevPsbIx); // WSBOOL will drop down one position.
- PageSettingsBlock latePsb = new PageSettingsBlock(rs);
- _psBlock = mergePSBs(_psBlock, latePsb);
- records.add(_psBlock);
continue;
}
@@ -247,7 +231,7 @@ public final class Sheet implements Model {
_mergedCellsTable.read(rs);
continue;
}
-
+
if (recSid == BOFRecord.sid) {
ChartSubstreamRecordAggregate chartAgg = new ChartSubstreamRecordAggregate(rs);
if (false) {
@@ -372,34 +356,7 @@ public final class Sheet implements Model {
recs.add(r);
}});
}
- /**
- * Hack to recover from the situation where the page settings block has been split by
- * an intervening {@link WSBoolRecord}
- */
- private static PageSettingsBlock mergePSBs(PageSettingsBlock a, PageSettingsBlock b) {
- List temp = new ArrayList();
- RecordTransferrer rt = new RecordTransferrer(temp);
- a.visitContainedRecords(rt);
- b.visitContainedRecords(rt);
- RecordStream rs = new RecordStream(temp, 0);
- PageSettingsBlock result = new PageSettingsBlock(rs);
- if (rs.hasNext()) {
- throw new RuntimeException("PageSettingsBlocks did not merge properly");
- }
- return result;
- }
- private static final class RecordTransferrer implements RecordVisitor {
-
- private final List _destList;
-
- public RecordTransferrer(List destList) {
- _destList = destList;
- }
- public void visitRecord(Record r) {
- _destList.add(r);
- }
- }
private static final class RecordCloner implements RecordVisitor {
private final List _destList;
@@ -1099,7 +1056,7 @@ public final class Sheet implements Model {
*/
public void setColumnWidth(int column, int width) {
if(width > 255*256) throw new IllegalArgumentException("The maximum column width for an individual cell is 255 characters.");
-
+
setColumn(column, null, new Integer(width), null, null, null);
}
diff --git a/src/java/org/apache/poi/hssf/record/aggregates/PageSettingsBlock.java b/src/java/org/apache/poi/hssf/record/aggregates/PageSettingsBlock.java
index f906c7a99..98b589e67 100644
--- a/src/java/org/apache/poi/hssf/record/aggregates/PageSettingsBlock.java
+++ b/src/java/org/apache/poi/hssf/record/aggregates/PageSettingsBlock.java
@@ -125,36 +125,47 @@ public final class PageSettingsBlock extends RecordAggregate {
private boolean readARecord(RecordStream rs) {
switch (rs.peekNextSid()) {
case HorizontalPageBreakRecord.sid:
+ checkNotPresent(_rowBreaksRecord);
_rowBreaksRecord = (PageBreakRecord) rs.getNext();
break;
case VerticalPageBreakRecord.sid:
+ checkNotPresent(_columnBreaksRecord);
_columnBreaksRecord = (PageBreakRecord) rs.getNext();
break;
case HeaderRecord.sid:
+ checkNotPresent(_header);
_header = (HeaderRecord) rs.getNext();
break;
case FooterRecord.sid:
+ checkNotPresent(_footer);
_footer = (FooterRecord) rs.getNext();
break;
case HCenterRecord.sid:
+ checkNotPresent(_hCenter);
_hCenter = (HCenterRecord) rs.getNext();
break;
case VCenterRecord.sid:
+ checkNotPresent(_vCenter);
_vCenter = (VCenterRecord) rs.getNext();
break;
case LeftMarginRecord.sid:
+ checkNotPresent(_leftMargin);
_leftMargin = (LeftMarginRecord) rs.getNext();
break;
case RightMarginRecord.sid:
+ checkNotPresent(_rightMargin);
_rightMargin = (RightMarginRecord) rs.getNext();
break;
case TopMarginRecord.sid:
+ checkNotPresent(_topMargin);
_topMargin = (TopMarginRecord) rs.getNext();
break;
case BottomMarginRecord.sid:
+ checkNotPresent(_bottomMargin);
_bottomMargin = (BottomMarginRecord) rs.getNext();
break;
case UnknownRecord.PLS_004D:
+ checkNotPresent(_pls);
_pls = rs.getNext();
while (rs.peekNextSid()==ContinueRecord.sid) {
if (_plsContinues==null) {
@@ -164,15 +175,19 @@ public final class PageSettingsBlock extends RecordAggregate {
}
break;
case PrintSetupRecord.sid:
+ checkNotPresent(_printSetup);
_printSetup = (PrintSetupRecord)rs.getNext();
break;
case UnknownRecord.BITMAP_00E9:
+ checkNotPresent(_bitmap);
_bitmap = rs.getNext();
break;
case UnknownRecord.PRINTSIZE_0033:
+ checkNotPresent(_printSize);
_printSize = rs.getNext();
break;
case UnknownRecord.HEADER_FOOTER_089C:
+ checkNotPresent(_headerFooter);
_headerFooter = rs.getNext();
break;
default:
@@ -182,6 +197,13 @@ public final class PageSettingsBlock extends RecordAggregate {
return true;
}
+ private void checkNotPresent(Record rec) {
+ if (rec != null) {
+ throw new RecordFormatException("Duplicate PageSettingsBlock record (sid=0x"
+ + Integer.toHexString(rec.getSid()) + ")");
+ }
+ }
+
private PageBreakRecord getRowBreaksRecord() {
if (_rowBreaksRecord == null) {
_rowBreaksRecord = new HorizontalPageBreakRecord();
@@ -551,4 +573,40 @@ public final class PageSettingsBlock extends RecordAggregate {
}
_headerFooter = rec;
}
+
+ /**
+ * This method reads PageSettingsBlock records from the supplied RecordStream until the first
+ * non-PageSettingsBlock record is encountered. As each record is read, it is incorporated
+ * into this PageSettingsBlock.
+ *
+ * The latest Excel version seems to write the PageSettingsBlock uninterrupted. However there
+ * are several examples (that Excel reads OK) where these records are not written together:
+ *
+ * - HEADER_FOOTER(0x089C) after WINDOW2 - This record is new in 2007. Some apps
+ * seem to have scattered this record long after the PageSettingsBlock where it belongs
+ * test samples: SharedFormulaTest.xls, ex44921-21902.xls, ex42570-20305.xls
+ * - PLS, WSBOOL, PageSettingsBlock - WSBOOL is not a PSB record.
+ * This happens in the test sample file "NoGutsRecords.xls" and "WORKBOOK_in_capitals.xls"
+ * - Margins after DIMENSION - All of PSB should be before DIMENSION. (Bug-47199)
+ *
+ * These were probably written by other applications (or earlier versions of Excel). It was
+ * decided to not write specific code for detecting each of these cases. POI now tolerates
+ * PageSettingsBlock records scattered all over the sheet record stream, and in any order, but
+ * does not allow duplicates of any of those records.
+ *
+ *
+ * Note - when POI writes out this PageSettingsBlock, the records will always be written
+ * in one consolidated block (in the standard ordering) regardless of how scattered the records
+ * were when they were originally read.
+ *
+ * @throws RecordFormatException if any PSB record encountered has the same type (sid) as
+ * a record that is already part of this PageSettingsBlock
+ */
+ public void addLateRecords(RecordStream rs) {
+ while(true) {
+ if (!readARecord(rs)) {
+ break;
+ }
+ }
+ }
}
diff --git a/src/testcases/org/apache/poi/hssf/record/aggregates/TestPageSettingsBlock.java b/src/testcases/org/apache/poi/hssf/record/aggregates/TestPageSettingsBlock.java
index 076bbfeb3..9380b6d2f 100644
--- a/src/testcases/org/apache/poi/hssf/record/aggregates/TestPageSettingsBlock.java
+++ b/src/testcases/org/apache/poi/hssf/record/aggregates/TestPageSettingsBlock.java
@@ -26,6 +26,7 @@ import org.apache.poi.hssf.HSSFTestDataSamples;
import org.apache.poi.hssf.model.RecordStream;
import org.apache.poi.hssf.model.Sheet;
import org.apache.poi.hssf.record.BOFRecord;
+import org.apache.poi.hssf.record.BottomMarginRecord;
import org.apache.poi.hssf.record.DimensionsRecord;
import org.apache.poi.hssf.record.EOFRecord;
import org.apache.poi.hssf.record.FooterRecord;
@@ -33,6 +34,7 @@ import org.apache.poi.hssf.record.HeaderRecord;
import org.apache.poi.hssf.record.IndexRecord;
import org.apache.poi.hssf.record.NumberRecord;
import org.apache.poi.hssf.record.Record;
+import org.apache.poi.hssf.record.RecordFormatException;
import org.apache.poi.hssf.record.UnknownRecord;
import org.apache.poi.hssf.record.WindowTwoRecord;
import org.apache.poi.hssf.usermodel.HSSFPrintSetup;
@@ -135,7 +137,7 @@ public final class TestPageSettingsBlock extends TestCase {
Sheet sheet = Sheet.createSheet(rs);
RecordCollector rv = new RecordCollector();
- sheet.visitContainedRecords(rv, rowIx);
+ sheet.visitContainedRecords(rv, 0);
Record[] outRecs = rv.getRecords();
if (outRecs[4] == EOFRecord.instance) {
throw new AssertionFailedError("Identified bug 46953 - EOF incorrectly appended to PSB");
@@ -151,6 +153,85 @@ public final class TestPageSettingsBlock extends TestCase {
assertEquals(WindowTwoRecord.class, outRecs[6].getClass());
assertEquals(EOFRecord.instance, outRecs[7]);
}
+ /**
+ * Bug 47199 was due to the margin records being located well after the initial PSB records.
+ * The example file supplied (attachment 23710) had three non-PSB record types
+ * between the PRINTSETUP record and first MARGIN record:
+ *
+ * - PRINTSETUP(0x00A1)
+ * - DEFAULTCOLWIDTH(0x0055)
+ * - COLINFO(0x007D)
+ * - DIMENSIONS(0x0200)
+ * - BottomMargin(0x0029)
+ *
+ */
+ public void testLateMargins_bug47199() {
+
+ Record[] recs = {
+ BOFRecord.createSheetBOF(),
+ new HeaderRecord("&LSales Figures"),
+ new FooterRecord("&LJanuary"),
+ new DimensionsRecord(),
+ createBottomMargin(0.787F),
+ new WindowTwoRecord(),
+ EOFRecord.instance,
+ };
+ RecordStream rs = new RecordStream(Arrays.asList(recs), 0);
+
+ Sheet sheet;
+ try {
+ sheet = Sheet.createSheet(rs);
+ } catch (RuntimeException e) {
+ if (e.getMessage().equals("two Page Settings Blocks found in the same sheet")) {
+ throw new AssertionFailedError("Identified bug 47199a - failed to process late margings records");
+ }
+ throw e;
+ }
+
+ RecordCollector rv = new RecordCollector();
+ sheet.visitContainedRecords(rv, 0);
+ Record[] outRecs = rv.getRecords();
+ assertEquals(recs.length+1, outRecs.length); // +1 for index record
+
+ assertEquals(BOFRecord.class, outRecs[0].getClass());
+ assertEquals(IndexRecord.class, outRecs[1].getClass());
+ assertEquals(HeaderRecord.class, outRecs[2].getClass());
+ assertEquals(FooterRecord.class, outRecs[3].getClass());
+ assertEquals(DimensionsRecord.class, outRecs[5].getClass());
+ assertEquals(WindowTwoRecord.class, outRecs[6].getClass());
+ assertEquals(EOFRecord.instance, outRecs[7]);
+ }
+
+ private Record createBottomMargin(float value) {
+ BottomMarginRecord result = new BottomMarginRecord();
+ result.setMargin(value);
+ return result;
+ }
+
+ /**
+ * The PageSettingsBlock should not allow multiple copies of the same record. This extra assertion
+ * was added while fixing bug 47199. All existing POI test samples comply with this requirement.
+ */
+ public void testDuplicatePSBRecord_bug47199() {
+
+ // Hypothetical setup of PSB records which should cause POI to crash
+ Record[] recs = {
+ new HeaderRecord("&LSales Figures"),
+ new HeaderRecord("&LInventory"),
+ };
+ RecordStream rs = new RecordStream(Arrays.asList(recs), 0);
+
+ try {
+ new PageSettingsBlock(rs);
+ throw new AssertionFailedError("Identified bug 47199b - duplicate PSB records should not be allowed");
+ } catch (RecordFormatException e) {
+ if (e.getMessage().equals("Duplicate PageSettingsBlock record (sid=0x14)")) {
+ // expected during successful test
+ } else {
+ throw new AssertionFailedError("Expected RecordFormatException due to duplicate PSB record");
+ }
+ }
+ }
private static UnknownRecord ur(int sid, String hexData) {
return new UnknownRecord(sid, HexRead.readFromString(hexData));