From: Josh Micich Date: Thu, 4 Jun 2009 03:38:52 +0000 (+0000) Subject: Bugzilla 47244 - Fixed HSSFSheet to handle missing header / footer records X-Git-Tag: REL_3_5-FINAL~122 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=40e6ccd12e3f9972a90e8e38735b21c70749a396;p=poi.git Bugzilla 47244 - Fixed HSSFSheet to handle missing header / footer records git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@781645 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index f84c086a8a..99942aaaeb 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -35,6 +35,7 @@ + 47244 - Fixed HSSFSheet to handle missing header / footer records 47312 - Fixed formula parser to properly reject cell references with a '0' row component 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 diff --git a/src/java/org/apache/poi/hssf/extractor/ExcelExtractor.java b/src/java/org/apache/poi/hssf/extractor/ExcelExtractor.java index 262f10a936..dc70fef2fb 100644 --- a/src/java/org/apache/poi/hssf/extractor/ExcelExtractor.java +++ b/src/java/org/apache/poi/hssf/extractor/ExcelExtractor.java @@ -293,10 +293,8 @@ public class ExcelExtractor extends POIOLE2TextExtractor implements org.apache.p } // Header text, if there is any - if(_includeHeadersFooters && sheet.getHeader() != null) { - text.append( - _extractHeaderFooter(sheet.getHeader()) - ); + if(_includeHeadersFooters) { + text.append(_extractHeaderFooter(sheet.getHeader())); } int firstRow = sheet.getFirstRowNum(); @@ -382,11 +380,9 @@ public class ExcelExtractor extends POIOLE2TextExtractor implements org.apache.p text.append("\n"); } - // Finally Feader text, if there is any - if(_includeHeadersFooters && sheet.getFooter() != null) { - text.append( - _extractHeaderFooter(sheet.getFooter()) - ); + // Finally Footer text, if there is any + if(_includeHeadersFooters) { + text.append(_extractHeaderFooter(sheet.getFooter())); } } diff --git a/src/java/org/apache/poi/hssf/record/FooterRecord.java b/src/java/org/apache/poi/hssf/record/FooterRecord.java index b08d3dca11..823372fc68 100644 --- a/src/java/org/apache/poi/hssf/record/FooterRecord.java +++ b/src/java/org/apache/poi/hssf/record/FooterRecord.java @@ -27,48 +27,30 @@ package org.apache.poi.hssf.record; * */ public final class FooterRecord extends HeaderFooterBase { - public final static short sid = 0x0015; + public final static short sid = 0x0015; - public FooterRecord(String text) { - super(text); - } + public FooterRecord(String text) { + super(text); + } - public FooterRecord(RecordInputStream in) { + public FooterRecord(RecordInputStream in) { super(in); } - /** - * set the footer string - * - * @param footer string to display - */ - public void setFooter(String footer) { - setText(footer); - } - - /** - * get the footer string - * - * @return footer string to display - */ - public String getFooter() { - return getText(); - } - - public String toString() { - StringBuffer buffer = new StringBuffer(); + public String toString() { + StringBuffer buffer = new StringBuffer(); - buffer.append("[FOOTER]\n"); - buffer.append(" .footer = ").append(getText()).append("\n"); - buffer.append("[/FOOTER]\n"); - return buffer.toString(); - } + buffer.append("[FOOTER]\n"); + buffer.append(" .footer = ").append(getText()).append("\n"); + buffer.append("[/FOOTER]\n"); + return buffer.toString(); + } - public short getSid() { - return sid; - } + public short getSid() { + return sid; + } - public Object clone() { - return new FooterRecord(getText()); - } + public Object clone() { + return new FooterRecord(getText()); + } } diff --git a/src/java/org/apache/poi/hssf/record/HeaderFooterBase.java b/src/java/org/apache/poi/hssf/record/HeaderFooterBase.java index 84678b7b65..53d5f46926 100644 --- a/src/java/org/apache/poi/hssf/record/HeaderFooterBase.java +++ b/src/java/org/apache/poi/hssf/record/HeaderFooterBase.java @@ -25,7 +25,7 @@ import org.apache.poi.util.StringUtil; * * @author Josh Micich */ -abstract class HeaderFooterBase extends StandardRecord { +public abstract class HeaderFooterBase extends StandardRecord { private boolean field_2_hasMultibyte; private String field_3_text; @@ -44,7 +44,8 @@ abstract class HeaderFooterBase extends StandardRecord { field_3_text = in.readCompressedUnicode(field_1_footer_len); } } else { - // Note - this is unusual: when the text is empty string, the whole record is empty (just the 4 byte BIFF header) + // Note - this is unusual for BIFF records in general, but normal for header / footer records: + // when the text is empty string, the whole record is empty (just the 4 byte BIFF header) field_3_text = ""; } } @@ -62,16 +63,9 @@ abstract class HeaderFooterBase extends StandardRecord { field_3_text = text; // Check it'll fit into the space in the record - if (field_2_hasMultibyte) { - if (field_3_text.length() > 127) { - throw new IllegalArgumentException( - "Footer string too long (limit is 127 for unicode strings)"); - } - } else { - if (field_3_text.length() > 255) { - throw new IllegalArgumentException( - "Footer string too long (limit is 255 for non-unicode strings)"); - } + if (getDataSize() > RecordInputStream.MAX_RECORD_DATA_SIZE) { + throw new IllegalArgumentException("Header/Footer string too long (limit is " + + RecordInputStream.MAX_RECORD_DATA_SIZE + " bytes)"); } } diff --git a/src/java/org/apache/poi/hssf/record/HeaderRecord.java b/src/java/org/apache/poi/hssf/record/HeaderRecord.java index 188149d7df..23adedc5a8 100644 --- a/src/java/org/apache/poi/hssf/record/HeaderRecord.java +++ b/src/java/org/apache/poi/hssf/record/HeaderRecord.java @@ -17,7 +17,6 @@ package org.apache.poi.hssf.record; - /** * Title: Header Record

