From 730f394261c5ea8d7ec0f455f018d909fd2e71dd Mon Sep 17 00:00:00 2001 From: Tim Allison Date: Thu, 16 Mar 2017 18:37:13 +0000 Subject: [PATCH] 60826 -- add initial support for streaming reading of xlsb files. git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1787228 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/extractor/ExtractorFactory.java | 8 + .../poi/xssf/XLSBUnsupportedException.java | 4 +- .../poi/xssf/binary/XSSFBCellHeader.java | 71 ++++ .../poi/xssf/binary/XSSFBCellRange.java | 54 +++ .../apache/poi/xssf/binary/XSSFBComment.java | 112 ++++++ .../poi/xssf/binary/XSSFBCommentsTable.java | 113 ++++++ .../poi/xssf/binary/XSSFBHeaderFooter.java | 75 ++++ .../poi/xssf/binary/XSSFBHeaderFooters.java | 87 +++++ .../poi/xssf/binary/XSSFBHyperlinksTable.java | 181 ++++++++++ .../poi/xssf/binary/XSSFBParseException.java | 28 ++ .../apache/poi/xssf/binary/XSSFBParser.java | 105 ++++++ .../poi/xssf/binary/XSSFBRecordType.java | 92 +++++ .../apache/poi/xssf/binary/XSSFBRelation.java | 85 +++++ .../apache/poi/xssf/binary/XSSFBRichStr.java | 47 +++ .../poi/xssf/binary/XSSFBRichTextString.java | 80 +++++ .../xssf/binary/XSSFBSharedStringsTable.java | 137 ++++++++ .../poi/xssf/binary/XSSFBSheetHandler.java | 329 ++++++++++++++++++ .../poi/xssf/binary/XSSFBStylesTable.java | 101 ++++++ .../apache/poi/xssf/binary/XSSFBUtils.java | 108 ++++++ .../poi/xssf/binary/XSSFHyperlinkRecord.java | 117 +++++++ .../org/apache/poi/xssf/binary/package.html | 44 +++ .../poi/xssf/eventusermodel/XSSFBReader.java | 172 +++++++++ .../poi/xssf/eventusermodel/XSSFReader.java | 131 +++++-- .../XSSFBEventBasedExcelExtractor.java | 160 +++++++++ .../XSSFEventBasedExcelExtractor.java | 24 +- .../poi/extractor/TestExtractorFactory.java | 9 + .../binary/TestXSSFBSharedStringsTable.java | 56 +++ .../TestXSSFBSheetHyperlinkManager.java | 54 +++ .../xssf/eventusermodel/TestXSSFBReader.java | 224 ++++++++++++ .../TestXSSFBEventBasedExcelExtractor.java | 102 ++++++ test-data/spreadsheet/51519.xlsb | Bin 0 -> 10897 bytes test-data/spreadsheet/WithTextBox.xlsb | Bin 0 -> 10076 bytes test-data/spreadsheet/comments.xlsb | Bin 0 -> 10796 bytes test-data/spreadsheet/date.xlsb | Bin 0 -> 7566 bytes test-data/spreadsheet/hyperlink.xlsb | Bin 0 -> 7882 bytes test-data/spreadsheet/sample.xlsb | Bin 0 -> 10843 bytes test-data/spreadsheet/testVarious.xlsb | Bin 0 -> 22715 bytes 37 files changed, 2867 insertions(+), 43 deletions(-) create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCellHeader.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCellRange.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBComment.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCommentsTable.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHeaderFooter.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHeaderFooters.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHyperlinksTable.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBParseException.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBParser.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRecordType.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRelation.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRichStr.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRichTextString.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBSharedStringsTable.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBSheetHandler.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBStylesTable.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFBUtils.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/XSSFHyperlinkRecord.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/binary/package.html create mode 100644 src/ooxml/java/org/apache/poi/xssf/eventusermodel/XSSFBReader.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/extractor/XSSFBEventBasedExcelExtractor.java create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/binary/TestXSSFBSharedStringsTable.java create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/binary/TestXSSFBSheetHyperlinkManager.java create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/eventusermodel/TestXSSFBReader.java create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/extractor/TestXSSFBEventBasedExcelExtractor.java create mode 100644 test-data/spreadsheet/51519.xlsb create mode 100644 test-data/spreadsheet/WithTextBox.xlsb create mode 100644 test-data/spreadsheet/comments.xlsb create mode 100644 test-data/spreadsheet/date.xlsb create mode 100644 test-data/spreadsheet/hyperlink.xlsb create mode 100644 test-data/spreadsheet/sample.xlsb create mode 100644 test-data/spreadsheet/testVarious.xlsb diff --git a/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java b/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java index 7533a2742..faae5bacb 100644 --- a/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java +++ b/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java @@ -56,6 +56,7 @@ import org.apache.poi.xdgf.extractor.XDGFVisioExtractor; import org.apache.poi.xslf.extractor.XSLFPowerPointExtractor; import org.apache.poi.xslf.usermodel.XSLFRelation; import org.apache.poi.xslf.usermodel.XSLFSlideShow; +import org.apache.poi.xssf.extractor.XSSFBEventBasedExcelExtractor; import org.apache.poi.xssf.extractor.XSSFEventBasedExcelExtractor; import org.apache.poi.xssf.extractor.XSSFExcelExtractor; import org.apache.poi.xssf.usermodel.XSSFRelation; @@ -244,6 +245,13 @@ public class ExtractorFactory { return new XSLFPowerPointExtractor(new XSLFSlideShow(pkg)); } + // How about xlsb? + for (XSSFRelation rel : XSSFBEventBasedExcelExtractor.SUPPORTED_TYPES) { + if (rel.getContentType().equals(contentType)) { + return new XSSFBEventBasedExcelExtractor(pkg); + } + } + throw new IllegalArgumentException("No supported documents found in the OOXML package (found "+contentType+")"); } catch (IOException e) { diff --git a/src/ooxml/java/org/apache/poi/xssf/XLSBUnsupportedException.java b/src/ooxml/java/org/apache/poi/xssf/XLSBUnsupportedException.java index 63260276f..c6ebcff54 100644 --- a/src/ooxml/java/org/apache/poi/xssf/XLSBUnsupportedException.java +++ b/src/ooxml/java/org/apache/poi/xssf/XLSBUnsupportedException.java @@ -19,7 +19,9 @@ package org.apache.poi.xssf; import org.apache.poi.UnsupportedFileFormatException; /** - * We don't support .xlsb files, sorry + * We don't support .xlsb for read and write via {@link org.apache.poi.xssf.usermodel.XSSFWorkbook}. + * As of POI 3.15-beta3, we do support streaming reading of xlsb files + * via {@link org.apache.poi.xssf.eventusermodel.XSSFBReader} */ public class XLSBUnsupportedException extends UnsupportedFileFormatException { private static final long serialVersionUID = 7849681804154571175L; diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCellHeader.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCellHeader.java new file mode 100644 index 000000000..5b427ae81 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCellHeader.java @@ -0,0 +1,71 @@ +/* ==================================================================== + 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.xssf.binary; + +import org.apache.poi.ss.util.CellReference; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; + +/** + * This class encapsulates what the spec calls a "Cell" object. + * I added "Header" to clarify that this does not contain the contents + * of the cell, only the column number, the style id and the phonetic boolean + */ +@Internal +class XSSFBCellHeader { + public static int length = 8; + + /** + * + * @param data raw data + * @param offset offset at which to start reading the record + * @param currentRow 0-based current row count + * @param cell cell buffer to update + */ + public static void parse(byte[] data, int offset, int currentRow, XSSFBCellHeader cell) { + long colNum = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + int styleIdx = XSSFBUtils.get24BitInt(data, offset); offset += 3; + //TODO: range checking + boolean showPhonetic = false;//TODO: fill this out + cell.reset(currentRow, (int)colNum, styleIdx, showPhonetic); + } + + private int rowNum; + private int colNum; + private int styleIdx; + private boolean showPhonetic; + + public void reset(int rowNum, int colNum, int styleIdx, boolean showPhonetic) { + this.rowNum = rowNum; + this.colNum = colNum; + this.styleIdx = styleIdx; + this.showPhonetic = showPhonetic; + } + + int getColNum() { + return colNum; + } + + String formatAddressAsString() { + return CellReference.convertNumToColString(colNum)+(rowNum+1); + } + + int getStyleIdx() { + return styleIdx; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCellRange.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCellRange.java new file mode 100644 index 000000000..3e2e79d8d --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCellRange.java @@ -0,0 +1,54 @@ +/* ==================================================================== + 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.xssf.binary; + + +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; + +@Internal +class XSSFBCellRange { + + public final static int length = 4* LittleEndian.INT_SIZE; + /** + * Parses an RfX cell range from the data starting at the offset. + * This performs no range checking. + * @param data raw bytes + * @param offset offset at which to start reading from data + * @param cellRange to overwrite. If null, a new cellRange will be created. + * @return a mutable cell range. + */ + public static XSSFBCellRange parse(byte[] data, int offset, XSSFBCellRange cellRange) { + if (cellRange == null) { + cellRange = new XSSFBCellRange(); + } + cellRange.firstRow = XSSFBUtils.castToInt(LittleEndian.getUInt(data, offset)); offset += LittleEndian.INT_SIZE; + cellRange.lastRow = XSSFBUtils.castToInt(LittleEndian.getUInt(data, offset)); offset += LittleEndian.INT_SIZE; + cellRange.firstCol = XSSFBUtils.castToInt(LittleEndian.getUInt(data, offset)); offset += LittleEndian.INT_SIZE; + cellRange.lastCol = XSSFBUtils.castToInt(LittleEndian.getUInt(data, offset)); + + return cellRange; + } + + int firstRow; + int lastRow; + int firstCol; + int lastCol; + + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBComment.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBComment.java new file mode 100644 index 000000000..ae7c1c56e --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBComment.java @@ -0,0 +1,112 @@ +/* ==================================================================== + 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.xssf.binary; + + +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.RichTextString; +import org.apache.poi.ss.util.CellAddress; +import org.apache.poi.util.Internal; +import org.apache.poi.xssf.usermodel.XSSFComment; + +@Internal +class XSSFBComment extends XSSFComment { + + private final CellAddress cellAddress; + private final String author; + private final XSSFBRichTextString comment; + private boolean visible = true; + + XSSFBComment(CellAddress cellAddress, String author, String comment) { + super(null, null, null); + this.cellAddress = cellAddress; + this.author = author; + this.comment = new XSSFBRichTextString(comment); + } + + @Override + public void setVisible(boolean visible) { + throw new IllegalArgumentException("XSSFBComment is read only."); + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public CellAddress getAddress() { + return cellAddress; + } + + @Override + public void setAddress(CellAddress addr) { + throw new IllegalArgumentException("XSSFBComment is read only"); + } + + @Override + public void setAddress(int row, int col) { + throw new IllegalArgumentException("XSSFBComment is read only"); + + } + + @Override + public int getRow() { + return cellAddress.getRow(); + } + + @Override + public void setRow(int row) { + throw new IllegalArgumentException("XSSFBComment is read only"); + } + + @Override + public int getColumn() { + return cellAddress.getColumn(); + } + + @Override + public void setColumn(int col) { + throw new IllegalArgumentException("XSSFBComment is read only"); + } + + @Override + public String getAuthor() { + return author; + } + + @Override + public void setAuthor(String author) { + throw new IllegalArgumentException("XSSFBComment is read only"); + } + + @Override + public XSSFBRichTextString getString() { + return comment; + } + + @Override + public void setString(RichTextString string) { + throw new IllegalArgumentException("XSSFBComment is read only"); + } + + @Override + public ClientAnchor getClientAnchor() { + return null; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCommentsTable.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCommentsTable.java new file mode 100644 index 000000000..642eaf99b --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBCommentsTable.java @@ -0,0 +1,113 @@ +/* ==================================================================== + 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.xssf.binary; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.TreeMap; + +import org.apache.poi.ss.util.CellAddress; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; + +@Internal +public class XSSFBCommentsTable extends XSSFBParser { + + private Map comments = new TreeMap(new CellAddressComparator());//String is the cellAddress A1 + private Queue commentAddresses = new LinkedList(); + private List authors = new ArrayList(); + + //these are all used only during parsing, and they are mutable! + private int authorId = -1; + private CellAddress cellAddress = null; + private XSSFBCellRange cellRange = null; + private String comment = null; + private StringBuilder authorBuffer = new StringBuilder(); + + + public XSSFBCommentsTable(InputStream is) throws IOException { + super(is); + parse(); + commentAddresses.addAll(comments.keySet()); + } + + @Override + public void handleRecord(int id, byte[] data) throws XSSFBParseException { + XSSFBRecordType recordType = XSSFBRecordType.lookup(id); + switch (recordType) { + case BrtBeginComment: + int offset = 0; + authorId = XSSFBUtils.castToInt(LittleEndian.getUInt(data)); offset += LittleEndian.INT_SIZE; + cellRange = XSSFBCellRange.parse(data, offset, cellRange); + offset+= XSSFBCellRange.length; + //for strict parsing; confirm that firstRow==lastRow and firstCol==colLats (2.4.28) + cellAddress = new CellAddress(cellRange.firstRow, cellRange.firstCol); + break; + case BrtCommentText: + XSSFBRichStr xssfbRichStr = XSSFBRichStr.build(data, 0); + comment = xssfbRichStr.getString(); + break; + case BrtEndComment: + comments.put(cellAddress, new XSSFBComment(cellAddress, authors.get(authorId), comment)); + authorId = -1; + cellAddress = null; + break; + case BrtCommentAuthor: + authorBuffer.setLength(0); + XSSFBUtils.readXLWideString(data, 0, authorBuffer); + authors.add(authorBuffer.toString()); + break; + } + } + + + public Queue getAddresses() { + return commentAddresses; + } + + public XSSFBComment get(CellAddress cellAddress) { + if (cellAddress == null) { + return null; + } + return comments.get(cellAddress); + } + + private final static class CellAddressComparator implements Comparator { + + @Override + public int compare(CellAddress o1, CellAddress o2) { + if (o1.getRow() < o2.getRow()) { + return -1; + } else if (o1.getRow() > o2.getRow()) { + return 1; + } + if (o1.getColumn() < o2.getColumn()) { + return -1; + } else if (o1.getColumn() > o2.getColumn()) { + return 1; + } + return 0; + } + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHeaderFooter.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHeaderFooter.java new file mode 100644 index 000000000..1f43e35dc --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHeaderFooter.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.xssf.binary; + +import org.apache.poi.util.Internal; +import org.apache.poi.xssf.usermodel.helpers.HeaderFooterHelper; + +@Internal +class XSSFBHeaderFooter { + private final String headerFooterTypeLabel; + private final boolean isHeader; + private String rawString; + private HeaderFooterHelper headerFooterHelper = new HeaderFooterHelper(); + + + XSSFBHeaderFooter(String headerFooterTypeLabel, boolean isHeader) { + this.headerFooterTypeLabel = headerFooterTypeLabel; + this.isHeader = isHeader; + } + + String getHeaderFooterTypeLabel() { + return headerFooterTypeLabel; + } + + String getRawString() { + return rawString; + } + + String getString() { + StringBuilder sb = new StringBuilder(); + String left = headerFooterHelper.getLeftSection(rawString); + String center = headerFooterHelper.getCenterSection(rawString); + String right = headerFooterHelper.getRightSection(rawString); + if (left != null && left.length() > 0) { + sb.append(left); + } + if (center != null && center.length() > 0) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(center); + } + if (right != null && right.length() > 0) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(right); + } + return sb.toString(); + } + + void setRawString(String rawString) { + this.rawString = rawString; + } + + boolean isHeader() { + return isHeader; + } + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHeaderFooters.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHeaderFooters.java new file mode 100644 index 000000000..c70b7843e --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHeaderFooters.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.xssf.binary; + +import org.apache.poi.util.Internal; + +@Internal +class XSSFBHeaderFooters { + + public static XSSFBHeaderFooters parse(byte[] data) { + boolean diffOddEven = false; + boolean diffFirst = false; + boolean scaleWDoc = false; + boolean alignMargins = false; + + int offset = 2; + XSSFBHeaderFooters xssfbHeaderFooter = new XSSFBHeaderFooters(); + xssfbHeaderFooter.header = new XSSFBHeaderFooter("header", true); + xssfbHeaderFooter.footer = new XSSFBHeaderFooter("footer", false); + xssfbHeaderFooter.headerEven = new XSSFBHeaderFooter("evenHeader", true); + xssfbHeaderFooter.footerEven = new XSSFBHeaderFooter("evenFooter", false); + xssfbHeaderFooter.headerFirst = new XSSFBHeaderFooter("firstHeader", true); + xssfbHeaderFooter.footerFirst = new XSSFBHeaderFooter("firstFooter", false); + offset += readHeaderFooter(data, offset, xssfbHeaderFooter.header); + offset += readHeaderFooter(data, offset, xssfbHeaderFooter.footer); + offset += readHeaderFooter(data, offset, xssfbHeaderFooter.headerEven); + offset += readHeaderFooter(data, offset, xssfbHeaderFooter.footerEven); + offset += readHeaderFooter(data, offset, xssfbHeaderFooter.headerFirst); + readHeaderFooter(data, offset, xssfbHeaderFooter.footerFirst); + return xssfbHeaderFooter; + } + + private static int readHeaderFooter(byte[] data, int offset, XSSFBHeaderFooter headerFooter) { + if (offset + 4 >= data.length) { + return 0; + } + StringBuilder sb = new StringBuilder(); + int bytesRead = XSSFBUtils.readXLNullableWideString(data, offset, sb); + headerFooter.setRawString(sb.toString()); + return bytesRead; + } + + private XSSFBHeaderFooter header; + private XSSFBHeaderFooter footer; + private XSSFBHeaderFooter headerEven; + private XSSFBHeaderFooter footerEven; + private XSSFBHeaderFooter headerFirst; + private XSSFBHeaderFooter footerFirst; + + public XSSFBHeaderFooter getHeader() { + return header; + } + + public XSSFBHeaderFooter getFooter() { + return footer; + } + + public XSSFBHeaderFooter getHeaderEven() { + return headerEven; + } + + public XSSFBHeaderFooter getFooterEven() { + return footerEven; + } + + public XSSFBHeaderFooter getHeaderFirst() { + return headerFirst; + } + + public XSSFBHeaderFooter getFooterFirst() { + return footerFirst; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHyperlinksTable.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHyperlinksTable.java new file mode 100644 index 000000000..28c020c57 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBHyperlinksTable.java @@ -0,0 +1,181 @@ +/* ==================================================================== + 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.xssf.binary; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.ss.util.CellAddress; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeUtil; +import org.apache.poi.util.Internal; +import org.apache.poi.xssf.usermodel.XSSFRelation; + +@Internal +public class XSSFBHyperlinksTable { + + private final static BitSet RECORDS = new BitSet(); + + + static { + RECORDS.set(XSSFBRecordType.BrtHLink.getId()); + } + + + private final List hyperlinkRecords = new ArrayList(); + + //cache the relId to hyperlink url from the sheet's .rels + private Map relIdToHyperlink = new HashMap(); + + public XSSFBHyperlinksTable(PackagePart sheetPart) throws IOException { + //load the urls from the sheet .rels + loadUrlsFromSheetRels(sheetPart); + //now load the hyperlinks from the bottom of the sheet + HyperlinkSheetScraper scraper = new HyperlinkSheetScraper(sheetPart.getInputStream()); + scraper.parse(); + } + + /** + * + * @return a map of the hyperlinks. The key is the top left cell address in their CellRange + */ + public Map> getHyperLinks() { + Map> hyperlinkMap = + new TreeMap>(new TopLeftCellAddressComparator()); + for (XSSFHyperlinkRecord hyperlinkRecord : hyperlinkRecords) { + CellAddress cellAddress = new CellAddress(hyperlinkRecord.getCellRangeAddress().getFirstRow(), + hyperlinkRecord.getCellRangeAddress().getFirstColumn()); + List list = hyperlinkMap.get(cellAddress); + if (list == null) { + list = new ArrayList(); + } + list.add(hyperlinkRecord); + hyperlinkMap.put(cellAddress, list); + } + return hyperlinkMap; + } + + + /** + * + * @param cellAddress cell address to find + * @return null if not a hyperlink + */ + public List findHyperlinkRecord(CellAddress cellAddress) { + List overlapping = null; + CellRangeAddress targetCellRangeAddress = new CellRangeAddress(cellAddress.getRow(), + cellAddress.getRow(), + cellAddress.getColumn(), + cellAddress.getColumn()); + for (XSSFHyperlinkRecord record : hyperlinkRecords) { + if (CellRangeUtil.intersect(targetCellRangeAddress, record.getCellRangeAddress()) != CellRangeUtil.NO_INTERSECTION) { + if (overlapping == null) { + overlapping = new ArrayList(); + } + overlapping.add(record); + } + } + return overlapping; + } + + private void loadUrlsFromSheetRels(PackagePart sheetPart) { + try { + for (PackageRelationship rel : sheetPart.getRelationshipsByType(XSSFRelation.SHEET_HYPERLINKS.getRelation())) { + relIdToHyperlink.put(rel.getId(), rel.getTargetURI().toString()); + } + } catch (InvalidFormatException e) { + //swallow + } + } + + private class HyperlinkSheetScraper extends XSSFBParser { + + private XSSFBCellRange hyperlinkCellRange = new XSSFBCellRange(); + private final StringBuilder xlWideStringBuffer = new StringBuilder(); + + HyperlinkSheetScraper(InputStream is) { + super(is, RECORDS); + } + + @Override + public void handleRecord(int recordType, byte[] data) throws XSSFBParseException { + if (recordType != XSSFBRecordType.BrtHLink.getId()) { + return; + } + int offset = 0; + String relId = ""; + String location = ""; + String toolTip = ""; + String display = ""; + + hyperlinkCellRange = XSSFBCellRange.parse(data, offset, hyperlinkCellRange); + offset += XSSFBCellRange.length; + xlWideStringBuffer.setLength(0); + offset += XSSFBUtils.readXLNullableWideString(data, offset, xlWideStringBuffer); + relId = xlWideStringBuffer.toString(); + xlWideStringBuffer.setLength(0); + offset += XSSFBUtils.readXLWideString(data, offset, xlWideStringBuffer); + location = xlWideStringBuffer.toString(); + xlWideStringBuffer.setLength(0); + offset += XSSFBUtils.readXLWideString(data, offset, xlWideStringBuffer); + toolTip = xlWideStringBuffer.toString(); + xlWideStringBuffer.setLength(0); + offset += XSSFBUtils.readXLWideString(data, offset, xlWideStringBuffer); + display = xlWideStringBuffer.toString(); + CellRangeAddress cellRangeAddress = new CellRangeAddress(hyperlinkCellRange.firstRow, hyperlinkCellRange.lastRow, hyperlinkCellRange.firstCol, hyperlinkCellRange.lastCol); + + String url = relIdToHyperlink.get(relId); + if (location == null || location.length() == 0) { + location = url; + } + + hyperlinkRecords.add( + new XSSFHyperlinkRecord(cellRangeAddress, relId, location, toolTip, display) + ); + } + } + + private static class TopLeftCellAddressComparator implements Comparator { + + @Override + public int compare(CellAddress o1, CellAddress o2) { + if (o1.getRow() < o2.getRow()) { + return -1; + } else if (o1.getRow() > o2.getRow()) { + return 1; + } + if (o1.getColumn() < o2.getColumn()) { + return -1; + } else if (o1.getColumn() > o2.getColumn()) { + return 1; + } + return 0; + } + } + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBParseException.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBParseException.java new file mode 100644 index 000000000..69ba7f041 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBParseException.java @@ -0,0 +1,28 @@ +/* ==================================================================== + 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.xssf.binary; + +/** + * Parse exception while reading an xssfb + */ +public class XSSFBParseException extends RuntimeException { + + public XSSFBParseException(String msg) { + super(msg); + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBParser.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBParser.java new file mode 100644 index 000000000..cace84316 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBParser.java @@ -0,0 +1,105 @@ +/* ==================================================================== + 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.xssf.binary; + +import java.io.IOException; +import java.io.InputStream; +import java.util.BitSet; + +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianInputStream; + +/** + * Experimental parser for Microsoft's ooxml xssfb format. + * Not thread safe, obviously. Need to create a new one + * for each thread. + */ +@Internal +public abstract class XSSFBParser { + + private final LittleEndianInputStream is; + private final BitSet records; + + public XSSFBParser(InputStream is) { + this.is = new LittleEndianInputStream(is); + records = null; + } + + XSSFBParser(InputStream is, BitSet bitSet) { + this.is = new LittleEndianInputStream(is); + records = bitSet; + } + + public void parse() throws IOException { + + while (true) { + int bInt = is.read(); + if (bInt == -1) { + return; + } + readNext((byte) bInt); + } + } + + private void readNext(byte b1) throws IOException { + int recordId = 0; + + //if highest bit == 1 + if ((b1 >> 7 & 1) == 1) { + byte b2 = is.readByte(); + b1 &= ~(1<<7); //unset highest bit + b2 &= ~(1<<7); //unset highest bit (if it exists?) + recordId = (128*(int)b2)+(int)b1; + } else { + recordId = (int)b1; + } + + long recordLength = 0; + int i = 0; + boolean halt = false; + while (i < 4 && ! halt) { + byte b = is.readByte(); + halt = (b >> 7 & 1) == 0; //if highest bit !=1 then continue + b &= ~(1<<7); + recordLength += (int)b << (i*7); //multiply by 128^i + i++; + + } + if (records == null || records.get(recordId)) { + //add sanity check for length? + byte[] buff = new byte[(int) recordLength]; + is.readFully(buff); + handleRecord(recordId, buff); + } else { + long length = is.skip(recordLength); + if (length != recordLength) { + throw new XSSFBParseException("End of file reached before expected.\t"+ + "Tried to skip "+recordLength + ", but only skipped "+length); + } + } + } + + //It hurts, hurts, hurts to create a new byte array for every record. + //However, on a large Excel spreadsheet, this parser was 1/3 faster than + //the ooxml sax parser (5 seconds for xssfb and 7.5 seconds for xssf. + //The code is far cleaner to have the parser read all + //of the data rather than having every component promise that it read + //the correct amount. + abstract public void handleRecord(int recordType, byte[] data) throws XSSFBParseException; + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRecordType.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRecordType.java new file mode 100644 index 000000000..65663f7fd --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRecordType.java @@ -0,0 +1,92 @@ +/* ==================================================================== + 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.xssf.binary; + +import org.apache.poi.util.Internal; + +@Internal +public enum XSSFBRecordType { + + BrtCellBlank(1), + BrtCellRk(2), + BrtCellError(3), + BrtCellBool(4), + BrtCellReal(5), + BrtCellSt(6), + BrtCellIsst(7), + BrtFmlaString(8), + BrtFmlaNum(9), + BrtFmlaBool(10), + BrtFmlaError(11), + BrtRowHdr(0), + BrtCellRString(62), + BrtBeginSheet(129), + BrtWsProp(147), + BrtWsDim(148), + BrtColInfo(60), + BrtBeginSheetData(145), + BrtEndSheetData(146), + BrtHLink(494), + BrtBeginHeaderFooter(479), + + //comments + BrtBeginCommentAuthors(630), + BrtEndCommentAuthors(631), + BrtCommentAuthor(632), + BrtBeginComment(635), + BrtCommentText(637), + BrtEndComment(636), + //styles table + BrtXf(47), + BrtFmt(44), + BrtBeginFmts(615), + BrtEndFmts(616), + BrtBeginCellXFs(617), + BrtEndCellXFs(618), + BrtBeginCellStyleXFS(626), + BrtEndCellStyleXFS(627), + + //stored strings table + BrtSstItem(19), //stored strings items + BrtBeginSst(159), //stored strings begin sst + BrtEndSst(160), //stored strings end sst + + BrtBundleSh(156), //defines worksheet in wb part + Unimplemented(-1); + + + private final int id; + + XSSFBRecordType(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static XSSFBRecordType lookup(int id) { + for (XSSFBRecordType r : XSSFBRecordType.values()) { + if (r.id == id) { + return r; + } + } + return Unimplemented; + } + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRelation.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRelation.java new file mode 100644 index 000000000..3f0b0286d --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRelation.java @@ -0,0 +1,85 @@ +/* ==================================================================== + 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.xssf.binary; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; + +import org.apache.poi.POIXMLDocumentPart; +import org.apache.poi.POIXMLRelation; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.openxml4j.opc.PackageRelationshipCollection; +import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; +import org.apache.poi.openxml4j.opc.PackagingURIHelper; +import org.apache.poi.util.Internal; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * Need to have this mirror class of {@link org.apache.poi.xssf.usermodel.XSSFRelation} + * because of conflicts with regular ooxml relations. + * If we failed to break this into a separate class, in the cases of SharedStrings and Styles, + * 2 parts would exist, and "Packages shall not contain equivalent part names..." + *

