From 13e963bc8c8397d5b71d202e869d076262e125fe Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Sat, 19 Dec 2009 11:37:45 +0000 Subject: [PATCH] fixed PageSettingsBlock to allow multiple HeaderFooterRecord records, see Bugzilla 48026 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@892467 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/status.xml | 3 + src/java/org/apache/poi/hssf/model/Sheet.java | 3 + .../poi/hssf/record/HeaderFooterRecord.java | 98 ++++++++++++++++ .../apache/poi/hssf/record/RecordFactory.java | 3 + .../apache/poi/hssf/record/UnknownRecord.java | 4 - .../poi/hssf/record/UserSViewBegin.java | 87 +++++++++++++++ .../apache/poi/hssf/record/UserSViewEnd.java | 75 +++++++++++++ .../ChartSubstreamRecordAggregate.java | 10 +- .../CustomViewSettingsRecordAggregate.java | 16 +-- .../record/aggregates/PageSettingsBlock.java | 85 +++++++++----- .../aggregates/TestPageSettingsBlock.java | 105 ++++++++++++++---- 11 files changed, 423 insertions(+), 66 deletions(-) create mode 100755 src/java/org/apache/poi/hssf/record/HeaderFooterRecord.java create mode 100644 src/java/org/apache/poi/hssf/record/UserSViewBegin.java create mode 100644 src/java/org/apache/poi/hssf/record/UserSViewEnd.java diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index acfb9b51a..2a1ce7d65 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,9 @@ + added Ant target to install artifacts in local repository + 48026 - fixed PageSettingsBlock to allow multiple HeaderFooterRecord records + 48202 - fixed CellRangeUtil.mergeCellRanges to work for adjacent cell regions 48339 - fixed ExternalNameRecord to properly distinguish DDE data from OLE data items 47920 - allow editing workbooks embedded into PowerPoint files 48343 - added implementation of SUBTOTAL function diff --git a/src/java/org/apache/poi/hssf/model/Sheet.java b/src/java/org/apache/poi/hssf/model/Sheet.java index df8d616d8..cc19ae1cd 100644 --- a/src/java/org/apache/poi/hssf/model/Sheet.java +++ b/src/java/org/apache/poi/hssf/model/Sheet.java @@ -221,6 +221,9 @@ public final class Sheet implements Model { // one or more PSB records found after some intervening non-PSB records _psBlock.addLateRecords(rs); } + // YK: in some cases records can be moved to the preceding + // CustomViewSettingsRecordAggregate blocks + _psBlock.positionRecords(records); continue; } diff --git a/src/java/org/apache/poi/hssf/record/HeaderFooterRecord.java b/src/java/org/apache/poi/hssf/record/HeaderFooterRecord.java new file mode 100755 index 000000000..23d266fa6 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/HeaderFooterRecord.java @@ -0,0 +1,98 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import org.apache.poi.hssf.record.aggregates.PageSettingsBlock; +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndianOutput; + +import java.util.Arrays; + +/** + * The HEADERFOOTER record stores information added in Office Excel 2007 for headers/footers. + * + * @author Yegor Kozlov + */ +public final class HeaderFooterRecord extends StandardRecord { + + private static final byte[] BLANK_GUID = new byte[16]; + + public final static short sid = 0x089C; + private byte[] _rawData; + + public HeaderFooterRecord(byte[] data) { + _rawData = data; + } + + /** + * construct a HeaderFooterRecord record. No fields are interpreted and the record will + * be serialized in its original form more or less + * @param in the RecordInputstream to read the record from + */ + public HeaderFooterRecord(RecordInputStream in) { + _rawData = in.readRemainder(); + } + + /** + * spit the record out AS IS. no interpretation or identification + */ + public void serialize(LittleEndianOutput out) { + out.write(_rawData); + } + + protected int getDataSize() { + return _rawData.length; + } + + public short getSid() + { + return sid; + } + + /** + * If this header belongs to a specific sheet view , the sheet view?s GUID will be saved here. + *

+ * If it is zero, it means the current sheet. Otherwise, this field MUST match the guid field + * of the preceding {@link UserSViewBegin} record. + * + * @return the sheet view?s GUID + */ + public byte[] getGuid(){ + byte[] guid = new byte[16]; + System.arraycopy(_rawData, 12, guid, 0, guid.length); + return guid; + } + + /** + * @return whether this record belongs to the current sheet + */ + public boolean isCurrentSheet(){ + return Arrays.equals(getGuid(), BLANK_GUID); + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + + sb.append("[").append("HEADERFOOTER").append("] (0x"); + sb.append(Integer.toHexString(sid).toUpperCase() + ")\n"); + sb.append(" rawData=").append(HexDump.toHex(_rawData)).append("\n"); + sb.append("[/").append("HEADERFOOTER").append("]\n"); + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/record/RecordFactory.java b/src/java/org/apache/poi/hssf/record/RecordFactory.java index 85f3a082e..86f947b92 100644 --- a/src/java/org/apache/poi/hssf/record/RecordFactory.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactory.java @@ -126,6 +126,7 @@ public final class RecordFactory { GutsRecord.class, HCenterRecord.class, HeaderRecord.class, + HeaderFooterRecord.class, HideObjRecord.class, HorizontalPageBreakRecord.class, HyperlinkRecord.class, @@ -179,6 +180,8 @@ public final class RecordFactory { TopMarginRecord.class, UncalcedRecord.class, UseSelFSRecord.class, + UserSViewBegin.class, + UserSViewEnd.class, VCenterRecord.class, VerticalPageBreakRecord.class, WindowOneRecord.class, diff --git a/src/java/org/apache/poi/hssf/record/UnknownRecord.java b/src/java/org/apache/poi/hssf/record/UnknownRecord.java index 750cc5dab..1351b7be5 100644 --- a/src/java/org/apache/poi/hssf/record/UnknownRecord.java +++ b/src/java/org/apache/poi/hssf/record/UnknownRecord.java @@ -53,8 +53,6 @@ public final class UnknownRecord extends StandardRecord { public static final int BITMAP_00E9 = 0x00E9; public static final int PHONETICPR_00EF = 0x00EF; public static final int LABELRANGES_015F = 0x015F; - public static final int USERSVIEWBEGIN_01AA = 0x01AA; - public static final int USERSVIEWEND_01AB = 0x01AB; public static final int QUICKTIP_0800 = 0x0800; public static final int SHEETEXT_0862 = 0x0862; // OOO calls this SHEETLAYOUT public static final int SHEETPROTECTION_0867 = 0x0867; @@ -160,8 +158,6 @@ public final class UnknownRecord extends StandardRecord { case LABELRANGES_015F: return "LABELRANGES"; case 0x01BA: return "CODENAME"; case 0x01A9: return "USERBVIEW"; - case USERSVIEWBEGIN_01AA: return "USERSVIEWBEGIN"; - case USERSVIEWEND_01AB: return "USERSVIEWEND"; case 0x01AD: return "QSI"; case 0x01C0: return "EXCEL9FILE"; diff --git a/src/java/org/apache/poi/hssf/record/UserSViewBegin.java b/src/java/org/apache/poi/hssf/record/UserSViewBegin.java new file mode 100644 index 000000000..ad59a5868 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/UserSViewBegin.java @@ -0,0 +1,87 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import org.apache.poi.hssf.record.aggregates.PageSettingsBlock; +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndianOutput; + +import java.util.Arrays; + +/** + * The UserSViewBegin record specifies settings for a custom view associated with the sheet. + * This record also marks the start of custom view records, which save custom view settings. + * Records between {@link UserSViewBegin} and {@link UserSViewEnd} contain settings for the custom view, + * not settings for the sheet itself. + * + * @author Yegor Kozlov + */ +public final class UserSViewBegin extends StandardRecord { + + public final static short sid = 0x01AA; + private byte[] _rawData; + + public UserSViewBegin(byte[] data) { + _rawData = data; + } + + /** + * construct an UserSViewBegin record. No fields are interpreted and the record will + * be serialized in its original form more or less + * @param in the RecordInputstream to read the record from + */ + public UserSViewBegin(RecordInputStream in) { + _rawData = in.readRemainder(); + } + + /** + * spit the record out AS IS. no interpretation or identification + */ + public void serialize(LittleEndianOutput out) { + out.write(_rawData); + } + + protected int getDataSize() { + return _rawData.length; + } + + public short getSid() + { + return sid; + } + + /** + * @return Globally unique identifier for the custom view + */ + public byte[] getGuid(){ + byte[] guid = new byte[16]; + System.arraycopy(_rawData, 0, guid, 0, guid.length); + return guid; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + + sb.append("[").append("USERSVIEWBEGIN").append("] (0x"); + sb.append(Integer.toHexString(sid).toUpperCase() + ")\n"); + sb.append(" rawData=").append(HexDump.toHex(_rawData)).append("\n"); + sb.append("[/").append("USERSVIEWBEGIN").append("]\n"); + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/record/UserSViewEnd.java b/src/java/org/apache/poi/hssf/record/UserSViewEnd.java new file mode 100644 index 000000000..271dd7758 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/UserSViewEnd.java @@ -0,0 +1,75 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.record; + +import org.apache.poi.hssf.record.aggregates.PageSettingsBlock; +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndianOutput; + +import java.util.Arrays; + +/** + * The UserSViewEnd record marks the end of the settings for a custom view associated with the sheet + * + * @author Yegor Kozlov + */ +public final class UserSViewEnd extends StandardRecord { + + public final static short sid = 0x01AB; + private byte[] _rawData; + + public UserSViewEnd(byte[] data) { + _rawData = data; + } + + /** + * construct an UserSViewEnd record. No fields are interpreted and the record will + * be serialized in its original form more or less + * @param in the RecordInputstream to read the record from + */ + public UserSViewEnd(RecordInputStream in) { + _rawData = in.readRemainder(); + } + + /** + * spit the record out AS IS. no interpretation or identification + */ + public void serialize(LittleEndianOutput out) { + out.write(_rawData); + } + + protected int getDataSize() { + return _rawData.length; + } + + public short getSid() + { + return sid; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + + sb.append("[").append("USERSVIEWEND").append("] (0x"); + sb.append(Integer.toHexString(sid).toUpperCase() + ")\n"); + sb.append(" rawData=").append(HexDump.toHex(_rawData)).append("\n"); + sb.append("[/").append("USERSVIEWEND").append("]\n"); + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/record/aggregates/ChartSubstreamRecordAggregate.java b/src/java/org/apache/poi/hssf/record/aggregates/ChartSubstreamRecordAggregate.java index a891488b9..dc7320993 100644 --- a/src/java/org/apache/poi/hssf/record/aggregates/ChartSubstreamRecordAggregate.java +++ b/src/java/org/apache/poi/hssf/record/aggregates/ChartSubstreamRecordAggregate.java @@ -21,11 +21,7 @@ import java.util.ArrayList; import java.util.List; import org.apache.poi.hssf.model.RecordStream; -import org.apache.poi.hssf.record.BOFRecord; -import org.apache.poi.hssf.record.EOFRecord; -import org.apache.poi.hssf.record.Record; -import org.apache.poi.hssf.record.RecordBase; -import org.apache.poi.hssf.record.UnknownRecord; +import org.apache.poi.hssf.record.*; /** * Manages the all the records associated with a chart sub-stream.
@@ -48,9 +44,9 @@ public final class ChartSubstreamRecordAggregate extends RecordAggregate { while (rs.peekNextClass() != EOFRecord.class) { if (PageSettingsBlock.isComponentRecord(rs.peekNextSid())) { if (_psBlock != null) { - if (rs.peekNextSid() == UnknownRecord.HEADER_FOOTER_089C) { + if (rs.peekNextSid() == HeaderFooterRecord.sid) { // test samples: 45538_classic_Footer.xls, 45538_classic_Header.xls - _psBlock.addLateHeaderFooter(rs.getNext()); + _psBlock.addLateHeaderFooter((HeaderFooterRecord)rs.getNext()); continue; } throw new IllegalStateException( diff --git a/src/java/org/apache/poi/hssf/record/aggregates/CustomViewSettingsRecordAggregate.java b/src/java/org/apache/poi/hssf/record/aggregates/CustomViewSettingsRecordAggregate.java index de1a2fe2a..bc6a737e6 100644 --- a/src/java/org/apache/poi/hssf/record/aggregates/CustomViewSettingsRecordAggregate.java +++ b/src/java/org/apache/poi/hssf/record/aggregates/CustomViewSettingsRecordAggregate.java @@ -21,9 +21,7 @@ import java.util.ArrayList; import java.util.List; import org.apache.poi.hssf.model.RecordStream; -import org.apache.poi.hssf.record.Record; -import org.apache.poi.hssf.record.RecordBase; -import org.apache.poi.hssf.record.UnknownRecord; +import org.apache.poi.hssf.record.*; /** * Manages the all the records associated with a 'Custom View Settings' sub-stream.
@@ -43,11 +41,11 @@ public final class CustomViewSettingsRecordAggregate extends RecordAggregate { public CustomViewSettingsRecordAggregate(RecordStream rs) { _begin = rs.getNext(); - if (_begin.getSid() != UnknownRecord.USERSVIEWBEGIN_01AA) { + if (_begin.getSid() != UserSViewBegin.sid) { throw new IllegalStateException("Bad begin record"); } List temp = new ArrayList(); - while (rs.peekNextSid() != UnknownRecord.USERSVIEWEND_01AB) { + while (rs.peekNextSid() != UserSViewEnd.sid) { if (PageSettingsBlock.isComponentRecord(rs.peekNextSid())) { if (_psBlock != null) { throw new IllegalStateException( @@ -61,7 +59,7 @@ public final class CustomViewSettingsRecordAggregate extends RecordAggregate { } _recs = temp; _end = rs.getNext(); // no need to save EOF in field - if (_end.getSid() != UnknownRecord.USERSVIEWEND_01AB) { + if (_end.getSid() != UserSViewEnd.sid) { throw new IllegalStateException("Bad custom view settings end record"); } } @@ -83,6 +81,10 @@ public final class CustomViewSettingsRecordAggregate extends RecordAggregate { } public static boolean isBeginRecord(int sid) { - return sid == UnknownRecord.USERSVIEWBEGIN_01AA; + return sid == UserSViewBegin.sid; } + + public void append(RecordBase r){ + _recs.add(r); + } } 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 097eff112..79ed333ce 100644 --- a/src/java/org/apache/poi/hssf/record/aggregates/PageSettingsBlock.java +++ b/src/java/org/apache/poi/hssf/record/aggregates/PageSettingsBlock.java @@ -20,26 +20,11 @@ package org.apache.poi.hssf.record.aggregates; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Arrays; import org.apache.poi.hssf.model.RecordStream; import org.apache.poi.hssf.model.Sheet; -import org.apache.poi.hssf.record.BottomMarginRecord; -import org.apache.poi.hssf.record.ContinueRecord; -import org.apache.poi.hssf.record.FooterRecord; -import org.apache.poi.hssf.record.HCenterRecord; -import org.apache.poi.hssf.record.HeaderRecord; -import org.apache.poi.hssf.record.HorizontalPageBreakRecord; -import org.apache.poi.hssf.record.LeftMarginRecord; -import org.apache.poi.hssf.record.Margin; -import org.apache.poi.hssf.record.PageBreakRecord; -import org.apache.poi.hssf.record.PrintSetupRecord; -import org.apache.poi.hssf.record.Record; -import org.apache.poi.hssf.record.RecordFormatException; -import org.apache.poi.hssf.record.RightMarginRecord; -import org.apache.poi.hssf.record.TopMarginRecord; -import org.apache.poi.hssf.record.UnknownRecord; -import org.apache.poi.hssf.record.VCenterRecord; -import org.apache.poi.hssf.record.VerticalPageBreakRecord; +import org.apache.poi.hssf.record.*; /** * Groups the page settings records for a worksheet.

@@ -102,8 +87,14 @@ public final class PageSettingsBlock extends RecordAggregate { private final List _plsRecords; private PrintSetupRecord _printSetup; private Record _bitmap; - private Record _headerFooter; - private Record _printSize; + private HeaderFooterRecord _headerFooter; + /** + * HeaderFooterRecord records belonging to preceding CustomViewSettingsRecordAggregates. + * The indicator of such records is a non-zero GUID, + * see {@link org.apache.poi.hssf.record.HeaderFooterRecord#getGuid()} + */ + private List _sviewHeaderFooters = new ArrayList(); + private Record _printSize; public PageSettingsBlock(RecordStream rs) { _plsRecords = new ArrayList(); @@ -126,7 +117,7 @@ public final class PageSettingsBlock extends RecordAggregate { _hCenter = createHCenter(); _vCenter = createVCenter(); _printSetup = createPrintSetup(); - } + } /** * @return true if the specified Record sid is one belonging to the @@ -148,7 +139,7 @@ public final class PageSettingsBlock extends RecordAggregate { case PrintSetupRecord.sid: case UnknownRecord.BITMAP_00E9: case UnknownRecord.PRINTSIZE_0033: - case UnknownRecord.HEADER_FOOTER_089C: // extra header/footer settings supported by Excel 2007 + case HeaderFooterRecord.sid: // extra header/footer settings supported by Excel 2007 return true; } return false; @@ -211,10 +202,14 @@ public final class PageSettingsBlock extends RecordAggregate { checkNotPresent(_printSize); _printSize = rs.getNext(); break; - case UnknownRecord.HEADER_FOOTER_089C: - checkNotPresent(_headerFooter); - _headerFooter = rs.getNext(); - break; + case HeaderFooterRecord.sid: + //there can be multiple HeaderFooterRecord records belonging to different sheet views + HeaderFooterRecord hf = (HeaderFooterRecord)rs.getNext(); + if(hf.isCurrentSheet()) _headerFooter = hf; + else { + _sviewHeaderFooters.add(hf); + } + break; default: // all other record types are not part of the PageSettingsBlock return false; @@ -596,11 +591,11 @@ public final class PageSettingsBlock extends RecordAggregate { * HEADERFOOTER is new in 2007. Some apps seem to have scattered this record long after * the {@link PageSettingsBlock} where it belongs. */ - public void addLateHeaderFooter(Record rec) { + public void addLateHeaderFooter(HeaderFooterRecord rec) { if (_headerFooter != null) { throw new IllegalStateException("This page settings block already has a header/footer record"); } - if (rec.getSid() != UnknownRecord.HEADER_FOOTER_089C) { + if (rec.getSid() != HeaderFooterRecord.sid) { throw new RecordFormatException("Unexpected header-footer record sid: 0x" + Integer.toHexString(rec.getSid())); } _headerFooter = rec; @@ -641,4 +636,40 @@ public final class PageSettingsBlock extends RecordAggregate { } } } + + /** + * Some apps can define multiple HeaderFooterRecord records for a sheet. + * When saving such a file Excel 2007 re-positiones them according to the followig rules: + * - take a HeaderFooterRecord and read 16-byte GUID at offset 12. If it is zero, + * it means the current sheet and the given HeaderFooterRecord belongs to this PageSettingsBlock + * - If GUID is not zero then search in preceding CustomViewSettingsRecordAggregates. + * Compare first 16 bytes of UserSViewBegin with the HeaderFooterRecord's GUID. If match, + * then append the HeaderFooterRecord to this CustomViewSettingsRecordAggregates + * + * @param sheetRecords the list of sheet records read so far + */ + public void positionRecords(List sheetRecords) { + // loop through HeaderFooterRecord records having not-empty GUID and match them with + // CustomViewSettingsRecordAggregate blocks having UserSViewBegin with the same GUID + for (final Iterator it = _sviewHeaderFooters.iterator(); it.hasNext(); ) { + final HeaderFooterRecord hf = it.next(); + for (RecordBase rb : sheetRecords) { + if (rb instanceof CustomViewSettingsRecordAggregate) { + final CustomViewSettingsRecordAggregate cv = (CustomViewSettingsRecordAggregate) rb; + cv.visitContainedRecords(new RecordVisitor() { + public void visitRecord(Record r) { + if (r.getSid() == UserSViewBegin.sid) { + byte[] guid1 = ((UserSViewBegin) r).getGuid(); + byte[] guid2 = hf.getGuid(); + if (Arrays.equals(guid1, guid2)) { + cv.append(hf); + it.remove(); + } + } + } + }); + } + } + } + } } 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 e3a01b476..94448af7a 100644 --- a/src/testcases/org/apache/poi/hssf/record/aggregates/TestPageSettingsBlock.java +++ b/src/testcases/org/apache/poi/hssf/record/aggregates/TestPageSettingsBlock.java @@ -25,21 +25,7 @@ import junit.framework.TestCase; 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.ContinueRecord; -import org.apache.poi.hssf.record.DimensionsRecord; -import org.apache.poi.hssf.record.EOFRecord; -import org.apache.poi.hssf.record.FooterRecord; -import org.apache.poi.hssf.record.HCenterRecord; -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.VCenterRecord; -import org.apache.poi.hssf.record.WindowTwoRecord; +import org.apache.poi.hssf.record.*; import org.apache.poi.hssf.usermodel.HSSFPrintSetup; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; @@ -86,14 +72,14 @@ public final class TestPageSettingsBlock extends TestCase { BOFRecord.createSheetBOF(), new HeaderRecord("&LSales Figures"), new FooterRecord("&LJanuary"), - ur(UnknownRecord.HEADER_FOOTER_089C, "9C 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C4 60 00 00 00 00 00 00 00 00"), + new HeaderFooterRecord(HexRead.readFromString("9C 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C4 60 00 00 00 00 00 00 00 00")), new DimensionsRecord(), new WindowTwoRecord(), - ur(UnknownRecord.USERSVIEWBEGIN_01AA, "ED 77 3B 86 BC 3F 37 4C A9 58 60 23 43 68 54 4B 01 00 00 00 64 00 00 00 40 00 00 00 02 00 00 00 3D 80 04 00 00 00 00 00 00 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F0 3F FF FF 01 00"), + new UserSViewBegin(HexRead.readFromString("ED 77 3B 86 BC 3F 37 4C A9 58 60 23 43 68 54 4B 01 00 00 00 64 00 00 00 40 00 00 00 02 00 00 00 3D 80 04 00 00 00 00 00 00 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F0 3F FF FF 01 00")), new HeaderRecord("&LSales Figures"), new FooterRecord("&LJanuary"), - ur(UnknownRecord.HEADER_FOOTER_089C, "9C 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C4 60 00 00 00 00 00 00 00 00"), - ur(UnknownRecord.USERSVIEWEND_01AB, "01, 00"), + new HeaderFooterRecord(HexRead.readFromString("9C 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C4 60 00 00 00 00 00 00 00 00")), + new UserSViewEnd(HexRead.readFromString("01, 00")), EOFRecord.instance, }; @@ -132,7 +118,7 @@ public final class TestPageSettingsBlock extends TestCase { new FooterRecord("&LJanuary"), new DimensionsRecord(), new WindowTwoRecord(), - ur(UnknownRecord.HEADER_FOOTER_089C, "9C 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C4 60 00 00 00 00 00 00 00 00"), + new HeaderFooterRecord(HexRead.readFromString("9C 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C4 60 00 00 00 00 00 00 00 00")), EOFRecord.instance, }; RecordStream rs = new RecordStream(Arrays.asList(recs), 0); @@ -150,7 +136,7 @@ public final class TestPageSettingsBlock extends TestCase { assertEquals(IndexRecord.class, outRecs[1].getClass()); assertEquals(HeaderRecord.class, outRecs[2].getClass()); assertEquals(FooterRecord.class, outRecs[3].getClass()); - assertEquals(UnknownRecord.HEADER_FOOTER_089C, outRecs[4].getSid()); + assertEquals(HeaderFooterRecord.class, outRecs[4].getClass()); assertEquals(DimensionsRecord.class, outRecs[5].getClass()); assertEquals(WindowTwoRecord.class, outRecs[6].getClass()); assertEquals(EOFRecord.instance, outRecs[7]); @@ -315,4 +301,81 @@ public final class TestPageSettingsBlock extends TestCase { // records were assembled in standard order, so this simple check is OK assertTrue(Arrays.equals(recs, outRecs)); } + + public void testDuplicateHeaderFooter_bug48026() { + + Record[] recs = { + BOFRecord.createSheetBOF(), + new IndexRecord(), + + //PageSettingsBlock + new HeaderRecord("&LDecember"), + new FooterRecord("&LJanuary"), + new DimensionsRecord(), + + new WindowTwoRecord(), + + //CustomViewSettingsRecordAggregate + new UserSViewBegin(HexRead.readFromString("53 CE BD CC DE 38 44 45 97 C1 5C 89 F9 37 32 1B 01 00 00 00 64 00 00 00 40 00 00 00 03 00 00 00 7D 00 00 20 00 00 34 00 00 00 18 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF FF FF")), + new SelectionRecord(0, 0), + new UserSViewEnd(HexRead.readFromString("01 00")), + + // two HeaderFooterRecord records, the first one has zero GUID (16 bytes at offset 12) and belongs to the PSB, + // the other is matched with a CustomViewSettingsRecordAggregate having UserSViewBegin with the same GUID + new HeaderFooterRecord(HexRead.readFromString("9C 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 34 33 00 00 00 00 00 00 00 00")), + new HeaderFooterRecord(HexRead.readFromString("9C 08 00 00 00 00 00 00 00 00 00 00 53 CE BD CC DE 38 44 45 97 C1 5C 89 F9 37 32 1B 34 33 00 00 00 00 00 00 00 00")), + + EOFRecord.instance, + }; + RecordStream rs = new RecordStream(Arrays.asList(recs), 0); + Sheet sheet; + try { + sheet = Sheet.createSheet(rs); + } catch (RuntimeException e) { + if (e.getMessage().equals("Duplicate PageSettingsBlock record (sid=0x89c)")) { + throw new AssertionFailedError("Identified bug 48026"); + } + throw e; + } + + RecordCollector rv = new RecordCollector(); + sheet.visitContainedRecords(rv, 0); + Record[] outRecs = rv.getRecords(); + + assertEquals(recs.length, outRecs.length); + //expected order of records: + Record[] expectedRecs = { + recs[0], //BOFRecord + recs[1], //IndexRecord + + //PageSettingsBlock + recs[2], //HeaderRecord + recs[3], //FooterRecord + recs[9], //HeaderFooterRecord + recs[4], // DimensionsRecord + recs[5], // WindowTwoRecord + + //CustomViewSettingsRecordAggregate + recs[6], // UserSViewBegin + recs[7], // SelectionRecord + recs[10], // HeaderFooterRecord + recs[8], // UserSViewEnd + + recs[11], //EOFRecord + }; + for(int i=0; i < expectedRecs.length; i++){ + assertEquals("Record mismatch at index " + i, expectedRecs[i].getClass(), outRecs[i].getClass()); + } + HeaderFooterRecord hd1 = (HeaderFooterRecord)expectedRecs[4]; + //GUID is zero + assertTrue(Arrays.equals(new byte[16], hd1.getGuid())); + assertTrue(hd1.isCurrentSheet()); + + UserSViewBegin svb = (UserSViewBegin)expectedRecs[7]; + HeaderFooterRecord hd2 = (HeaderFooterRecord)expectedRecs[9]; + assertFalse(hd2.isCurrentSheet()); + //GUIDs of HeaderFooterRecord and UserSViewBegin must be the same + assertTrue(Arrays.equals(svb.getGuid(), hd2.getGuid())); + } + }