* Description: Specifies a header for a sheet

@@ -27,49 +26,30 @@ package org.apache.poi.hssf.record; * @author Jason Height (jheight at chariot dot net dot au) */ public final class HeaderRecord extends HeaderFooterBase { - public final static short sid = 0x0014; - - public HeaderRecord(String text) { - super(text); - } - - public HeaderRecord(RecordInputStream in) { - super(in); - } - - /** - * set the header string - * - * @param header string to display - */ - public void setHeader(String header) { - setText(header); - } + public final static short sid = 0x0014; - /** - * get the header string - * - * @return header string to display - */ - public String getHeader() { - return getText(); - } + public HeaderRecord(String text) { + super(text); + } - public String toString() { - StringBuffer buffer = new StringBuffer(); + public HeaderRecord(RecordInputStream in) { + super(in); + } - buffer.append("[HEADER]\n"); - buffer.append(" .header = ").append(getText()).append("\n"); - buffer.append("[/HEADER]\n"); - return buffer.toString(); - } + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("[HEADER]\n"); + buffer.append(" .header = ").append(getText()).append("\n"); + buffer.append("[/HEADER]\n"); + return buffer.toString(); + } - public short getSid() { - return sid; - } + public short getSid() { + return sid; + } - public Object clone() { - return new HeaderRecord(getText()); - } + public Object clone() { + return new HeaderRecord(getText()); + } } 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 98b589e67e..1b9228bec8 100644 --- a/src/java/org/apache/poi/hssf/record/aggregates/PageSettingsBlock.java +++ b/src/java/org/apache/poi/hssf/record/aggregates/PageSettingsBlock.java @@ -239,10 +239,21 @@ public final class PageSettingsBlock extends RecordAggregate { public void visitContainedRecords(RecordVisitor rv) { + // Replicates record order from Excel 2007, though this is not critical + visitIfPresent(_rowBreaksRecord, rv); visitIfPresent(_columnBreaksRecord, rv); - visitIfPresent(_header, rv); - visitIfPresent(_footer, rv); + // Write out empty header / footer records if these are missing + if (_header == null) { + rv.visitRecord(new HeaderRecord("")); + } else { + rv.visitRecord(_header); + } + if (_footer == null) { + rv.visitRecord(new FooterRecord("")); + } else { + rv.visitRecord(_footer); + } visitIfPresent(_hCenter, rv); visitIfPresent(_vCenter, rv); visitIfPresent(_leftMargin, rv); diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFFooter.java b/src/java/org/apache/poi/hssf/usermodel/HSSFFooter.java index 4f10ba8c9f..da7d7403d3 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFFooter.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFFooter.java @@ -15,10 +15,10 @@ limitations under the License. ==================================================================== */ - package org.apache.poi.hssf.usermodel; import org.apache.poi.hssf.record.FooterRecord; +import org.apache.poi.hssf.record.aggregates.PageSettingsBlock; import org.apache.poi.ss.usermodel.Footer; /** @@ -33,59 +33,29 @@ import org.apache.poi.ss.usermodel.Footer; *

* @author Shawn Laubach (slaubach at apache dot org) */ -public class HSSFFooter extends HeaderFooter implements Footer { - private FooterRecord footerRecord; - - /** - * Constructor. Creates a new footer interface from a footer record - * @param footerRecord Footer record to create the footer with - */ - protected HSSFFooter(FooterRecord footerRecord) { - super(footerRecord.getFooter()); - this.footerRecord = footerRecord; - } - - /** - * Sets the left string. - * @param newLeft The string to set as the left side. - */ - public void setLeft(String newLeft) { - left = newLeft; - createFooterString(); - } - - /** - * Sets the center string. - * @param newCenter The string to set as the center. - */ - public void setCenter(String newCenter) { - center = newCenter; - createFooterString(); - } - - /** - * Sets the right string. - * @param newRight The string to set as the right side. - */ - public void setRight(String newRight) { - right = newRight; - createFooterString(); - } - - protected String getRawFooter() { - return footerRecord.getFooter(); - } - - - /** - * Creates the complete footer string based on the left, center, and middle - * strings. - */ - private void createFooterString() { - footerRecord.setFooter( - "&C" + (center == null ? "" : center) + - "&L" + (left == null ? "" : left) + - "&R" + (right == null ? "" : right)); - } +public final class HSSFFooter extends HeaderFooter implements Footer { + private final PageSettingsBlock _psb; + + protected HSSFFooter(PageSettingsBlock psb) { + _psb = psb; + } + + protected String getRawText() { + FooterRecord hf = _psb.getFooter(); + if (hf == null) { + return ""; + } + return hf.getText(); + } + + @Override + protected void setHeaderFooterText(String text) { + FooterRecord hfr = _psb.getFooter(); + if (hfr == null) { + hfr = new FooterRecord(text); + _psb.setFooter(hfr); + } else { + hfr.setText(text); + } + } } - diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFHeader.java b/src/java/org/apache/poi/hssf/usermodel/HSSFHeader.java index 36dfc969b0..4d706159ee 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFHeader.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFHeader.java @@ -18,6 +18,7 @@ package org.apache.poi.hssf.usermodel; import org.apache.poi.hssf.record.HeaderRecord; +import org.apache.poi.hssf.record.aggregates.PageSettingsBlock; import org.apache.poi.ss.usermodel.Header; /** @@ -33,66 +34,30 @@ import org.apache.poi.ss.usermodel.Header; * * @author Shawn Laubach (slaubach at apache dot org) */ -public class HSSFHeader extends HeaderFooter implements Header { - private HeaderRecord headerRecord; - - /** - * Constructor. Creates a new header interface from a header record - * - * @param headerRecord Header record to create the header with - */ - protected HSSFHeader( HeaderRecord headerRecord ) { - super(headerRecord.getHeader()); - this.headerRecord = headerRecord; - } - - /** - * Sets the left string. - * - * @param newLeft The string to set as the left side. - */ - public void setLeft( String newLeft ) - { - left = newLeft; - createHeaderString(); - } - - /** - * Sets the center string. - * - * @param newCenter The string to set as the center. - */ - public void setCenter( String newCenter ) - { - center = newCenter; - createHeaderString(); - } - - /** - * Sets the right string. - * - * @param newRight The string to set as the right side. - */ - public void setRight( String newRight ) - { - right = newRight; - createHeaderString(); - } - - protected String getRawHeader() { - return headerRecord.getHeader(); - } - - /** - * Creates the complete header string based on the left, center, and middle - * strings. - */ - private void createHeaderString() - { - headerRecord.setHeader( "&C" + ( center == null ? "" : center ) + - "&L" + ( left == null ? "" : left ) + - "&R" + ( right == null ? "" : right ) ); - } - +public final class HSSFHeader extends HeaderFooter implements Header { + + private final PageSettingsBlock _psb; + + protected HSSFHeader(PageSettingsBlock psb) { + _psb = psb; + } + + protected String getRawText() { + HeaderRecord hf = _psb.getHeader(); + if (hf == null) { + return ""; + } + return hf.getText(); + } + + @Override + protected void setHeaderFooterText(String text) { + HeaderRecord hfr = _psb.getHeader(); + if (hfr == null) { + hfr = new HeaderRecord(text); + _psb.setHeader(hfr); + } else { + hfr.setText(text); + } + } } - diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java b/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java index 0067a6bb8c..83474957e6 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java @@ -913,20 +913,12 @@ public final class HSSFSheet implements org.apache.poi.ss.usermodel.Sheet { return new HSSFPrintSetup(_sheet.getPageSettings().getPrintSetup()); } - /** - * Gets the user model for the document header. - * @return The Document header. - */ public HSSFHeader getHeader() { - return new HSSFHeader(_sheet.getPageSettings().getHeader()); + return new HSSFHeader(_sheet.getPageSettings()); } - /** - * Gets the user model for the document footer. - * @return The Document footer. - */ public HSSFFooter getFooter() { - return new HSSFFooter(_sheet.getPageSettings().getFooter()); + return new HSSFFooter(_sheet.getPageSettings()); } /** diff --git a/src/java/org/apache/poi/hssf/usermodel/HeaderFooter.java b/src/java/org/apache/poi/hssf/usermodel/HeaderFooter.java index 8e091ec003..4fd7c89cc0 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HeaderFooter.java +++ b/src/java/org/apache/poi/hssf/usermodel/HeaderFooter.java @@ -14,318 +14,325 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ + package org.apache.poi.hssf.usermodel; -import java.util.ArrayList; /** - * Common class for {@link HSSFHeader} and - * {@link HSSFFooter}. + * Common class for {@link HSSFHeader} and {@link HSSFFooter}. */ public abstract class HeaderFooter implements org.apache.poi.ss.usermodel.HeaderFooter { - protected String left; - protected String center; - protected String right; + + protected HeaderFooter() { + // + } - private boolean stripFields = false; + /** + * @return the internal text representation (combining center, left and right parts). + * Possibly empty string if no header or footer is set. Never null. + */ + protected abstract String getRawText(); - protected HeaderFooter(String text) { - while (text != null && text.length() > 1) { - int pos = text.length(); - switch (text.substring(1, 2).charAt(0)) { - case 'L' : + private String[] splitParts() { + String text = getRawText(); + // default values + String _left = ""; + String _center = ""; + String _right = ""; + + while (text.length() > 1) { + int pos = text.length(); + switch (text.charAt(1)) { + case 'L': if (text.indexOf("&C") >= 0) { - pos = Math.min(pos, text.indexOf("&C")); - } + pos = Math.min(pos, text.indexOf("&C")); + } if (text.indexOf("&R") >= 0) { - pos = Math.min(pos, text.indexOf("&R")); - } - left = text.substring(2, pos); + pos = Math.min(pos, text.indexOf("&R")); + } + _left = text.substring(2, pos); text = text.substring(pos); break; - case 'C' : + case 'C': if (text.indexOf("&L") >= 0) { - pos = Math.min(pos, text.indexOf("&L")); - } + pos = Math.min(pos, text.indexOf("&L")); + } if (text.indexOf("&R") >= 0) { - pos = Math.min(pos, text.indexOf("&R")); - } - center = text.substring(2, pos); + pos = Math.min(pos, text.indexOf("&R")); + } + _center = text.substring(2, pos); text = text.substring(pos); break; - case 'R' : + case 'R': if (text.indexOf("&C") >= 0) { - pos = Math.min(pos, text.indexOf("&C")); - } + pos = Math.min(pos, text.indexOf("&C")); + } if (text.indexOf("&L") >= 0) { - pos = Math.min(pos, text.indexOf("&L")); - } - right = text.substring(2, pos); + pos = Math.min(pos, text.indexOf("&L")); + } + _right = text.substring(2, pos); text = text.substring(pos); break; - default: - text = null; - } + default: + throw new IllegalStateException("bad text '" + getRawText() + "'."); + } } + return new String[] { _left, _center, _right, }; } - - /** - * Get the left side of the header or footer. - * @return The string representing the left side. - */ - public String getLeft() { - if(stripFields) - return stripFields(left); - return left; + + /** + * @return the left side of the header or footer. + */ + public final String getLeft() { + return splitParts()[0]; + } + + /** + * @param newLeft The string to set as the left side. + */ + public final void setLeft(String newLeft) { + updatePart(0, newLeft); + } + + /** + * @return the center of the header or footer. + */ + public final String getCenter() { + return splitParts()[1]; + } + + /** + * @param newCenter The string to set as the center. + */ + public final void setCenter(String newCenter) { + updatePart(1, newCenter); + } + + /** + * @return The right side of the header or footer. + */ + public final String getRight() { + return splitParts()[2]; + } + + /** + * @param newRight The string to set as the right side. + */ + public final void setRight(String newRight) { + updatePart(2, newRight); } - public abstract void setLeft( String newLeft ); - - /** - * Get the center of the header or footer. - * @return The string representing the center. - */ - public String getCenter() { - if(stripFields) - return stripFields(center); - return center; - } - public abstract void setCenter( String newCenter ); - - /** - * Get the right side of the header or footer. - * @return The string representing the right side. - */ - public String getRight() { - if(stripFields) - return stripFields(right); - return right; - } - public abstract void setRight( String newRight ); - - - /** - * Returns the string that represents the change in font size. - * - * @param size the new font size - * @return The special string to represent a new font size - */ - public static String fontSize( short size ) - { - return "&" + size; - } - - /** - * Returns the string that represents the change in font. - * - * @param font the new font - * @param style the fonts style, one of regular, italic, bold, italic bold or bold italic - * @return The special string to represent a new font size - */ - public static String font( String font, String style ) - { - return "&\"" + font + "," + style + "\""; - } - - /** - * Returns the string representing the current page number - * - * @return The special string for page number - */ - public static String page() { - return PAGE_FIELD.sequence; - } - - /** - * Returns the string representing the number of pages. - * - * @return The special string for the number of pages - */ - public static String numPages() { - return NUM_PAGES_FIELD.sequence; - } - - /** - * Returns the string representing the current date - * - * @return The special string for the date - */ - public static String date() { - return DATE_FIELD.sequence; - } - - /** - * Returns the string representing the current time - * - * @return The special string for the time - */ - public static String time() { - return TIME_FIELD.sequence; - } - - /** - * Returns the string representing the current file name - * - * @return The special string for the file name - */ - public static String file() { - return FILE_FIELD.sequence; - } - - /** - * Returns the string representing the current tab (sheet) name - * - * @return The special string for tab name - */ - public static String tab() { - return SHEET_NAME_FIELD.sequence; - } - - /** - * Returns the string representing the start bold - * - * @return The special string for start bold - */ - public static String startBold() { - return BOLD_FIELD.sequence; - } - - /** - * Returns the string representing the end bold - * - * @return The special string for end bold - */ - public static String endBold() { - return BOLD_FIELD.sequence; - } - - /** - * Returns the string representing the start underline - * - * @return The special string for start underline - */ - public static String startUnderline() { - return UNDERLINE_FIELD.sequence; - } - - /** - * Returns the string representing the end underline - * - * @return The special string for end underline - */ - public static String endUnderline() { - return UNDERLINE_FIELD.sequence; - } - - /** - * Returns the string representing the start double underline - * - * @return The special string for start double underline - */ - public static String startDoubleUnderline() { - return DOUBLE_UNDERLINE_FIELD.sequence; - } - - /** - * Returns the string representing the end double underline - * - * @return The special string for end double underline - */ - public static String endDoubleUnderline() { - return DOUBLE_UNDERLINE_FIELD.sequence; - } - - - /** - * Removes any fields (eg macros, page markers etc) - * from the string. - * Normally used to make some text suitable for showing - * to humans, and the resultant text should not normally - * be saved back into the document! - */ - public static String stripFields(String text) { - int pos; - - // Check we really got something to work on - if(text == null || text.length() == 0) { - return text; - } - - // Firstly, do the easy ones which are static - for(int i=0; i -1) { - text = text.substring(0, pos) + - text.substring(pos+seq.length()); - } - } - - // Now do the tricky, dynamic ones - // These are things like font sizes and font names - text = text.replaceAll("\\&\\d+", ""); - text = text.replaceAll("\\&\".*?,.*?\"", ""); - - // All done - return text; - } - + private void updatePart(int partIndex, String newValue) { + String[] parts = splitParts(); + parts[partIndex] = newValue == null ? "" : newValue; + updateHeaderFooterText(parts); + } + /** + * Creates the complete footer string based on the left, center, and middle + * strings. + */ + private void updateHeaderFooterText(String[] parts) { + String _left = parts[0]; + String _center = parts[1]; + String _right = parts[2]; + + if (_center.length() < 1 && _left.length() < 1 && _right.length() < 1) { + setHeaderFooterText(""); + return; + } + StringBuilder sb = new StringBuilder(64); + sb.append("&C"); + sb.append(_center); + sb.append("&L"); + sb.append(_left); + sb.append("&R"); + sb.append(_right); + String text = sb.toString(); + setHeaderFooterText(text); + } + + /** + * @param text the new header footer text (contains mark-up tags). Possibly + * empty string never null + */ + protected abstract void setHeaderFooterText(String text); + + /** + * @param size + * the new font size + * @return The mark-up tag representing a new font size + */ + public static String fontSize(short size) { + return "&" + size; + } + + /** + * @param font + * the new font + * @param style + * the fonts style, one of regular, italic, bold, italic bold or + * bold italic + * @return The mark-up tag representing a new font size + */ + public static String font(String font, String style) { + return "&\"" + font + "," + style + "\""; + } + + /** + * @return The mark-up tag representing the current page number + */ + public static String page() { + return MarkupTag.PAGE_FIELD.getRepresentation(); + } + + /** + * @return The mark-up tag representing the number of pages + */ + public static String numPages() { + return MarkupTag.NUM_PAGES_FIELD.getRepresentation(); + } + + /** + * @return The mark-up tag representing the current date date + */ + public static String date() { + return MarkupTag.DATE_FIELD.getRepresentation(); + } + + /** + * @return The mark-up tag representing current time + */ + public static String time() { + return MarkupTag.TIME_FIELD.getRepresentation(); + } + + /** + * @return The mark-up tag representing the current file name + */ + public static String file() { + return MarkupTag.FILE_FIELD.getRepresentation(); + } + /** - * Are fields currently being stripped from - * the text that this {@link HeaderFooter} returns? - * Default is false, but can be changed + * @return The mark-up tag representing the current tab (sheet) name */ - public boolean areFieldsStripped() { - return stripFields; + public static String tab() { + return MarkupTag.SHEET_NAME_FIELD.getRepresentation(); } + + /** + * @return The mark-up tag for start bold + */ + public static String startBold() { + return MarkupTag.BOLD_FIELD.getRepresentation(); + } + + /** + * @return The mark-up tag for end bold + */ + public static String endBold() { + return MarkupTag.BOLD_FIELD.getRepresentation(); + } + + /** + * @return The mark-up tag for start underline + */ + public static String startUnderline() { + return MarkupTag.UNDERLINE_FIELD.getRepresentation(); + } + /** - * Should fields (eg macros) be stripped from - * the text that this class returns? - * Default is not to strip. - * @param stripFields + * @return The mark-up tag for end underline */ - public void setAreFieldsStripped(boolean stripFields) { - this.stripFields = stripFields; + public static String endUnderline() { + return MarkupTag.UNDERLINE_FIELD.getRepresentation(); } - - public static final Field SHEET_NAME_FIELD = new Field("&A"); - public static final Field DATE_FIELD = new Field("&D"); - public static final Field FILE_FIELD = new Field("&F"); - public static final Field FULL_FILE_FIELD = new Field("&Z"); - public static final Field PAGE_FIELD = new Field("&P"); - public static final Field TIME_FIELD = new Field("&T"); - public static final Field NUM_PAGES_FIELD = new Field("&N"); - - public static final Field PICTURE_FIELD = new Field("&G"); - - public static final PairField BOLD_FIELD = new PairField("&B"); - public static final PairField ITALIC_FIELD = new PairField("&I"); - public static final PairField STRIKETHROUGH_FIELD = new PairField("&S"); - public static final PairField SUBSCRIPT_FIELD = new PairField("&Y"); - public static final PairField SUPERSCRIPT_FIELD = new PairField("&X"); - public static final PairField UNDERLINE_FIELD = new PairField("&U"); - public static final PairField DOUBLE_UNDERLINE_FIELD = new PairField("&E"); - - /** - * Represents a special field in a header or footer, - * eg the page number - */ - public static class Field { - private static ArrayList ALL_FIELDS = new ArrayList(); - /** The character sequence that marks this field */ - public final String sequence; - private Field(String sequence) { - this.sequence = sequence; - ALL_FIELDS.add(this); - } - } - /** - * A special field that normally comes in a pair, eg - * turn on underline / turn off underline - */ - public static class PairField extends Field { - private PairField(String sequence) { - super(sequence); - } - } + /** + * @return The mark-up tag for start double underline + */ + public static String startDoubleUnderline() { + return MarkupTag.DOUBLE_UNDERLINE_FIELD.getRepresentation(); + } + + /** + * @return The mark-up tag for end double underline + */ + public static String endDoubleUnderline() { + return MarkupTag.DOUBLE_UNDERLINE_FIELD.getRepresentation(); + } + + /** + * Removes any fields (eg macros, page markers etc) from the string. + * Normally used to make some text suitable for showing to humans, and the + * resultant text should not normally be saved back into the document! + */ + public static String stripFields(String pText) { + int pos; + + // Check we really got something to work on + if (pText == null || pText.length() == 0) { + return pText; + } + + String text = pText; + + // Firstly, do the easy ones which are static + for (MarkupTag mt : MarkupTag.values()) { + String seq = mt.getRepresentation(); + while ((pos = text.indexOf(seq)) > -1) { + text = text.substring(0, pos) + text.substring(pos + seq.length()); + } + } + + // Now do the tricky, dynamic ones + // These are things like font sizes and font names + text = text.replaceAll("\\&\\d+", ""); + text = text.replaceAll("\\&\".*?,.*?\"", ""); + + // All done + return text; + } + + private enum MarkupTag { + SHEET_NAME_FIELD ("&A", false), + DATE_FIELD ("&D", false), + FILE_FIELD ("&F", false), + FULL_FILE_FIELD ("&Z", false), + PAGE_FIELD ("&P", false), + TIME_FIELD ("&T", false), + NUM_PAGES_FIELD ("&N", false), + + PICTURE_FIELD ("&G", false), + + BOLD_FIELD ("&B", true), + ITALIC_FIELD ("&I", true), + STRIKETHROUGH_FIELD ("&S", true), + SUBSCRIPT_FIELD ("&Y", true), + SUPERSCRIPT_FIELD ("&X", true), + UNDERLINE_FIELD ("&U", true), + DOUBLE_UNDERLINE_FIELD ("&E", true), + ; + + private final String _representation; + private final boolean _occursInPairs; + private MarkupTag(String sequence, boolean occursInPairs) { + _representation = sequence; + _occursInPairs = occursInPairs; + } + /** + * @return The character sequence that marks this field + */ + public String getRepresentation() { + return _representation; + } + + /** + * @return true if this markup tag normally comes in a pair, eg turn on + * underline / turn off underline + */ + public boolean occursPairs() { + return _occursInPairs; + } + } } diff --git a/src/java/org/apache/poi/ss/usermodel/Sheet.java b/src/java/org/apache/poi/ss/usermodel/Sheet.java index ffeb96abda..1ec70bd3d7 100644 --- a/src/java/org/apache/poi/ss/usermodel/Sheet.java +++ b/src/java/org/apache/poi/ss/usermodel/Sheet.java @@ -182,7 +182,7 @@ public interface Sheet extends Iterable { * @param height default row height */ void setDefaultRowHeightInPoints(float height); - + /** * Returns the CellStyle that applies to the given * (0 based) column, or null if no style has been @@ -275,7 +275,7 @@ public interface Sheet extends Iterable { /** * Set whether the window should show 0 (zero) in cells containing zero value. * When false, cells with zero value appear blank instead of showing the number zero. - * + * * @param value whether to display or hide all zero values on the worksheet */ void setDisplayZeros(boolean value); @@ -407,18 +407,19 @@ public interface Sheet extends Iterable { /** * Gets the user model for the default document header. - *

+ *

* Note that XSSF offers more kinds of document headers than HSSF does *

- * @return the document header. + * @return the document header. Never null */ Header getHeader(); /** * Gets the user model for the default document footer. + *

* Note that XSSF offers more kinds of document footers than HSSF does. * - * @return the document footer. + * @return the document footer. Never null */ Footer getFooter(); @@ -473,7 +474,7 @@ public interface Sheet extends Iterable { void setZoom(int numerator, int denominator); /** - * The top row in the visible view when the sheet is + * The top row in the visible view when the sheet is * first viewed after opening it in a viewer * * @return short indicating the rownum (0 based) of the top row @@ -481,7 +482,7 @@ public interface Sheet extends Iterable { short getTopRow(); /** - * The left col in the visible view when the sheet is + * The left col in the visible view when the sheet is * first viewed after opening it in a viewer * * @return short indicating the rownum (0 based) of the top row @@ -489,7 +490,7 @@ public interface Sheet extends Iterable { short getLeftCol(); /** - * Sets desktop window pane display area, when the + * Sets desktop window pane display area, when the * file is first opened in a viewer. * * @param toprow the top row to show in desktop window pane diff --git a/src/testcases/org/apache/poi/hssf/data/noHeaderFooter47244.xls b/src/testcases/org/apache/poi/hssf/data/noHeaderFooter47244.xls new file mode 100644 index 0000000000..1323ab3c53 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/noHeaderFooter47244.xls differ 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 9380b6d2f8..f3730efd7f 100644 --- a/src/testcases/org/apache/poi/hssf/record/aggregates/TestPageSettingsBlock.java +++ b/src/testcases/org/apache/poi/hssf/record/aggregates/TestPageSettingsBlock.java @@ -30,12 +30,14 @@ 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; +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.usermodel.HSSFPrintSetup; import org.apache.poi.hssf.usermodel.HSSFSheet; @@ -236,4 +238,39 @@ public final class TestPageSettingsBlock extends TestCase { private static UnknownRecord ur(int sid, String hexData) { return new UnknownRecord(sid, HexRead.readFromString(hexData)); } + + /** + * Excel tolerates missing header / footer records, but adds them (empty) in when re-saving. + * This is not critical functionality but it has been decided to keep POI consistent with + * Excel in this regard. + */ + public void testMissingHeaderFooter() { + // initialise PSB with some records, but not the header / footer + Record[] recs = { + new HCenterRecord(), + new VCenterRecord(), + }; + RecordStream rs = new RecordStream(Arrays.asList(recs), 0); + PageSettingsBlock psb = new PageSettingsBlock(rs); + + // serialize the PSB to see what records come out + RecordCollector rc = new RecordCollector(); + psb.visitContainedRecords(rc); + Record[] outRecs = rc.getRecords(); + + if (outRecs.length == 2) { + throw new AssertionFailedError("PageSettingsBlock didn't add missing header/footer records"); + } + assertEquals(4, outRecs.length); + assertEquals(HeaderRecord.class, outRecs[0].getClass()); + assertEquals(FooterRecord.class, outRecs[1].getClass()); + assertEquals(HCenterRecord.class, outRecs[2].getClass()); + assertEquals(VCenterRecord.class, outRecs[3].getClass()); + + // make sure the added header / footer records are empty + HeaderRecord hr = (HeaderRecord) outRecs[0]; + assertEquals("", hr.getText()); + FooterRecord fr = (FooterRecord) outRecs[1]; + assertEquals("", fr.getText()); + } } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java index ae65043271..771dd838b7 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java @@ -17,16 +17,20 @@ package org.apache.poi.hssf.usermodel; -import java.io.*; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import junit.framework.AssertionFailedError; -import junit.framework.TestCase; +import org.apache.poi.hssf.HSSFITestDataProvider; import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.hssf.OldExcelFormatException; -import org.apache.poi.hssf.HSSFITestDataProvider; import org.apache.poi.hssf.model.Workbook; import org.apache.poi.hssf.record.CellValueRecordInterface; import org.apache.poi.hssf.record.EmbeddedObjectRefSubRecord; @@ -34,11 +38,7 @@ import org.apache.poi.hssf.record.NameRecord; import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate; import org.apache.poi.hssf.record.formula.DeletedArea3DPtg; import org.apache.poi.hssf.record.formula.Ptg; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.RichTextString; -import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.BaseTestBugzillaIssues; -import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.util.TempFile; /** @@ -1247,16 +1247,18 @@ public final class TestBugs extends BaseTestBugzillaIssues { } /** - * header / footer text too long + * The resolution for bug 45777 assumed that the maximum text length in a header / footer + * record was 256 bytes. This assumption appears to be wrong. Since the fix for bug 47244, + * POI now supports header / footer text lengths beyond 256 bytes. */ public void test45777() { HSSFWorkbook wb = new HSSFWorkbook(); HSSFSheet s = wb.createSheet(); - String s248 = ""; - for(int i=0; i<248; i++) { - s248 += "x"; - } + char[] cc248 = new char[248]; + Arrays.fill(cc248, 'x'); + String s248 = new String(cc248); + String s249 = s248 + "1"; String s250 = s248 + "12"; String s251 = s248 + "123"; @@ -1268,42 +1270,45 @@ public final class TestBugs extends BaseTestBugzillaIssues { // Try on headers s.getHeader().setCenter(s248); - assertEquals(254, s.getHeader().getRawHeader().length()); + assertEquals(254, s.getHeader().getRawText().length()); writeOutAndReadBack(wb); - s.getHeader().setCenter(s249); - assertEquals(255, s.getHeader().getRawHeader().length()); + s.getHeader().setCenter(s251); + assertEquals(257, s.getHeader().getRawText().length()); writeOutAndReadBack(wb); try { - s.getHeader().setCenter(s250); // 256 - fail(); - } catch(IllegalArgumentException e) {} + s.getHeader().setCenter(s250); // 256 bytes required + } catch(IllegalArgumentException e) { + throw new AssertionFailedError("Identified bug 47244b - header can be more than 256 bytes"); + } try { - s.getHeader().setCenter(s251); // 257 - fail(); - } catch(IllegalArgumentException e) {} - + s.getHeader().setCenter(s251); // 257 bytes required + } catch(IllegalArgumentException e) { + throw new AssertionFailedError("Identified bug 47244b - header can be more than 256 bytes"); + } // Now try on footers s.getFooter().setCenter(s248); - assertEquals(254, s.getFooter().getRawFooter().length()); + assertEquals(254, s.getFooter().getRawText().length()); writeOutAndReadBack(wb); - s.getFooter().setCenter(s249); - assertEquals(255, s.getFooter().getRawFooter().length()); + s.getFooter().setCenter(s251); + assertEquals(257, s.getFooter().getRawText().length()); writeOutAndReadBack(wb); try { - s.getFooter().setCenter(s250); // 256 - fail(); - } catch(IllegalArgumentException e) {} + s.getFooter().setCenter(s250); // 256 bytes required + } catch(IllegalArgumentException e) { + throw new AssertionFailedError("Identified bug 47244b - footer can be more than 256 bytes"); + } try { - s.getFooter().setCenter(s251); // 257 - fail(); - } catch(IllegalArgumentException e) {} + s.getFooter().setCenter(s251); // 257 bytes required + } catch(IllegalArgumentException e) { + throw new AssertionFailedError("Identified bug 47244b - footer can be more than 256 bytes"); + } } /** @@ -1469,24 +1474,24 @@ public final class TestBugs extends BaseTestBugzillaIssues { * java.io.IOException: block[ 0 ] already removed * (is an excel 95 file though) */ - public void test46904() throws IOException { - try { - HSSFWorkbook wb = openSample("46904.xls"); - fail(); - } catch(OldExcelFormatException e) { - assertTrue(e.getMessage().startsWith( - "The supplied spreadsheet seems to be Excel" - )); - } + public void test46904() { + try { + openSample("46904.xls"); + fail(); + } catch(OldExcelFormatException e) { + assertTrue(e.getMessage().startsWith( + "The supplied spreadsheet seems to be Excel" + )); + } } /** * java.lang.NegativeArraySizeException reading long * non-unicode data for a name record */ - public void test47034() throws IOException { - HSSFWorkbook wb = openSample("47034.xls"); - assertEquals(893, wb.getNumberOfNames()); - assertEquals("Matthew\\Matthew11_1\\Matthew2331_1\\Matthew2351_1\\Matthew2361_1___lab", wb.getNameName(300)); + public void test47034() { + HSSFWorkbook wb = openSample("47034.xls"); + assertEquals(893, wb.getNumberOfNames()); + assertEquals("Matthew\\Matthew11_1\\Matthew2331_1\\Matthew2351_1\\Matthew2361_1___lab", wb.getNameName(300)); } } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFHeaderFooter.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFHeaderFooter.java index 3ae1efcaab..d2e8200132 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFHeaderFooter.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFHeaderFooter.java @@ -17,43 +17,43 @@ package org.apache.poi.hssf.usermodel; +import junit.framework.AssertionFailedError; import junit.framework.TestCase; import org.apache.poi.hssf.HSSFTestDataSamples; /** - * Tests row shifting capabilities. - * + * Tests for {@link HSSFHeader} / {@link HSSFFooter} * * @author Shawn Laubach (slaubach at apache dot com) */ public final class TestHSSFHeaderFooter extends TestCase { /** - * Tests that get header retreives the proper values. + * Tests that get header retrieves the proper values. * * @author Shawn Laubach (slaubach at apache dot org) */ public void testRetrieveCorrectHeader() { - // Read initial file in + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("EmbeddedChartHeaderTest.xls"); HSSFSheet s = wb.getSheetAt( 0 ); - HSSFHeader head = s.getHeader(); - - assertEquals("Top Left", head.getLeft()); - assertEquals("Top Center", head.getCenter()); - assertEquals("Top Right", head.getRight()); + HSSFHeader head = s.getHeader(); + + assertEquals("Top Left", head.getLeft()); + assertEquals("Top Center", head.getCenter()); + assertEquals("Top Right", head.getRight()); } - + public void testSpecialChars() { assertEquals("&U", HSSFHeader.startUnderline()); assertEquals("&U", HSSFHeader.endUnderline()); assertEquals("&P", HSSFHeader.page()); - + assertEquals("&22", HSSFFooter.fontSize((short)22)); assertEquals("&\"Arial,bold\"", HSSFFooter.font("Arial", "bold")); } - + public void testStripFields() { String simple = "I am a test header"; String withPage = "I am a&P test header"; @@ -61,54 +61,51 @@ public final class TestHSSFHeaderFooter extends TestCase { String withFont = "I&22 am a&\"Arial,bold\" test header"; String withOtherAnds = "I am a&P test header&&"; String withOtherAnds2 = "I am a&P test header&a&b"; - + assertEquals(simple, HSSFHeader.stripFields(simple)); assertEquals(simple, HSSFHeader.stripFields(withPage)); assertEquals(simple, HSSFHeader.stripFields(withLots)); assertEquals(simple, HSSFHeader.stripFields(withFont)); assertEquals(simple + "&&", HSSFHeader.stripFields(withOtherAnds)); assertEquals(simple + "&a&b", HSSFHeader.stripFields(withOtherAnds2)); - + // Now test the default strip flag HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("EmbeddedChartHeaderTest.xls"); HSSFSheet s = wb.getSheetAt( 0 ); - HSSFHeader head = s.getHeader(); - - assertEquals("Top Left", head.getLeft()); - assertEquals("Top Center", head.getCenter()); - assertEquals("Top Right", head.getRight()); - - head.setLeft("Top &P&F&D Left"); - assertEquals("Top &P&F&D Left", head.getLeft()); - assertFalse(head.areFieldsStripped()); - - head.setAreFieldsStripped(true); - assertEquals("Top Left", head.getLeft()); - assertTrue(head.areFieldsStripped()); - - // Now even more complex - head.setCenter("HEADER TEXT &P&N&D&T&Z&F&F&A&G&X END"); - assertEquals("HEADER TEXT END", head.getCenter()); + HSSFHeader head = s.getHeader(); + + assertEquals("Top Left", head.getLeft()); + assertEquals("Top Center", head.getCenter()); + assertEquals("Top Right", head.getRight()); + + head.setLeft("Top &P&F&D Left"); + assertEquals("Top &P&F&D Left", head.getLeft()); + + assertEquals("Top Left", HeaderFooter.stripFields(head.getLeft())); + + // Now even more complex + head.setCenter("HEADER TEXT &P&N&D&T&Z&F&F&A&G&X END"); + assertEquals("HEADER TEXT END", HeaderFooter.stripFields(head.getCenter())); } /** - * Tests that get header retreives the proper values. + * Tests that get header retrieves the proper values. * * @author Shawn Laubach (slaubach at apache dot org) */ public void testRetrieveCorrectFooter() { - // Read initial file in + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("EmbeddedChartHeaderTest.xls"); - HSSFSheet s = wb.getSheetAt( 0 ); - HSSFFooter foot = s.getFooter(); - - assertEquals("Bottom Left", foot.getLeft()); - assertEquals("Bottom Center", foot.getCenter()); - assertEquals("Bottom Right", foot.getRight()); + HSSFSheet s = wb.getSheetAt(0); + HSSFFooter foot = s.getFooter(); + + assertEquals("Bottom Left", foot.getLeft()); + assertEquals("Bottom Center", foot.getCenter()); + assertEquals("Bottom Right", foot.getRight()); } - + /** - * Testcase for Bug 17039 HSSFHeader doesnot support DBCS + * Testcase for Bug 17039 HSSFHeader does not support DBCS */ public void testHeaderHas16bitCharacter() { HSSFWorkbook b = new HSSFWorkbook(); @@ -117,17 +114,17 @@ public final class TestHSSFHeaderFooter extends TestCase { h.setLeft("\u0391"); h.setCenter("\u0392"); h.setRight("\u0393"); - + HSSFWorkbook b2 = HSSFTestDataSamples.writeOutAndReadBack(b); HSSFHeader h2 = b2.getSheet("Test").getHeader(); - + assertEquals(h2.getLeft(),"\u0391"); assertEquals(h2.getCenter(),"\u0392"); assertEquals(h2.getRight(),"\u0393"); } - + /** - * Testcase for Bug 17039 HSSFFooter doesnot support DBCS + * Testcase for Bug 17039 HSSFFooter does not support DBCS */ public void testFooterHas16bitCharacter() { HSSFWorkbook b = new HSSFWorkbook(); @@ -136,10 +133,10 @@ public final class TestHSSFHeaderFooter extends TestCase { f.setLeft("\u0391"); f.setCenter("\u0392"); f.setRight("\u0393"); - + HSSFWorkbook b2 = HSSFTestDataSamples.writeOutAndReadBack(b); HSSFFooter f2 = b2.getSheet("Test").getFooter(); - + assertEquals(f2.getLeft(),"\u0391"); assertEquals(f2.getCenter(),"\u0392"); assertEquals(f2.getRight(),"\u0393"); @@ -152,11 +149,38 @@ public final class TestHSSFHeaderFooter extends TestCase { assertEquals("Header Left " ,h.getLeft(),"\u090f\u0915"); assertEquals("Header Center " ,h.getCenter(),"\u0939\u094b\u0917\u093e"); assertEquals("Header Right " ,h.getRight(),"\u091c\u093e"); - + HSSFFooter f = s.getFooter(); assertEquals("Footer Left " ,f.getLeft(),"\u091c\u093e"); assertEquals("Footer Center " ,f.getCenter(),"\u091c\u093e"); assertEquals("Footer Right " ,f.getRight(),"\u091c\u093e"); } -} + /** + * Excel tolerates files with missing HEADER/FOOTER records. POI should do the same. + */ + public void testMissingHeaderFooterRecord_bug47244() { + // noHeaderFooter47244.xls was created by a slightly modified POI + // which omitted the HEADER/FOOTER records + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("noHeaderFooter47244.xls"); + HSSFSheet sheet = wb.getSheetAt(0); + HSSFFooter footer; + try { + footer = sheet.getFooter(); + } catch (NullPointerException e) { + throw new AssertionFailedError("Identified bug 47244a"); + } + assertEquals("", footer.getRawText()); + HSSFHeader header = sheet.getHeader(); + assertEquals("", header.getRawText()); + + // make sure header / footer is properly linked to underlying data + HSSFHeader header2 = sheet.getHeader(); + header.setCenter("foo"); + assertEquals("foo", header2.getCenter()); + + HSSFFooter footer2 = sheet.getFooter(); + footer.setCenter("bar"); + assertEquals("bar", footer2.getCenter()); + } +}