+ * Also, we need to avoid the possibility of breaking the marshalling process for xml. + */ +@Internal +public class XSSFBRelation extends POIXMLRelation { + private static final POILogger log = POILogFactory.getLogger(XSSFBRelation.class); + + static final XSSFBRelation SHARED_STRINGS_BINARY = new XSSFBRelation( + "application/vnd.ms-excel.sharedStrings", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings", + "/xl/sharedStrings.bin", + null + ); + + public static final XSSFBRelation STYLES_BINARY = new XSSFBRelation( + "application/vnd.ms-excel.styles", + PackageRelationshipTypes.STYLE_PART, + "/xl/styles.bin", + null + ); + + private XSSFBRelation(String type, String rel, String defaultName, Class cls) { + super(type, rel, defaultName, cls); + } + + /** + * Fetches the InputStream to read the contents, based + * of the specified core part, for which we are defined + * as a suitable relationship + */ + public InputStream getContents(PackagePart corePart) throws IOException, InvalidFormatException { + PackageRelationshipCollection prc = + corePart.getRelationshipsByType(getRelation()); + Iterator it = prc.iterator(); + if (it.hasNext()) { + PackageRelationship rel = it.next(); + PackagePartName relName = PackagingURIHelper.createPartName(rel.getTargetURI()); + PackagePart part = corePart.getPackage().getPart(relName); + return part.getInputStream(); + } + log.log(POILogger.WARN, "No part " + getDefaultFileName() + " found"); + return null; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRichStr.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRichStr.java new file mode 100644 index 000000000..e9ba59a4e --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRichStr.java @@ -0,0 +1,47 @@ +/* ==================================================================== + 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.xssf.binary; + +import org.apache.poi.util.Internal; + +@Internal +class XSSFBRichStr { + + public static XSSFBRichStr build(byte[] bytes, int offset) throws XSSFBParseException { + byte first = bytes[offset]; + boolean dwSizeStrRunExists = (first >> 7 & 1) == 1;//first bit == 1? + boolean phoneticExists = (first >> 6 & 1) == 1;//second bit == 1? + StringBuilder sb = new StringBuilder(); + + int read = XSSFBUtils.readXLWideString(bytes, offset+1, sb); + //TODO: parse phonetic strings. + return new XSSFBRichStr(sb.toString(), ""); + } + + private final String string; + private final String phoneticString; + + XSSFBRichStr(String string, String phoneticString) { + this.string = string; + this.phoneticString = phoneticString; + } + + public String getString() { + return string; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRichTextString.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRichTextString.java new file mode 100644 index 000000000..1fb5b54ae --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBRichTextString.java @@ -0,0 +1,80 @@ +/* ==================================================================== + 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.xssf.binary; + +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.util.Internal; +import org.apache.poi.xssf.usermodel.XSSFRichTextString; + +/** + * Wrapper class around String so that we can use it in Comment. + * Nothing has been implemented yet except for {@link #getString()}. + */ +@Internal +class XSSFBRichTextString extends XSSFRichTextString { + private final String string; + + XSSFBRichTextString(String string) { + this.string = string; + } + + @Override + public void applyFont(int startIndex, int endIndex, short fontIndex) { + + } + + @Override + public void applyFont(int startIndex, int endIndex, Font font) { + + } + + @Override + public void applyFont(Font font) { + + } + + @Override + public void clearFormatting() { + + } + + @Override + public String getString() { + return string; + } + + @Override + public int length() { + return string.length(); + } + + @Override + public int numFormattingRuns() { + return 0; + } + + @Override + public int getIndexOfFormattingRun(int index) { + return 0; + } + + @Override + public void applyFont(short fontIndex) { + + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBSharedStringsTable.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBSharedStringsTable.java new file mode 100644 index 000000000..49d1a46f9 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBSharedStringsTable.java @@ -0,0 +1,137 @@ +/* ==================================================================== + 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.xssf.binary; + + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; +import org.xml.sax.SAXException; + +@Internal +public class XSSFBSharedStringsTable { + + /** + * An integer representing the total count of strings in the workbook. This count does not + * include any numbers, it counts only the total of text strings in the workbook. + */ + private int count; + + /** + * An integer representing the total count of unique strings in the Shared String Table. + * A string is unique even if it is a copy of another string, but has different formatting applied + * at the character level. + */ + private int uniqueCount; + + /** + * The shared strings table. + */ + private List strings = new ArrayList(); + + /** + * @param pkg The {@link OPCPackage} to use as basis for the shared-strings table. + * @throws IOException If reading the data from the package fails. + * @throws SAXException if parsing the XML data fails. + */ + public XSSFBSharedStringsTable(OPCPackage pkg) + throws IOException, SAXException { + ArrayList parts = + pkg.getPartsByContentType(XSSFBRelation.SHARED_STRINGS_BINARY.getContentType()); + + // Some workbooks have no shared strings table. + if (parts.size() > 0) { + PackagePart sstPart = parts.get(0); + + readFrom(sstPart.getInputStream()); + } + } + + /** + * Like POIXMLDocumentPart constructor + * + * @since POI 3.14-Beta3 + */ + XSSFBSharedStringsTable(PackagePart part) throws IOException, SAXException { + readFrom(part.getInputStream()); + } + + private void readFrom(InputStream inputStream) throws IOException { + SSTBinaryReader reader = new SSTBinaryReader(inputStream); + reader.parse(); + } + + public List getItems() { + return strings; + } + + public String getEntryAt(int i) { + return strings.get(i); + } + + /** + * Return an integer representing the total count of strings in the workbook. This count does not + * include any numbers, it counts only the total of text strings in the workbook. + * + * @return the total count of strings in the workbook + */ + public int getCount() { + return this.count; + } + + /** + * Returns an integer representing the total count of unique strings in the Shared String Table. + * A string is unique even if it is a copy of another string, but has different formatting applied + * at the character level. + * + * @return the total count of unique strings in the workbook + */ + public int getUniqueCount() { + return this.uniqueCount; + } + + private class SSTBinaryReader extends XSSFBParser { + + SSTBinaryReader(InputStream is) { + super(is); + } + + @Override + public void handleRecord(int recordType, byte[] data) throws XSSFBParseException { + XSSFBRecordType type = XSSFBRecordType.lookup(recordType); + + switch (type) { + case BrtSstItem: + XSSFBRichStr rstr = XSSFBRichStr.build(data, 0); + strings.add(rstr.getString()); + break; + case BrtBeginSst: + count = (int) LittleEndian.getUInt(data,0); + uniqueCount = (int) LittleEndian.getUInt(data, 4); + break; + } + + } + } + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBSheetHandler.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBSheetHandler.java new file mode 100644 index 000000000..ca5dab5a3 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBSheetHandler.java @@ -0,0 +1,329 @@ +/* ==================================================================== + 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.xssf.binary; + + +import java.io.InputStream; +import java.util.Queue; + +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.util.CellAddress; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler; +import org.apache.poi.xssf.usermodel.XSSFComment; +import org.apache.poi.xssf.usermodel.XSSFRichTextString; + +@Internal +public class XSSFBSheetHandler extends XSSFBParser { + + private final static int CHECK_ALL_ROWS = -1; + + private final XSSFBSharedStringsTable stringsTable; + private final XSSFSheetXMLHandler.SheetContentsHandler handler; + private final XSSFBStylesTable styles; + private final XSSFBCommentsTable comments; + private final DataFormatter dataFormatter; + private final boolean formulasNotResults;//TODO: implement this + + private int lastEndedRow = -1; + private int lastStartedRow = -1; + private int currentRow = 0; + private byte[] rkBuffer = new byte[8]; + private XSSFBCellRange hyperlinkCellRange = null; + private StringBuilder xlWideStringBuffer = new StringBuilder(); + + private final XSSFBCellHeader cellBuffer = new XSSFBCellHeader(); + public XSSFBSheetHandler(InputStream is, + XSSFBStylesTable styles, + XSSFBCommentsTable comments, + XSSFBSharedStringsTable strings, + XSSFSheetXMLHandler.SheetContentsHandler sheetContentsHandler, + DataFormatter dataFormatter, + boolean formulasNotResults) { + super(is); + this.styles = styles; + this.comments = comments; + this.stringsTable = strings; + this.handler = sheetContentsHandler; + this.dataFormatter = dataFormatter; + this.formulasNotResults = formulasNotResults; + } + + @Override + public void handleRecord(int id, byte[] data) throws XSSFBParseException { + XSSFBRecordType type = XSSFBRecordType.lookup(id); + + switch(type) { + case BrtRowHdr: + long rw = LittleEndian.getUInt(data, 0); + if (rw > 0x00100000L) {//could make sure this is larger than currentRow, according to spec? + throw new XSSFBParseException("Row number beyond allowable range: "+rw); + } + currentRow = (int)rw; + checkMissedComments(currentRow); + startRow(currentRow); + break; + case BrtCellIsst: + handleBrtCellIsst(data); + break; + case BrtCellSt: //TODO: needs test + handleCellSt(data); + break; + case BrtCellRk: + handleCellRk(data); + break; + case BrtCellReal: + handleCellReal(data); + break; + case BrtCellBool: + handleBoolean(data); + break; + case BrtCellError: + handleCellError(data); + break; + case BrtCellBlank: + beforeCellValue(data);//read cell info and check for missing comments + break; + case BrtFmlaString: + handleFmlaString(data); + break; + case BrtFmlaNum: + handleFmlaNum(data); + break; + case BrtFmlaError: + handleFmlaError(data); + break; + //TODO: All the PCDI and PCDIA + case BrtEndSheetData: + checkMissedComments(CHECK_ALL_ROWS); + endRow(lastStartedRow); + break; + case BrtBeginHeaderFooter: + handleHeaderFooter(data); + break; + } + } + + + private void beforeCellValue(byte[] data) { + XSSFBCellHeader.parse(data, 0, currentRow, cellBuffer); + checkMissedComments(currentRow, cellBuffer.getColNum()); + } + + private void handleCellValue(String formattedValue) { + CellAddress cellAddress = new CellAddress(currentRow, cellBuffer.getColNum()); + XSSFBComment comment = null; + if (comments != null) { + comment = comments.get(cellAddress); + } + handler.cell(cellAddress.formatAsString(), formattedValue, comment); + } + + private void handleFmlaNum(byte[] data) { + beforeCellValue(data); + //xNum + double val = LittleEndian.getDouble(data, XSSFBCellHeader.length); + String formatString = styles.getNumberFormatString(cellBuffer.getStyleIdx()); + String formattedVal = dataFormatter.formatRawCellContents(val, cellBuffer.getStyleIdx(), formatString); + handleCellValue(formattedVal); + } + + private void handleCellSt(byte[] data) { + beforeCellValue(data); + xlWideStringBuffer.setLength(0); + XSSFBUtils.readXLWideString(data, XSSFBCellHeader.length, xlWideStringBuffer); + handleCellValue(xlWideStringBuffer.toString()); + } + + private void handleFmlaString(byte[] data) { + beforeCellValue(data); + xlWideStringBuffer.setLength(0); + XSSFBUtils.readXLWideString(data, XSSFBCellHeader.length, xlWideStringBuffer); + handleCellValue(xlWideStringBuffer.toString()); + } + + private void handleCellError(byte[] data) { + beforeCellValue(data); + //TODO, read byte to figure out the type of error + handleCellValue("ERROR"); + } + + private void handleFmlaError(byte[] data) { + beforeCellValue(data); + //TODO, read byte to figure out the type of error + handleCellValue("ERROR"); + } + + private void handleBoolean(byte[] data) { + beforeCellValue(data); + String formattedVal = (data[XSSFBCellHeader.length] == 1) ? "TRUE" : "FALSE"; + handleCellValue(formattedVal); + } + + private void handleCellReal(byte[] data) { + beforeCellValue(data); + //xNum + double val = LittleEndian.getDouble(data, XSSFBCellHeader.length); + String formatString = styles.getNumberFormatString(cellBuffer.getStyleIdx()); + String formattedVal = dataFormatter.formatRawCellContents(val, cellBuffer.getStyleIdx(), formatString); + handleCellValue(formattedVal); + } + + private void handleCellRk(byte[] data) { + beforeCellValue(data); + double val = rkNumber(data, XSSFBCellHeader.length); + String formatString = styles.getNumberFormatString(cellBuffer.getStyleIdx()); + String formattedVal = dataFormatter.formatRawCellContents(val, cellBuffer.getStyleIdx(), formatString); + handleCellValue(formattedVal); + } + + private void handleBrtCellIsst(byte[] data) { + beforeCellValue(data); + long idx = LittleEndian.getUInt(data, XSSFBCellHeader.length); + //check for out of range, buffer overflow + + XSSFRichTextString rtss = new XSSFRichTextString(stringsTable.getEntryAt((int)idx)); + handleCellValue(rtss.getString()); + } + + + private void handleHeaderFooter(byte[] data) { + XSSFBHeaderFooters headerFooter = XSSFBHeaderFooters.parse(data); + outputHeaderFooter(headerFooter.getHeader()); + outputHeaderFooter(headerFooter.getFooter()); + outputHeaderFooter(headerFooter.getHeaderEven()); + outputHeaderFooter(headerFooter.getFooterEven()); + outputHeaderFooter(headerFooter.getHeaderFirst()); + outputHeaderFooter(headerFooter.getFooterFirst()); + } + + private void outputHeaderFooter(XSSFBHeaderFooter headerFooter) { + String text = headerFooter.getString(); + if (text != null && text.trim().length() > 0) { + handler.headerFooter(text, headerFooter.isHeader(), headerFooter.getHeaderFooterTypeLabel()); + } + } + + + //at start of next cell or end of row, return the cellAddress if it equals currentRow and col + private void checkMissedComments(int currentRow, int colNum) { + if (comments == null) { + return; + } + Queue queue = comments.getAddresses(); + while (queue.size() > 0) { + CellAddress cellAddress = queue.peek(); + if (cellAddress.getRow() == currentRow && cellAddress.getColumn() < colNum) { + cellAddress = queue.remove(); + dumpEmptyCellComment(cellAddress, comments.get(cellAddress)); + } else if (cellAddress.getRow() == currentRow && cellAddress.getColumn() == colNum) { + queue.remove(); + return; + } else if (cellAddress.getRow() == currentRow && cellAddress.getColumn() > colNum) { + return; + } else if (cellAddress.getRow() > currentRow) { + return; + } + } + } + + //check for anything from rows before + private void checkMissedComments(int currentRow) { + if (comments == null) { + return; + } + Queue queue = comments.getAddresses(); + int lastInterpolatedRow = -1; + while (queue.size() > 0) { + CellAddress cellAddress = queue.peek(); + if (currentRow == CHECK_ALL_ROWS || cellAddress.getRow() < currentRow) { + cellAddress = queue.remove(); + if (cellAddress.getRow() != lastInterpolatedRow) { + startRow(cellAddress.getRow()); + } + dumpEmptyCellComment(cellAddress, comments.get(cellAddress)); + lastInterpolatedRow = cellAddress.getRow(); + } else { + break; + } + } + + } + + private void startRow(int row) { + if (row == lastStartedRow) { + return; + } + + if (lastStartedRow != lastEndedRow) { + endRow(lastStartedRow); + } + handler.startRow(row); + lastStartedRow = row; + } + + private void endRow(int row) { + if (lastEndedRow == row) { + return; + } + handler.endRow(row); + lastEndedRow = row; + } + + private void dumpEmptyCellComment(CellAddress cellAddress, XSSFBComment comment) { + handler.cell(cellAddress.formatAsString(), null, comment); + } + + private double rkNumber(byte[] data, int offset) { + //see 2.5.122 for this abomination + byte b0 = data[offset]; + String s = Integer.toString(b0, 2); + boolean numDivBy100 = ((b0 & 1) == 1); // else as is + boolean floatingPoint = ((b0 >> 1 & 1) == 0); // else signed integer + + //unset highest 2 bits + b0 &= ~1; + b0 &= ~(1<<1); + + rkBuffer[4] = b0; + for (int i = 1; i < 4; i++) { + rkBuffer[i+4] = data[offset+i]; + } + double d = 0.0; + if (floatingPoint) { + d = LittleEndian.getDouble(rkBuffer); + } else { + d = LittleEndian.getInt(rkBuffer); + } + d = (numDivBy100) ? d/100 : d; + return d; + } + + /** + * You need to implement this to handle the results + * of the sheet parsing. + */ + public interface SheetContentsHandler extends XSSFSheetXMLHandler.SheetContentsHandler { + /** + * A cell, with the given formatted value (may be null), + * a url (may be null), a toolTip (may be null) + * and possibly a comment (may be null), was encountered */ + void hyperlinkCell(String cellReference, String formattedValue, String url, String toolTip, XSSFComment comment); + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBStylesTable.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBStylesTable.java new file mode 100644 index 000000000..8584e9533 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBStylesTable.java @@ -0,0 +1,101 @@ +/* ==================================================================== + 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.xssf.binary; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.poi.POIXMLException; +import org.apache.poi.ss.usermodel.BuiltinFormats; +import org.apache.poi.util.Internal; + +@Internal +public class XSSFBStylesTable extends XSSFBParser { + + private final SortedMap numberFormats = new TreeMap(); + private final List styleIds = new ArrayList(); + + private boolean inCellXFS = false; + private boolean inFmts = false; + public XSSFBStylesTable(InputStream is) throws IOException { + super(is); + parse(); + } + + String getNumberFormatString(int idx) { + if (numberFormats.containsKey(styleIds.get((short)idx))) { + return numberFormats.get(styleIds.get((short)idx)); + } + + return BuiltinFormats.getBuiltinFormat(styleIds.get((short)idx)); + } + + @Override + public void handleRecord(int recordType, byte[] data) throws XSSFBParseException { + XSSFBRecordType type = XSSFBRecordType.lookup(recordType); + switch (type) { + case BrtBeginCellXFs: + inCellXFS = true; + break; + case BrtEndCellXFs: + inCellXFS = false; + break; + case BrtXf: + if (inCellXFS) { + handleBrtXFInCellXF(data); + } + break; + case BrtBeginFmts: + inFmts = true; + break; + case BrtEndFmts: + inFmts = false; + break; + case BrtFmt: + if (inFmts) { + handleFormat(data); + } + break; + + } + } + + private void handleFormat(byte[] data) { + int ifmt = data[0] & 0xFF; + if (ifmt > Short.MAX_VALUE) { + throw new POIXMLException("Format id must be a short"); + } + StringBuilder sb = new StringBuilder(); + XSSFBUtils.readXLWideString(data, 2, sb); + String fmt = sb.toString(); + numberFormats.put((short)ifmt, fmt); + } + + private void handleBrtXFInCellXF(byte[] data) { + int ifmtOffset = 2; + //int ifmtLength = 2; + + //numFmtId in xml terms + int ifmt = data[ifmtOffset] & 0xFF;//the second byte is ignored + styleIds.add((short)ifmt); + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBUtils.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBUtils.java new file mode 100644 index 000000000..e3a46b0f0 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFBUtils.java @@ -0,0 +1,108 @@ +/* ==================================================================== + 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.xssf.binary; + + +import java.nio.charset.Charset; + +import org.apache.poi.POIXMLException; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; + +@Internal +public class XSSFBUtils { + + /** + * Reads an XLNullableWideString. + * @param data data from which to read + * @param offset in data from which to start + * @param sb buffer to which to write. You must setLength(0) before calling! + * @return number of bytes read + * @throws XSSFBParseException if there was an exception during reading + */ + static int readXLNullableWideString(byte[] data, int offset, StringBuilder sb) throws XSSFBParseException { + long numChars = LittleEndian.getUInt(data, offset); + if (numChars < 0) { + throw new XSSFBParseException("too few chars to read"); + } else if (numChars == 0xFFFFFFFFL) { //this means null value (2.5.166), do not read any bytes!!! + return 0; + } else if (numChars > 0xFFFFFFFFL) { + throw new XSSFBParseException("too many chars to read"); + } + + int numBytes = 2*(int)numChars; + offset += 4; + if (offset+numBytes > data.length) { + throw new XSSFBParseException("trying to read beyond data length:" + + "offset="+offset+", numBytes="+numBytes+", data.length="+data.length); + } + sb.append(new String(data, offset, numBytes, Charset.forName("UTF-16LE"))); + numBytes+=4; + return numBytes; + } + + + /** + * Reads an XLNullableWideString. + * @param data data from which to read + * @param offset in data from which to start + * @param sb buffer to which to write. You must setLength(0) before calling! + * @return number of bytes read + * @throws XSSFBParseException if there was an exception while trying to read the string + */ + public static int readXLWideString(byte[] data, int offset, StringBuilder sb) throws XSSFBParseException { + long numChars = LittleEndian.getUInt(data, offset); + if (numChars < 0) { + throw new XSSFBParseException("too few chars to read"); + } else if (numChars > 0xFFFFFFFFL) { + throw new XSSFBParseException("too many chars to read"); + } + int numBytes = 2*(int)numChars; + offset += 4; + if (offset+numBytes > data.length) { + throw new XSSFBParseException("trying to read beyond data length"); + } + sb.append(new String(data, offset, numBytes, Charset.forName("UTF-16LE"))); + numBytes+=4; + return numBytes; + } + + static int castToInt(long val) { + if (val < Integer.MAX_VALUE && val > Integer.MIN_VALUE) { + return (int)val; + } + throw new POIXMLException("val ("+val+") can't be cast to int"); + } + + static short castToShort(int val) { + if (val < Short.MAX_VALUE && val > Short.MIN_VALUE) { + return (short)val; + } + throw new POIXMLException("val ("+val+") can't be cast to short"); + + } + + //TODO: move to LittleEndian? + static int get24BitInt( byte[] data, int offset) { + int i = offset; + int b0 = data[i++] & 0xFF; + int b1 = data[i++] & 0xFF; + int b2 = data[i] & 0xFF; + return ( b2 << 16 ) + ( b1 << 8 ) + b0; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/XSSFHyperlinkRecord.java b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFHyperlinkRecord.java new file mode 100644 index 000000000..a02e8ce92 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/XSSFHyperlinkRecord.java @@ -0,0 +1,117 @@ +/* ==================================================================== + 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.xssf.binary; + +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.util.Internal; + +/** + * This is a read only record that maintains information about + * a hyperlink. In OOXML land, this information has to be merged + * from 1) the sheet's .rels to get the url and 2) from after the + * sheet data in they hyperlink section. + * + * The {@link #display} is often empty and should be filled from + * the contents of the anchor cell. + * + */ +@Internal +public class XSSFHyperlinkRecord { + + private final CellRangeAddress cellRangeAddress; + private final String relId; + private String location; + private String toolTip; + private String display; + + XSSFHyperlinkRecord(CellRangeAddress cellRangeAddress, String relId, String location, String toolTip, String display) { + this.cellRangeAddress = cellRangeAddress; + this.relId = relId; + this.location = location; + this.toolTip = toolTip; + this.display = display; + } + + void setLocation(String location) { + this.location = location; + } + + void setToolTip(String toolTip) { + this.toolTip = toolTip; + } + + void setDisplay(String display) { + this.display = display; + } + + CellRangeAddress getCellRangeAddress() { + return cellRangeAddress; + } + + public String getRelId() { + return relId; + } + + public String getLocation() { + return location; + } + + public String getToolTip() { + return toolTip; + } + + public String getDisplay() { + return display; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + XSSFHyperlinkRecord that = (XSSFHyperlinkRecord) o; + + if (cellRangeAddress != null ? !cellRangeAddress.equals(that.cellRangeAddress) : that.cellRangeAddress != null) + return false; + if (relId != null ? !relId.equals(that.relId) : that.relId != null) return false; + if (location != null ? !location.equals(that.location) : that.location != null) return false; + if (toolTip != null ? !toolTip.equals(that.toolTip) : that.toolTip != null) return false; + return display != null ? display.equals(that.display) : that.display == null; + } + + @Override + public int hashCode() { + int result = cellRangeAddress != null ? cellRangeAddress.hashCode() : 0; + result = 31 * result + (relId != null ? relId.hashCode() : 0); + result = 31 * result + (location != null ? location.hashCode() : 0); + result = 31 * result + (toolTip != null ? toolTip.hashCode() : 0); + result = 31 * result + (display != null ? display.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "XSSFHyperlinkRecord{" + + "cellRangeAddress=" + cellRangeAddress + + ", relId='" + relId + '\'' + + ", location='" + location + '\'' + + ", toolTip='" + toolTip + '\'' + + ", display='" + display + '\'' + + '}'; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/binary/package.html b/src/ooxml/java/org/apache/poi/xssf/binary/package.html new file mode 100644 index 000000000..c7e4a018b --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/binary/package.html @@ -0,0 +1,44 @@ + + + + + + + +

The org.apache.poi.xssf.binary package includes necessary underlying components +for streaming/read-only processing of xlsb files. +

+

+ POI does not yet support opening .xlsb files with XSSFWorkbook, but you can read files with XSSFBReader + in o.a.p.xssf.eventusermodel. +

+

+ This feature was added in poi-3.15-beta3 and should be considered experimental. Most classes + have been marked @Internal and the API is subject to change. +

+

Related Documentation

+ +For overviews, tutorials, examples, guides, and tool documentation, please see: + + + + diff --git a/src/ooxml/java/org/apache/poi/xssf/eventusermodel/XSSFBReader.java b/src/ooxml/java/org/apache/poi/xssf/eventusermodel/XSSFBReader.java new file mode 100644 index 000000000..b8f54cdf5 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/eventusermodel/XSSFBReader.java @@ -0,0 +1,172 @@ +/* ==================================================================== + 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.xssf.eventusermodel; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.exceptions.OpenXML4JException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.openxml4j.opc.PackageRelationshipCollection; +import org.apache.poi.openxml4j.opc.PackagingURIHelper; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.xssf.binary.XSSFBCommentsTable; +import org.apache.poi.xssf.binary.XSSFBParseException; +import org.apache.poi.xssf.binary.XSSFBParser; +import org.apache.poi.xssf.binary.XSSFBRecordType; +import org.apache.poi.xssf.binary.XSSFBRelation; +import org.apache.poi.xssf.binary.XSSFBStylesTable; +import org.apache.poi.xssf.binary.XSSFBUtils; +import org.apache.poi.xssf.model.CommentsTable; +import org.apache.poi.xssf.usermodel.XSSFRelation; + +/** + * Reader for xlsb files. + */ +public class XSSFBReader extends XSSFReader { + /** + * Creates a new XSSFReader, for the given package + * + * @param pkg opc package + */ + public XSSFBReader(OPCPackage pkg) throws IOException, OpenXML4JException { + super(pkg); + } + + /** + * Returns an Iterator which will let you get at all the + * different Sheets in turn. + * Each sheet's InputStream is only opened when fetched + * from the Iterator. It's up to you to close the + * InputStreams when done with each one. + */ + @Override + public Iterator getSheetsData() throws IOException, InvalidFormatException { + return new SheetIterator(workbookPart); + } + + public XSSFBStylesTable getXSSFBStylesTable() throws IOException { + ArrayList parts = pkg.getPartsByContentType(XSSFBRelation.STYLES_BINARY.getContentType()); + if(parts.size() == 0) return null; + + // Create the Styles Table, and associate the Themes if present + return new XSSFBStylesTable(parts.get(0).getInputStream()); + + } + + + public static class SheetIterator extends XSSFReader.SheetIterator { + + /** + * Construct a new SheetIterator + * + * @param wb package part holding workbook.xml + */ + private SheetIterator(PackagePart wb) throws IOException { + super(wb); + } + + Iterator createSheetIteratorFromWB(PackagePart wb) throws IOException { + SheetRefLoader sheetRefLoader = new SheetRefLoader(wb.getInputStream()); + sheetRefLoader.parse(); + return sheetRefLoader.getSheets().iterator(); + } + + /** + * Not supported by XSSFBReader's SheetIterator. + * Please use {@link #getXSSFBSheetComments()} instead. + * @return nothing, always throws IllegalArgumentException! + */ + @Override + public CommentsTable getSheetComments() { + throw new IllegalArgumentException("Please use getXSSFBSheetComments"); + } + + public XSSFBCommentsTable getXSSFBSheetComments() { + PackagePart sheetPkg = getSheetPart(); + + // Do we have a comments relationship? (Only ever one if so) + try { + PackageRelationshipCollection commentsList = + sheetPkg.getRelationshipsByType(XSSFRelation.SHEET_COMMENTS.getRelation()); + if (commentsList.size() > 0) { + PackageRelationship comments = commentsList.getRelationship(0); + if (comments == null || comments.getTargetURI() == null) { + return null; + } + PackagePartName commentsName = PackagingURIHelper.createPartName(comments.getTargetURI()); + PackagePart commentsPart = sheetPkg.getPackage().getPart(commentsName); + return new XSSFBCommentsTable(commentsPart.getInputStream()); + } + } catch (InvalidFormatException e) { + return null; + } catch (IOException e) { + return null; + } + return null; + } + + } + + private static class SheetRefLoader extends XSSFBParser { + List sheets = new LinkedList(); + + private SheetRefLoader(InputStream is) { + super(is); + } + + @Override + public void handleRecord(int recordType, byte[] data) throws XSSFBParseException { + if (recordType == XSSFBRecordType.BrtBundleSh.getId()) { + addWorksheet(data); + } + } + + private void addWorksheet(byte[] data) { + int offset = 0; + //this is the sheet state #2.5.142 + long hsShtat = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + + long iTabID = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + //according to #2.4.304 + if (iTabID < 1 || iTabID > 0x0000FFFFL) { + throw new XSSFBParseException("table id out of range: "+iTabID); + } + StringBuilder sb = new StringBuilder(); + offset += XSSFBUtils.readXLWideString(data, offset, sb); + String relId = sb.toString(); + sb.setLength(0); + XSSFBUtils.readXLWideString(data, offset, sb); + String name = sb.toString(); + if (relId != null && relId.trim().length() > 0) { + sheets.add(new XSSFSheetRef(relId, name)); + } + } + + List getSheets() { + return sheets; + } + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xssf/eventusermodel/XSSFReader.java b/src/ooxml/java/org/apache/poi/xssf/eventusermodel/XSSFReader.java index e5c9cb25b..5b43c2010 100644 --- a/src/ooxml/java/org/apache/poi/xssf/eventusermodel/XSSFReader.java +++ b/src/ooxml/java/org/apache/poi/xssf/eventusermodel/XSSFReader.java @@ -16,15 +16,16 @@ ==================================================================== */ package org.apache.poi.xssf.eventusermodel; -import static org.apache.poi.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; - +import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import org.apache.poi.POIXMLException; @@ -39,6 +40,7 @@ import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; import org.apache.poi.openxml4j.opc.PackagingURIHelper; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; +import org.apache.poi.util.SAXHelper; import org.apache.poi.xssf.model.CommentsTable; import org.apache.poi.xssf.model.SharedStringsTable; import org.apache.poi.xssf.model.StylesTable; @@ -47,9 +49,11 @@ import org.apache.poi.xssf.usermodel.XSSFDrawing; import org.apache.poi.xssf.usermodel.XSSFRelation; import org.apache.poi.xssf.usermodel.XSSFShape; import org.apache.xmlbeans.XmlException; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSheet; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbook; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.WorkbookDocument; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; /** * This class makes it easy to get at individual parts @@ -62,8 +66,8 @@ public class XSSFReader { private static final POILogger LOGGER = POILogFactory.getLogger(XSSFReader.class); - private OPCPackage pkg; - private PackagePart workbookPart; + protected OPCPackage pkg; + protected PackagePart workbookPart; /** * Creates a new XSSFReader, for the given package @@ -194,23 +198,23 @@ public class XSSFReader { private final Map sheetMap; /** - * Current CTSheet bean + * Current sheet reference */ - private CTSheet ctSheet; - + XSSFSheetRef xssfSheetRef; + /** * Iterator over CTSheet objects, returns sheets in logical order. * We can't rely on the Ooxml4J's relationship iterator because it returns objects in physical order, * i.e. as they are stored in the underlying package */ - private final Iterator sheetIterator; + final Iterator sheetIterator; /** * Construct a new SheetIterator * * @param wb package part holding workbook.xml */ - private SheetIterator(PackagePart wb) throws IOException { + SheetIterator(PackagePart wb) throws IOException { /** * The order of sheets is defined by the order of CTSheet elements in workbook.xml @@ -228,25 +232,44 @@ public class XSSFReader { sheetMap.put(rel.getId(), pkg.getPart(relName)); } } - //step 2. Read array of CTSheet elements, wrap it in a ArayList and construct an iterator - //Note, using XMLBeans might be expensive, consider refactoring to use SAX or a plain regexp search - CTWorkbook wbBean = WorkbookDocument.Factory.parse(wb.getInputStream(), DEFAULT_XML_OPTIONS).getWorkbook(); - List validSheets = new ArrayList(); - for (CTSheet ctSheet : wbBean.getSheets().getSheetList()) { - //if there's no relationship id, silently skip the sheet - String sheetId = ctSheet.getId(); - if (sheetId != null && sheetId.length() > 0) { - validSheets.add(ctSheet); - } - } - sheetIterator = validSheets.iterator(); + //step 2. Read array of CTSheet elements, wrap it in a LinkedList + //and construct an iterator + sheetIterator = createSheetIteratorFromWB(wb); } catch (InvalidFormatException e){ throw new POIXMLException(e); - } catch (XmlException e){ - throw new POIXMLException(e); } } + Iterator createSheetIteratorFromWB(PackagePart wb) throws IOException { + + XMLSheetRefReader xmlSheetRefReader = new XMLSheetRefReader(); + XMLReader xmlReader = null; + try { + xmlReader = SAXHelper.newXMLReader(); + } catch (ParserConfigurationException e) { + throw new POIXMLException(e); + } catch (SAXException e) { + throw new POIXMLException(e); + } + xmlReader.setContentHandler(xmlSheetRefReader); + try { + xmlReader.parse(new InputSource(wb.getInputStream())); + } catch (SAXException e) { + throw new POIXMLException(e); + } + + List validSheets = new ArrayList(); + for (XSSFSheetRef xssfSheetRef : xmlSheetRefReader.getSheetRefs()) { + //if there's no relationship id, silently skip the sheet + String sheetId = xssfSheetRef.getId(); + if (sheetId != null && sheetId.length() > 0) { + validSheets.add(xssfSheetRef); + } + } + return validSheets.iterator(); + } + + /** * Returns true if the iteration has more elements. * @@ -264,9 +287,9 @@ public class XSSFReader { */ @Override public InputStream next() { - ctSheet = sheetIterator.next(); + xssfSheetRef = sheetIterator.next(); - String sheetId = ctSheet.getId(); + String sheetId = xssfSheetRef.getId(); try { PackagePart sheetPkg = sheetMap.get(sheetId); return sheetPkg.getInputStream(); @@ -281,7 +304,7 @@ public class XSSFReader { * @return name of the current sheet */ public String getSheetName() { - return ctSheet.getName(); + return xssfSheetRef.getName(); } /** @@ -344,7 +367,7 @@ public class XSSFReader { } public PackagePart getSheetPart() { - String sheetId = ctSheet.getId(); + String sheetId = xssfSheetRef.getId(); return sheetMap.get(sheetId); } @@ -356,4 +379,52 @@ public class XSSFReader { throw new IllegalStateException("Not supported"); } } + + protected final static class XSSFSheetRef { + //do we need to store sheetId, too? + private final String id; + private final String name; + + public XSSFSheetRef(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + } + + //scrapes sheet reference info and order from workbook.xml + private static class XMLSheetRefReader extends DefaultHandler { + private final static String SHEET = "sheet"; + private final static String ID = "id"; + private final static String NAME = "name"; + + private final List sheetRefs = new LinkedList(); + + @Override + public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException { + if (localName.toLowerCase(Locale.US).equals(SHEET)) { + String name = null; + String id = null; + for (int i = 0; i < attrs.getLength(); i++) { + if (attrs.getLocalName(i).toLowerCase(Locale.US).equals(NAME)) { + name = attrs.getValue(i); + } else if (attrs.getLocalName(i).toLowerCase(Locale.US).equals(ID)) { + id = attrs.getValue(i); + } + sheetRefs.add(new XSSFSheetRef(id, name)); + } + } + } + + List getSheetRefs() { + return Collections.unmodifiableList(sheetRefs); + } + } } diff --git a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFBEventBasedExcelExtractor.java b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFBEventBasedExcelExtractor.java new file mode 100644 index 000000000..b3e667e4a --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFBEventBasedExcelExtractor.java @@ -0,0 +1,160 @@ +/* ==================================================================== + 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.xssf.extractor; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.poi.POIXMLTextExtractor; +import org.apache.poi.openxml4j.exceptions.OpenXML4JException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.xssf.binary.XSSFBCommentsTable; +import org.apache.poi.xssf.binary.XSSFBHyperlinksTable; +import org.apache.poi.xssf.binary.XSSFBSharedStringsTable; +import org.apache.poi.xssf.binary.XSSFBSheetHandler; +import org.apache.poi.xssf.binary.XSSFBStylesTable; +import org.apache.poi.xssf.eventusermodel.XSSFBReader; +import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler; +import org.apache.poi.xssf.usermodel.XSSFRelation; +import org.apache.xmlbeans.XmlException; +import org.xml.sax.SAXException; + +/** + * Implementation of a text extractor or xlsb Excel + * files that uses SAX-like binary parsing. + */ +public class XSSFBEventBasedExcelExtractor extends XSSFEventBasedExcelExtractor + implements org.apache.poi.ss.extractor.ExcelExtractor { + + public static final XSSFRelation[] SUPPORTED_TYPES = new XSSFRelation[] { + XSSFRelation.XLSB_BINARY_WORKBOOK + }; + + private boolean handleHyperlinksInCells = false; + + public XSSFBEventBasedExcelExtractor(String path) throws XmlException, OpenXML4JException, IOException { + super(path); + } + + public XSSFBEventBasedExcelExtractor(OPCPackage container) throws XmlException, OpenXML4JException, IOException { + super(container); + } + + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.err.println("Use:"); + System.err.println(" XSSFBEventBasedExcelExtractor "); + System.exit(1); + } + POIXMLTextExtractor extractor = + new XSSFBEventBasedExcelExtractor(args[0]); + System.out.println(extractor.getText()); + extractor.close(); + } + + public void setHandleHyperlinksInCells(boolean handleHyperlinksInCells) { + this.handleHyperlinksInCells = handleHyperlinksInCells; + } + + /** + * Should we return the formula itself, and not + * the result it produces? Default is false + * This is currently unsupported for xssfb + */ + @Override + public void setFormulasNotResults(boolean formulasNotResults) { + throw new IllegalArgumentException("Not currently supported"); + } + + /** + * Processes the given sheet + */ + public void processSheet( + SheetContentsHandler sheetContentsExtractor, + XSSFBStylesTable styles, + XSSFBCommentsTable comments, + XSSFBSharedStringsTable strings, + InputStream sheetInputStream) + throws IOException, SAXException { + + DataFormatter formatter; + if (locale == null) { + formatter = new DataFormatter(); + } else { + formatter = new DataFormatter(locale); + } + + XSSFBSheetHandler xssfbSheetHandler = new XSSFBSheetHandler( + sheetInputStream, + styles, comments, strings, sheetContentsExtractor, formatter, formulasNotResults + ); + xssfbSheetHandler.parse(); + } + + /** + * Processes the file and returns the text + */ + public String getText() { + try { + XSSFBSharedStringsTable strings = new XSSFBSharedStringsTable(container); + XSSFBReader xssfbReader = new XSSFBReader(container); + XSSFBStylesTable styles = xssfbReader.getXSSFBStylesTable(); + XSSFBReader.SheetIterator iter = (XSSFBReader.SheetIterator) xssfbReader.getSheetsData(); + + StringBuffer text = new StringBuffer(); + SheetTextExtractor sheetExtractor = new SheetTextExtractor(); + XSSFBHyperlinksTable hyperlinksTable = null; + while (iter.hasNext()) { + InputStream stream = iter.next(); + if (includeSheetNames) { + text.append(iter.getSheetName()); + text.append('\n'); + } + if (handleHyperlinksInCells) { + hyperlinksTable = new XSSFBHyperlinksTable(iter.getSheetPart()); + } + XSSFBCommentsTable comments = includeCellComments ? iter.getXSSFBSheetComments() : null; + processSheet(sheetExtractor, styles, comments, strings, stream); + if (includeHeadersFooters) { + sheetExtractor.appendHeaderText(text); + } + sheetExtractor.appendCellText(text); + if (includeTextBoxes) { + processShapes(iter.getShapes(), text); + } + if (includeHeadersFooters) { + sheetExtractor.appendFooterText(text); + } + sheetExtractor.reset(); + stream.close(); + } + + return text.toString(); + } catch (IOException e) { + System.err.println(e); + return null; + } catch (SAXException se) { + System.err.println(se); + return null; + } catch (OpenXML4JException o4je) { + System.err.println(o4je); + return null; + } + } + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFEventBasedExcelExtractor.java b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFEventBasedExcelExtractor.java index e49c11c2e..2cfa099d9 100644 --- a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFEventBasedExcelExtractor.java +++ b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFEventBasedExcelExtractor.java @@ -54,15 +54,15 @@ import org.xml.sax.XMLReader; */ public class XSSFEventBasedExcelExtractor extends POIXMLTextExtractor implements org.apache.poi.ss.extractor.ExcelExtractor { - private OPCPackage container; + OPCPackage container; private POIXMLProperties properties; - private Locale locale; - private boolean includeTextBoxes = true; - private boolean includeSheetNames = true; - private boolean includeCellComments = false; - private boolean includeHeadersFooters = true; - private boolean formulasNotResults = false; + Locale locale; + boolean includeTextBoxes = true; + boolean includeSheetNames = true; + boolean includeCellComments = false; + boolean includeHeadersFooters = true; + boolean formulasNotResults = false; private boolean concatenatePhoneticRuns = true; public XSSFEventBasedExcelExtractor(String path) throws XmlException, OpenXML4JException, IOException { @@ -240,7 +240,7 @@ public class XSSFEventBasedExcelExtractor extends POIXMLTextExtractor } } - private void processShapes(List shapes, StringBuffer text) { + void processShapes(List shapes, StringBuffer text) { if (shapes == null){ return; } @@ -349,7 +349,7 @@ public class XSSFEventBasedExcelExtractor extends POIXMLTextExtractor * @see XSSFExcelExtractor#getText() * @see org.apache.poi.hssf.extractor.ExcelExtractor#_extractHeaderFooter(org.apache.poi.ss.usermodel.HeaderFooter) */ - private void appendHeaderText(StringBuffer buffer) { + void appendHeaderText(StringBuffer buffer) { appendHeaderFooterText(buffer, "firstHeader"); appendHeaderFooterText(buffer, "oddHeader"); appendHeaderFooterText(buffer, "evenHeader"); @@ -361,7 +361,7 @@ public class XSSFEventBasedExcelExtractor extends POIXMLTextExtractor * @see XSSFExcelExtractor#getText() * @see org.apache.poi.hssf.extractor.ExcelExtractor#_extractHeaderFooter(org.apache.poi.ss.usermodel.HeaderFooter) */ - private void appendFooterText(StringBuffer buffer) { + void appendFooterText(StringBuffer buffer) { // append the text for each footer type in the same order // they are appended in XSSFExcelExtractor appendHeaderFooterText(buffer, "firstFooter"); @@ -372,7 +372,7 @@ public class XSSFEventBasedExcelExtractor extends POIXMLTextExtractor /** * Append the cell contents we have collected. */ - private void appendCellText(StringBuffer buffer) { + void appendCellText(StringBuffer buffer) { checkMaxTextSize(buffer, output.toString()); buffer.append(output); } @@ -380,7 +380,7 @@ public class XSSFEventBasedExcelExtractor extends POIXMLTextExtractor /** * Reset this SheetTextExtractor for the next sheet. */ - private void reset() { + void reset() { output.setLength(0); firstCellOfRow = true; if (headerFooterMap != null) { diff --git a/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java b/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java index 0d7bc5a8a..8405447c0 100644 --- a/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java +++ b/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java @@ -68,6 +68,7 @@ public class TestExtractorFactory { private static File xlsxStrict; private static File xltx; private static File xlsEmb; + private static File xlsb; private static File doc; private static File doc6; @@ -108,6 +109,7 @@ public class TestExtractorFactory { xlsxStrict = getFileAndCheck(ssTests, "SampleSS.strict.xlsx"); xltx = getFileAndCheck(ssTests, "test.xltx"); xlsEmb = getFileAndCheck(ssTests, "excel_with_embeded.xls"); + xlsb = getFileAndCheck(ssTests, "testVarious.xlsb"); POIDataSamples wpTests = POIDataSamples.getDocumentInstance(); doc = getFileAndCheck(wpTests, "SampleDoc.doc"); @@ -172,6 +174,13 @@ public class TestExtractorFactory { ); extractor.close(); + extractor = ExtractorFactory.createExtractor(xlsb); + assertTrue( + extractor.getText().contains("test") + ); + extractor.close(); + + extractor = ExtractorFactory.createExtractor(xltx); assertTrue( extractor.getText().contains("test") diff --git a/src/ooxml/testcases/org/apache/poi/xssf/binary/TestXSSFBSharedStringsTable.java b/src/ooxml/testcases/org/apache/poi/xssf/binary/TestXSSFBSharedStringsTable.java new file mode 100644 index 000000000..7bf1cf391 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/binary/TestXSSFBSharedStringsTable.java @@ -0,0 +1,56 @@ +/* ==================================================================== + 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.xssf.binary; + +import static org.junit.Assert.assertEquals; + +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.junit.Test; + +public class TestXSSFBSharedStringsTable { + + + private static POIDataSamples _ssTests = POIDataSamples.getSpreadSheetInstance(); + + @Test + public void testBasic() throws Exception { + + OPCPackage pkg = OPCPackage.open(_ssTests.openResourceAsStream("51519.xlsb")); + List parts = pkg.getPartsByName(Pattern.compile("/xl/sharedStrings.bin")); + assertEquals(1, parts.size()); + + XSSFBSharedStringsTable rtbl = new XSSFBSharedStringsTable(parts.get(0)); + List strings = rtbl.getItems(); + assertEquals(49, strings.size()); + + assertEquals("\u30B3\u30E1\u30F3\u30C8", rtbl.getEntryAt(0)); + assertEquals("\u65E5\u672C\u30AA\u30E9\u30AF\u30EB", rtbl.getEntryAt(3)); + assertEquals(55, rtbl.getCount()); + assertEquals(49, rtbl.getUniqueCount()); + + //TODO: add in tests for phonetic runs + + } + + +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/binary/TestXSSFBSheetHyperlinkManager.java b/src/ooxml/testcases/org/apache/poi/xssf/binary/TestXSSFBSheetHyperlinkManager.java new file mode 100644 index 000000000..992517dfc --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/binary/TestXSSFBSheetHyperlinkManager.java @@ -0,0 +1,54 @@ +/* ==================================================================== + 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.xssf.binary; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.List; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.ss.util.CellAddress; +import org.apache.poi.xssf.eventusermodel.XSSFBReader; +import org.apache.poi.xssf.eventusermodel.XSSFReader; +import org.junit.Test; + +public class TestXSSFBSheetHyperlinkManager { + + private static POIDataSamples _ssTests = POIDataSamples.getSpreadSheetInstance(); + + @Test + public void testBasic() throws Exception { + + OPCPackage pkg = OPCPackage.open(_ssTests.openResourceAsStream("hyperlink.xlsb")); + XSSFBReader reader = new XSSFBReader(pkg); + XSSFReader.SheetIterator it = (XSSFReader.SheetIterator) reader.getSheetsData(); + it.next(); + XSSFBHyperlinksTable manager = new XSSFBHyperlinksTable(it.getSheetPart()); + List records = manager.getHyperLinks().get(new CellAddress(0, 0)); + assertNotNull(records); + assertEquals(1, records.size()); + XSSFHyperlinkRecord record = records.get(0); + assertEquals("http://tika.apache.org/", record.getLocation()); + assertEquals("rId2", record.getRelId()); + + } + + +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/eventusermodel/TestXSSFBReader.java b/src/ooxml/testcases/org/apache/poi/xssf/eventusermodel/TestXSSFBReader.java new file mode 100644 index 000000000..57e1e836c --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/eventusermodel/TestXSSFBReader.java @@ -0,0 +1,224 @@ +/* ==================================================================== + 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.xssf.eventusermodel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.xssf.binary.XSSFBSharedStringsTable; +import org.apache.poi.xssf.binary.XSSFBSheetHandler; +import org.apache.poi.xssf.binary.XSSFBStylesTable; +import org.apache.poi.xssf.usermodel.XSSFComment; +import org.junit.Test; + +public class TestXSSFBReader { + + private static POIDataSamples _ssTests = POIDataSamples.getSpreadSheetInstance(); + + @Test + public void testBasic() throws Exception { + List sheetTexts = getSheets("testVarious.xlsb"); + + assertEquals(1, sheetTexts.size()); + String xsxml = sheetTexts.get(0); + assertContains("This is a string", xsxml); + assertContains("13", xsxml); + assertContains("13.12112313", xsxml); + assertContains("$ 3.03", xsxml); + assertContains("20%", xsxml); + assertContains("13.12", xsxml); + assertContains("1.23457E+14", xsxml); + assertContains("1.23457E+15", xsxml); + + assertContains("46/1963", xsxml);//custom format 1 + assertContains("3/128", xsxml);//custom format 2 + + assertContains("\n" + + "\tlonger int\n" + + "\t1.23457E+15\n" + + "\tAllison, Timothy B.:\n" + + "test comment2\n" + + "", xsxml); + + assertContains("\n" + + "\tcomment6Allison, Timothy B.:\n" + + "comment6 actually in cell\n" + + "", xsxml); + + assertContains("\n" + + "\tAllison, Timothy B.:\n" + + "comment7 end of file\n" + + "", xsxml); + + assertContains("\n" + + "\tAllison, Timothy B.:\n" + + "comment8 end of file\n" + + "", xsxml); + + assertContains("
OddLeftHeader OddCenterHeader OddRightHeader
", xsxml); + assertContains("
OddLeftFooter OddCenterFooter OddRightFooter
", xsxml); + assertContains( + "
EvenLeftHeader EvenCenterHeader EvenRightHeader\n
", + xsxml); + assertContains( + "
EvenLeftFooter EvenCenterFooter EvenRightFooter
", + xsxml); + assertContains( + "
FirstPageLeftHeader FirstPageCenterHeader FirstPageRightHeader
", + xsxml); + assertContains( + "
FirstPageLeftFooter FirstPageCenterFooter FirstPageRightFooter
", + xsxml); + + } + + @Test + public void testComments() throws Exception { + List sheetTexts = getSheets("comments.xlsb"); + String xsxml = sheetTexts.get(0); + assertContains( + "\n" + + "\tcomment top row1 (index0)\n" + + "\trow1\n" + + "", xsxml); + assertContains( + "\n" + + "\tAllison, Timothy B.:\n" + + "comment row2 (index1)\n" + + "", + xsxml); + assertContains("\n" + + "\trow3comment top row3 (index2)\n" + + "\trow3\n", xsxml); + + assertContains("\n" + + "\tcomment top row4 (index3)\n" + + "\trow4\n" + + "", xsxml); + + } + + private List getSheets(String testFileName) throws Exception { + OPCPackage pkg = OPCPackage.open(_ssTests.openResourceAsStream(testFileName)); + List sheetTexts = new ArrayList(); + XSSFBReader r = new XSSFBReader(pkg); + +// assertNotNull(r.getWorkbookData()); + // assertNotNull(r.getSharedStringsData()); + assertNotNull(r.getXSSFBStylesTable()); + XSSFBSharedStringsTable sst = new XSSFBSharedStringsTable(pkg); + XSSFBStylesTable xssfbStylesTable = r.getXSSFBStylesTable(); + XSSFBReader.SheetIterator it = (XSSFBReader.SheetIterator)r.getSheetsData(); + + while (it.hasNext()) { + InputStream is = it.next(); + String name = it.getSheetName(); + TestSheetHandler testSheetHandler = new TestSheetHandler(); + testSheetHandler.startSheet(name); + XSSFBSheetHandler sheetHandler = new XSSFBSheetHandler(is, + xssfbStylesTable, + it.getXSSFBSheetComments(), + sst, testSheetHandler, + new DataFormatter(), + false); + sheetHandler.parse(); + testSheetHandler.endSheet(); + sheetTexts.add(testSheetHandler.toString()); + } + return sheetTexts; + + } + + //This converts all [\r\n\t]+ to " " + private void assertContains(String needle, String haystack) { + needle = needle.replaceAll("[\r\n\t]+", " "); + haystack = haystack.replaceAll("[\r\n\t]+", " "); + if (haystack.indexOf(needle) < 0) { + fail("couldn't find >"+needle+"< in: "+haystack ); + } + } + + + @Test + public void testDate() throws Exception { + List sheets = getSheets("date.xlsb"); + assertEquals(1, sheets.size()); + assertContains("1/12/13", sheets.get(0)); + + } + + + private class TestSheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler { + private final StringBuilder sb = new StringBuilder(); + + public void startSheet(String sheetName) { + sb.append(""); + } + + public void endSheet(){ + sb.append(""); + } + @Override + public void startRow(int rowNum) { + sb.append("\n"); + } + + @Override + public void endRow(int rowNum) { + sb.append("\n"); + } + + @Override + public void cell(String cellReference, String formattedValue, XSSFComment comment) { + formattedValue = (formattedValue == null) ? "" : formattedValue; + if (comment == null) { + sb.append("\n\t").append(formattedValue).append(""); + } else { + sb.append("\n\t") + .append(formattedValue) + .append("") + .append(comment.getString().toString().trim()).append("") + .append(""); + } + } + + @Override + public void headerFooter(String text, boolean isHeader, String tagName) { + if (isHeader) { + sb.append("
"+text+"
"); + } else { + sb.append("
"+text+"
"); + + } + } + + @Override + public String toString() { + return sb.toString(); + } + } +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/extractor/TestXSSFBEventBasedExcelExtractor.java b/src/ooxml/testcases/org/apache/poi/xssf/extractor/TestXSSFBEventBasedExcelExtractor.java new file mode 100644 index 000000000..da38882ab --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/extractor/TestXSSFBEventBasedExcelExtractor.java @@ -0,0 +1,102 @@ +/* ==================================================================== + 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.xssf.extractor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.apache.poi.xssf.XSSFTestDataSamples; +import org.junit.Test; + +/** + * Tests for {@link XSSFBEventBasedExcelExtractor} + */ +public class TestXSSFBEventBasedExcelExtractor { + + + protected XSSFEventBasedExcelExtractor getExtractor(String sampleName) throws Exception { + return new XSSFBEventBasedExcelExtractor(XSSFTestDataSamples. + openSamplePackage(sampleName)); + } + + /** + * Get text out of the simple file + */ + @Test + public void testGetSimpleText() throws Exception { + // a very simple file + XSSFEventBasedExcelExtractor extractor = getExtractor("sample.xlsb"); + extractor.setIncludeCellComments(true); + extractor.getText(); + + String text = extractor.getText(); + assertTrue(text.length() > 0); + + // Check sheet names + assertTrue(text.startsWith("Sheet1")); + assertTrue(text.endsWith("Sheet3\n")); + + // Now without, will have text + extractor.setIncludeSheetNames(false); + text = extractor.getText(); + String CHUNK1 = + "Lorem\t111\n" + + "ipsum\t222\n" + + "dolor\t333\n" + + "sit\t444\n" + + "amet\t555\n" + + "consectetuer\t666\n" + + "adipiscing\t777\n" + + "elit\t888\n" + + "Nunc\t999\n"; + String CHUNK2 = + "The quick brown fox jumps over the lazy dog\n" + + "hello, xssf hello, xssf\n" + + "hello, xssf hello, xssf\n" + + "hello, xssf hello, xssf\n" + + "hello, xssf hello, xssf\n"; + assertEquals( + CHUNK1 + + "at\t4995\n" + + CHUNK2 + , text); + + } + + + /** + * Test text extraction from text box using getShapes() + * + * @throws Exception + */ + @Test + public void testShapes() throws Exception { + XSSFEventBasedExcelExtractor ooxmlExtractor = getExtractor("WithTextBox.xlsb"); + + try { + String text = ooxmlExtractor.getText(); + + assertTrue(text.indexOf("Line 1") > -1); + assertTrue(text.indexOf("Line 2") > -1); + assertTrue(text.indexOf("Line 3") > -1); + } finally { + ooxmlExtractor.close(); + } + } + +} diff --git a/test-data/spreadsheet/51519.xlsb b/test-data/spreadsheet/51519.xlsb new file mode 100644 index 0000000000000000000000000000000000000000..54876cdb9cc1165e35d1cb45f11c240a15911f71 GIT binary patch literal 10897 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 b{sq=wVSofnAOHX!>~9@5Ak2VC00933%&yZ* literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/WithTextBox.xlsb b/test-data/spreadsheet/WithTextBox.xlsb new file mode 100644 index 0000000000000000000000000000000000000000..558395dc13b6c6b160dae89c55be6408986504f1 GIT binary patch literal 10076 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 E0U2yW9{>OV literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/comments.xlsb b/test-data/spreadsheet/comments.xlsb new file mode 100644 index 0000000000000000000000000000000000000000..881e51c8679ec25cbd8ad84f5123d4d4a95b713e GIT binary patch literal 10796 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 MK+^DHhy(!eKd66yhX4Qo literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/date.xlsb b/test-data/spreadsheet/date.xlsb new file mode 100644 index 0000000000000000000000000000000000000000..d47d602940b4c6eaf6d3e2bc05714478e73cf90c GIT binary patch literal 7566 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 nBb$^6f?7OqBs4V-VAHz2@AY(0<({&x3&sgaw; literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/hyperlink.xlsb b/test-data/spreadsheet/hyperlink.xlsb new file mode 100644 index 0000000000000000000000000000000000000000..2a089368234b6993b08beaabe0ca4f9dd990867c GIT binary patch literal 7882 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 a`NLbQE1@Aw4*Hh#`!yaD% literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/sample.xlsb b/test-data/spreadsheet/sample.xlsb new file mode 100644 index 0000000000000000000000000000000000000000..676b4da2d69568477bf698b1163f3194a54a3682 GIT binary patch literal 10843 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 GcmD(M(iZsu literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/testVarious.xlsb b/test-data/spreadsheet/testVarious.xlsb new file mode 100644 index 0000000000000000000000000000000000000000..22cc9b41d3353d37ee709b53da970f41a99cd50d GIT binary patch literal 22715 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 Y&ydB+d