From f06c45421b1904f196f6aa4c2f99989a1f3185c6 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sun, 28 Jun 2020 11:57:37 +0000 Subject: [PATCH] [github-184] New EmittingSXSSFWorkbook. Thanks to mobreza. This closes #184 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1879302 13f79535-47bb-0310-9956-ffa450edef68 --- .../xssf/streaming/EmittingSXSSFSheet.java | 69 +++ .../xssf/streaming/EmittingSXSSFWorkbook.java | 219 ++++++++ .../poi/xssf/streaming/IRowGenerator.java | 37 ++ .../xssf/streaming/SXSSFFormulaEvaluator.java | 12 +- .../poi/xssf/streaming/SXSSFPicture.java | 4 +- .../apache/poi/xssf/streaming/SXSSFSheet.java | 211 ++++---- .../poi/xssf/streaming/SXSSFWorkbook.java | 220 ++++---- .../poi/xssf/streaming/SheetDataWriter.java | 7 +- .../xssf/streaming/StreamingSheetWriter.java | 84 +++ .../streaming/TestEmittingSXSSFWorkbook.java | 488 ++++++++++++++++++ 10 files changed, 1138 insertions(+), 213 deletions(-) create mode 100644 src/ooxml/java/org/apache/poi/xssf/streaming/EmittingSXSSFSheet.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/streaming/EmittingSXSSFWorkbook.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/streaming/IRowGenerator.java create mode 100644 src/ooxml/java/org/apache/poi/xssf/streaming/StreamingSheetWriter.java create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/streaming/TestEmittingSXSSFWorkbook.java diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/EmittingSXSSFSheet.java b/src/ooxml/java/org/apache/poi/xssf/streaming/EmittingSXSSFSheet.java new file mode 100644 index 00000000000..c0b49d5d7a3 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/EmittingSXSSFSheet.java @@ -0,0 +1,69 @@ +/* ==================================================================== + 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.streaming; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.poi.util.Beta; +import org.apache.poi.xssf.usermodel.XSSFSheet; + +/** + * A variant of SXSSFSheet that uses IRowGenerator to create rows. + * + * This variant is experimental and APIs may change at short notice. + * + * @see EmittingSXSSFWorkbook + * @since 5.0.0 + */ +@Beta +public class EmittingSXSSFSheet extends SXSSFSheet { + private IRowGenerator rowGenerator; + + public EmittingSXSSFSheet(EmittingSXSSFWorkbook workbook, XSSFSheet xSheet) throws IOException { + super(workbook, xSheet, workbook.getRandomAccessWindowSize()); + } + + @Override + public InputStream getWorksheetXMLInputStream() throws IOException { + throw new RuntimeException("Not supported by EmittingSXSSFSheet"); + } + + public void setRowGenerator(IRowGenerator rowGenerator) { + this.rowGenerator = rowGenerator; + } + + public void writeRows(OutputStream out) throws IOException { + // delayed creation of SheetDataWriter + _writer = ((EmittingSXSSFWorkbook) _workbook).createSheetDataWriter(out); + try { + if (this.rowGenerator != null) { + this.rowGenerator.generateRows(this); + } + } catch (Exception e) { + throw new IOException("Error generating Excel rows", e); + } finally { + // flush buffered rows + flushRows(0); + // flush writer buffer + _writer.close(); + out.flush(); + } + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/EmittingSXSSFWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/streaming/EmittingSXSSFWorkbook.java new file mode 100644 index 00000000000..fb2d28a6ccd --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/EmittingSXSSFWorkbook.java @@ -0,0 +1,219 @@ +/* ==================================================================== + 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.streaming; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.util.Beta; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +/** + * An variant of SXSSFWorkbook that avoids generating a temporary file and writes data directly to + * the provided OutputStream. + * + * This variant is experimental and APIs may change at short notice. + * + * @since 5.0.0 + */ +@Beta +public class EmittingSXSSFWorkbook extends SXSSFWorkbook { + private static final POILogger logger = POILogFactory.getLogger(EmittingSXSSFWorkbook.class); + + public EmittingSXSSFWorkbook() { + this(null); + } + + public EmittingSXSSFWorkbook(XSSFWorkbook workbook) { + this(workbook, SXSSFWorkbook.DEFAULT_WINDOW_SIZE); + } + + public EmittingSXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize) { + super(workbook, rowAccessWindowSize, false, false); + } + + @Override + protected SheetDataWriter createSheetDataWriter() throws IOException { + throw new RuntimeException("Not supported by EmittingSXSSFWorkbook"); + } + + protected StreamingSheetWriter createSheetDataWriter(OutputStream out) throws IOException { + return new StreamingSheetWriter(out); + } + + @Override + protected ISheetInjector createSheetInjector(SXSSFSheet sxSheet) throws IOException { + EmittingSXSSFSheet ssxSheet = (EmittingSXSSFSheet) sxSheet; + return (output) -> { + ssxSheet.writeRows(output); + }; + } + + @Override + SXSSFSheet createAndRegisterSXSSFSheet(XSSFSheet xSheet) { + final EmittingSXSSFSheet sxSheet; + try { + sxSheet = new EmittingSXSSFSheet(this, xSheet); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + registerSheetMapping(sxSheet, xSheet); + return sxSheet; + } + + public EmittingSXSSFSheet createSheet() { + return (EmittingSXSSFSheet) super.createSheet(); + } + + public EmittingSXSSFSheet createSheet(String sheetname) { + return (EmittingSXSSFSheet) super.createSheet(sheetname); + } + + /** + * Returns an iterator of the sheets in the workbook in sheet order. Includes hidden and very hidden sheets. + * + * @return an iterator of the sheets. + */ + @Override + public Iterator sheetIterator() { + return new SheetIterator(); + } + + private final class SheetIterator implements Iterator { + final private Iterator it; + + @SuppressWarnings("unchecked") + public SheetIterator() { + it = (Iterator) (Iterator) _wb.iterator(); + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + @SuppressWarnings("unchecked") + public T next() throws NoSuchElementException { + final XSSFSheet xssfSheet = it.next(); + EmittingSXSSFSheet sxSheet = (EmittingSXSSFSheet) getSXSSFSheet(xssfSheet); + return (T) (sxSheet == null ? xssfSheet : sxSheet); + } + + /** + * Unexpected behavior may occur if sheets are reordered after iterator has been created. Support for the remove + * method may be added in the future if someone can figure out a reliable implementation. + */ + @Override + public void remove() throws IllegalStateException { + throw new UnsupportedOperationException("remove method not supported on XSSFWorkbook.iterator(). " + + "Use Sheet.removeSheetAt(int) instead."); + } + } + + /** + * Alias for {@link #sheetIterator()} to allow foreach loops + */ + @Override + public Iterator iterator() { + return sheetIterator(); + } + + @Override + public SXSSFSheet getSheetAt(int index) { + throw new RuntimeException("Not supported by EmittingSXSSFWorkbook"); + } + + public XSSFSheet getXSSFSheetAt(int index) { + return _wb.getSheetAt(index); + } + + /** + * Gets the sheet at the given index for streaming. + * + * @param index the index + * @return the streaming sheet at + */ + public EmittingSXSSFSheet getStreamingSheetAt(int index) { + XSSFSheet xSheet = _wb.getSheetAt(index); + SXSSFSheet sxSheet = getSXSSFSheet(xSheet); + if (sxSheet == null && xSheet != null) { + return (EmittingSXSSFSheet) createAndRegisterSXSSFSheet(xSheet); + } else { + return (EmittingSXSSFSheet) sxSheet; + } + } + + @Override + public SXSSFSheet getSheet(String name) { + throw new RuntimeException("Not supported by EmittingSXSSFWorkbook"); + } + + public XSSFSheet getXSSFSheet(String name) { + return _wb.getSheet(name); + } + + /** + * Gets sheet with the given name for streaming. + * + * @param name the name + * @return the streaming sheet + */ + public EmittingSXSSFSheet getStreamingSheet(String name) { + XSSFSheet xSheet = _wb.getSheet(name); + EmittingSXSSFSheet sxSheet = (EmittingSXSSFSheet) getSXSSFSheet(xSheet); + if (sxSheet == null && xSheet != null) { + return (EmittingSXSSFSheet) createAndRegisterSXSSFSheet(xSheet); + } else { + return sxSheet; + } + } + + /** + * Removes sheet at the given index + * + * @param index of the sheet to remove (0-based) + */ + @Override + public void removeSheetAt(int index) { + // Get the sheet to be removed + XSSFSheet xSheet = _wb.getSheetAt(index); + SXSSFSheet sxSheet = getSXSSFSheet(xSheet); + + // De-register it + _wb.removeSheetAt(index); + + // The sheet may not be a streaming sheet and is not mapped + if (sxSheet != null) { + deregisterSheetMapping(xSheet); + + // Clean up temporary resources + try { + sxSheet.dispose(); + } catch (IOException e) { + logger.log(POILogger.WARN, e); + } + } + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/IRowGenerator.java b/src/ooxml/java/org/apache/poi/xssf/streaming/IRowGenerator.java new file mode 100644 index 00000000000..c9ec4e6c5d0 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/IRowGenerator.java @@ -0,0 +1,37 @@ +/* ==================================================================== + 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.streaming; + +import org.apache.poi.util.Beta; + +/** + * IRowGenerator for Emitting SXSSF sheets + * + * @see EmittingSXSSFWorkbook + */ +@Beta +public interface IRowGenerator { + + /** + * Generate and add rows to the sheet + * + * @param sheet the sheet + * @throws Exception the exception + */ + void generateRows(SXSSFSheet sheet) throws Exception; +} diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFFormulaEvaluator.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFFormulaEvaluator.java index 088694054bb..69f704ef788 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFFormulaEvaluator.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFFormulaEvaluator.java @@ -106,11 +106,13 @@ public static void evaluateAllFormulaCells(SXSSFWorkbook wb, boolean skipOutOfWi // Process the sheets as best we can for (Sheet sheet : wb) { - // Check if any rows have already been flushed out - int lastFlushedRowNum = ((SXSSFSheet) sheet).getLastFlushedRowNum(); - if (lastFlushedRowNum > -1) { - if (! skipOutOfWindow) throw new RowFlushedException(0); - logger.log(POILogger.INFO, "Rows up to " + lastFlushedRowNum + " have already been flushed, skipping"); + if (sheet instanceof SXSSFSheet) { + // Check if any rows have already been flushed out + int lastFlushedRowNum = ((SXSSFSheet) sheet).getLastFlushedRowNum(); + if (lastFlushedRowNum > -1) { + if (! skipOutOfWindow) throw new RowFlushedException(0); + logger.log(POILogger.INFO, "Rows up to " + lastFlushedRowNum + " have already been flushed, skipping"); + } } // Evaluate what we have diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFPicture.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFPicture.java index 1d83b7c7d63..23a43dc3425 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFPicture.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFPicture.java @@ -24,6 +24,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more import org.apache.poi.ss.usermodel.Picture; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Shape; +import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.ImageUtils; import org.apache.poi.util.Internal; @@ -201,7 +202,8 @@ private float getRowHeightInPixels(int rowIndex) { // THE FOLLOWING THREE LINES ARE THE MAIN CHANGE compared to the non-streaming version: use the SXSSF sheet, // not the XSSF sheet (which never contais rows when using SXSSF) XSSFSheet xssfSheet = getSheet(); - SXSSFSheet sheet = _wb.getSXSSFSheet(xssfSheet); + SXSSFSheet sxSheet = _wb.getSXSSFSheet(xssfSheet); + Sheet sheet = sxSheet == null ? xssfSheet : sxSheet; Row row = sheet.getRow(rowIndex); float height = row != null ? row.getHeightInPoints() : sheet.getDefaultRowHeightInPoints(); return height * Units.PIXEL_DPI / Units.POINT_DPI; diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFSheet.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFSheet.java index f639dc12e7b..64f24ddcc1a 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFSheet.java @@ -61,19 +61,26 @@ Licensed to the Apache Software Foundation (ASF) under one or more /** * Streaming version of XSSFSheet implementing the "BigGridDemo" strategy. -*/ + */ public class SXSSFSheet implements Sheet { /*package*/ final XSSFSheet _sh; - private final SXSSFWorkbook _workbook; + protected final SXSSFWorkbook _workbook; private final TreeMap _rows = new TreeMap<>(); - private final SheetDataWriter _writer; + protected SheetDataWriter _writer; private int _randomAccessWindowSize = SXSSFWorkbook.DEFAULT_WINDOW_SIZE; - private final AutoSizeColumnTracker _autoSizeColumnTracker; + protected final AutoSizeColumnTracker _autoSizeColumnTracker; private int outlineLevelRow; private int lastFlushedRowNumber = -1; private boolean allFlushed; + protected SXSSFSheet(SXSSFWorkbook workbook, XSSFSheet xSheet, int randomAccessWindowSize) { + _workbook = workbook; + _sh = xSheet; + setRandomAccessWindowSize(randomAccessWindowSize); + _autoSizeColumnTracker = new AutoSizeColumnTracker(this); + } + public SXSSFSheet(SXSSFWorkbook workbook, XSSFSheet xSheet) throws IOException { _workbook = workbook; _sh = xSheet; @@ -90,8 +97,8 @@ SheetDataWriter getSheetDataWriter(){ return _writer; } -/* Gets "" document fragment*/ - public InputStream getWorksheetXMLInputStream() throws IOException + /* Gets "" document fragment*/ + public InputStream getWorksheetXMLInputStream() throws IOException { // flush all remaining data and close the temp file writer flushRows(0); @@ -99,7 +106,7 @@ public InputStream getWorksheetXMLInputStream() throws IOException return _writer.getWorksheetXMLInputStream(); } -//start of interface implementation + //start of interface implementation @Override public Iterator iterator() { @@ -128,7 +135,7 @@ public SXSSFRow createRow(int rownum) if(rownum <= _writer.getLastFlushedRow() ) { throw new IllegalArgumentException( "Attempting to write a row["+rownum+"] " + - "in the range [0," + _writer.getLastFlushedRow() + "] that is already written to disk."); + "in the range [0," + _writer.getLastFlushedRow() + "] that is already written to disk."); } // attempt to overwrite a existing row in the input template @@ -143,7 +150,7 @@ public SXSSFRow createRow(int rownum) allFlushed = false; if(_randomAccessWindowSize >= 0 && _rows.size() > _randomAccessWindowSize) { try { - flushRows(_randomAccessWindowSize); + flushRows(_randomAccessWindowSize); } catch (IOException ioe) { throw new RuntimeException(ioe); } @@ -274,7 +281,7 @@ public int getColumnWidth(int columnIndex) /** * Get the actual column width in pixels - * + * *

* Please note, that this method works correctly only for workbooks * with the default font size (Calibri 11pt for .xlsx). @@ -283,8 +290,8 @@ public int getColumnWidth(int columnIndex) @Override public float getColumnWidthInPixels(int columnIndex) { return _sh.getColumnWidthInPixels(columnIndex); - } - + } + /** * Set the default column width for the sheet (if the columns do not define their own width) * in characters @@ -308,7 +315,7 @@ public int getDefaultColumnWidth() { return _sh.getDefaultColumnWidth(); } - + /** * Get the default row height for the sheet (if the rows do not define their own height) in @@ -356,7 +363,7 @@ public void setDefaultRowHeightInPoints(float height) { _sh.setDefaultRowHeightInPoints(height); } - + /** * Returns the CellStyle that applies to the given @@ -461,7 +468,7 @@ public void removeMergedRegion(int index) { _sh.removeMergedRegion(index); } - + /** * Removes a merged region of cells (hence letting them free) * @@ -568,7 +575,7 @@ public boolean isDisplayZeros() { return _sh.isDisplayZeros(); } - + /** * Sets whether the worksheet is displayed from right to left instead of from left to right. * @@ -577,7 +584,7 @@ public boolean isDisplayZeros() @Override public void setRightToLeft(boolean value) { - _sh.setRightToLeft(value); + _sh.setRightToLeft(value); } /** @@ -588,7 +595,7 @@ public void setRightToLeft(boolean value) @Override public boolean isRightToLeft() { - return _sh.isRightToLeft(); + return _sh.isRightToLeft(); } /** @@ -733,7 +740,7 @@ public void setPrintGridlines(boolean show) { _sh.setPrintGridlines(show); } - + /** * Returns whether row and column headings are printed. * @@ -841,7 +848,7 @@ public boolean getProtect() { return _sh.getProtect(); } - + /** * Sets the protection enabled as well as the password * @param password to set for protection. Pass null to remove protection @@ -851,7 +858,7 @@ public void protectSheet(String password) { _sh.protectSheet(password); } - + /** * Answer whether scenario protection is enabled or disabled * @@ -862,7 +869,7 @@ public boolean getScenarioProtect() { return _sh.getScenarioProtect(); } - + /** * Window zoom magnification for current view representing percent values. * Valid values range from 10 to 400. Horizontal and Vertical scale together. @@ -933,7 +940,7 @@ public void showInPane(int topRow, int leftCol) */ @Override public void setForceFormulaRecalculation(boolean value) { - _sh.setForceFormulaRecalculation(value); + _sh.setForceFormulaRecalculation(value); } /** @@ -942,12 +949,12 @@ public void setForceFormulaRecalculation(boolean value) { */ @Override public boolean getForceFormulaRecalculation() { - return _sh.getForceFormulaRecalculation(); + return _sh.getForceFormulaRecalculation(); } /** * Not implemented for SXSSFSheets - * + * * Shifts rows between startRow and endRow n number of rows. * If you use a negative number, it will shift rows up. * Code ensures that rows don't wrap around. @@ -969,7 +976,7 @@ public void shiftRows(int startRow, int endRow, int n) { /** * Not implemented for SXSSFSheets - * + * * Shifts rows between startRow and endRow n number of rows. * If you use a negative number, it will shift rows up. * Code ensures that rows don't wrap around @@ -1236,7 +1243,7 @@ public void ungroupColumn(int fromColumn, int toColumn) * Please note the rows being grouped must be in the current window, * if the rows are already flushed then groupRow has no effect. *

- * + * * Correct code: *

      *       Workbook wb = new SXSSFWorkbook(100);  // keep 100 rows in memory
@@ -1249,8 +1256,8 @@ public void ungroupColumn(int fromColumn, int toColumn)
      *       }
      *
      *      
- * - * + * + * * Incorrect code: *

      *       Workbook wb = new SXSSFWorkbook(100);  // keep 100 rows in memory
@@ -1261,7 +1268,7 @@ public void ungroupColumn(int fromColumn, int toColumn)
      *       sh.groupRow(100, 200); // the rows in the range [100, 200] are already flushed and groupRows has no effect
      *
      *      
- * + * * * @param fromRow start row (0-based) * @param toRow end row (0-based) @@ -1280,7 +1287,7 @@ public void groupRow(int fromRow, int toRow) setWorksheetOutlineLevelRow(); } - + /** * Set row groupings (like groupRow) in a stream-friendly manner * @@ -1308,8 +1315,8 @@ public void setRowOutlineLevel(int rownum, int level) private void setWorksheetOutlineLevelRow() { CTWorksheet ct = _sh.getCTWorksheet(); CTSheetFormatPr pr = ct.isSetSheetFormatPr() ? - ct.getSheetFormatPr() : - ct.addNewSheetFormatPr(); + ct.getSheetFormatPr() : + ct.addNewSheetFormatPr(); if(outlineLevelRow > 0) { pr.setOutlineLevelRow((short)outlineLevelRow); } @@ -1346,7 +1353,7 @@ public void setRowGroupCollapsed(int row, boolean collapse) throw new RuntimeException("Unable to expand row: Not Implemented"); } } - + /** * @param rowIndex the zero based row index to collapse */ @@ -1359,7 +1366,7 @@ private void collapseRow(int rowIndex) { // Hide all the columns until the end of the group int lastRow = writeHidden(row, startRow); - SXSSFRow lastRowObj = getRow(lastRow); + SXSSFRow lastRowObj = getRow(lastRow); if (lastRowObj != null) { lastRowObj.setCollapsed(true); } else { @@ -1368,7 +1375,7 @@ private void collapseRow(int rowIndex) { } } } - + /** * @param rowIndex the zero based row index to find from */ @@ -1388,7 +1395,7 @@ private int findStartOfRowOutlineGroup(int rowIndex) { } return currentRow + 1; } - + private int writeHidden(SXSSFRow xRow, int rowIndex) { int level = xRow.getOutlineLevel(); SXSSFRow currRow = getRow(rowIndex); @@ -1412,8 +1419,8 @@ public void setDefaultColumnStyle(int column, CellStyle style) { _sh.setDefaultColumnStyle(column, style); } - - + + /** * Track a column in the sheet for auto-sizing. * Note this has undefined behavior if a column is tracked after one or more rows are written to the sheet. @@ -1428,7 +1435,7 @@ public void trackColumnForAutoSizing(int column) { _autoSizeColumnTracker.trackColumn(column); } - + /** * Track several columns in the sheet for auto-sizing. * Note this has undefined behavior if columns are tracked after one or more rows are written to the sheet. @@ -1441,7 +1448,7 @@ public void trackColumnsForAutoSizing(Collection columns) { _autoSizeColumnTracker.trackColumns(columns); } - + /** * Tracks all columns in the sheet for auto-sizing. If this is called, individual columns do not need to be tracked. * Because determining the best-fit width for a cell is expensive, this may affect the performance. @@ -1451,7 +1458,7 @@ public void trackAllColumnsForAutoSizing() { _autoSizeColumnTracker.trackAllColumns(); } - + /** * Removes a column that was previously marked for inclusion in auto-size column tracking. * When a column is untracked, the best-fit width is forgotten. @@ -1467,7 +1474,7 @@ public boolean untrackColumnForAutoSizing(int column) { return _autoSizeColumnTracker.untrackColumn(column); } - + /** * Untracks several columns in the sheet for auto-sizing. * When a column is untracked, the best-fit width is forgotten. @@ -1481,7 +1488,7 @@ public boolean untrackColumnsForAutoSizing(Collection columns) { return _autoSizeColumnTracker.untrackColumns(columns); } - + /** * Untracks all columns in the sheet for auto-sizing. Best-fit column widths are forgotten. * If this is called, individual columns do not need to be untracked. @@ -1491,7 +1498,7 @@ public void untrackAllColumnsForAutoSizing() { _autoSizeColumnTracker.untrackAllColumns(); } - + /** * Returns true if column is currently tracked for auto-sizing. * @@ -1503,7 +1510,7 @@ public boolean isColumnTrackedForAutoSizing(int column) { return _autoSizeColumnTracker.isColumnTracked(column); } - + /** * Get the currently tracked columns for auto-sizing. * Note if all columns are tracked, this will only return the columns that have been explicitly or implicitly tracked, @@ -1527,7 +1534,7 @@ public Set getTrackedColumnsForAutoSizing() *

* You can specify whether the content of merged cells should be considered or ignored. * Default is to ignore merged cells. - * + * *

* Special note about SXSSF implementation: You must register the columns you wish to track with * the SXSSFSheet using {@link #trackColumnForAutoSizing(int)} or {@link #trackAllColumnsForAutoSizing()}. @@ -1554,7 +1561,7 @@ public void autoSizeColumn(int column) *

* You can specify whether the content of merged cells should be considered or ignored. * Default is to ignore merged cells. - * + * *

* Special note about SXSSF implementation: You must register the columns you wish to track with * the SXSSFSheet using {@link #trackColumnForAutoSizing(int)} or {@link #trackAllColumnsForAutoSizing()}. @@ -1590,21 +1597,21 @@ public void autoSizeColumn(int column, boolean useMergedCells) catch (final IllegalStateException e) { throw new IllegalStateException("Could not auto-size column. Make sure the column was tracked prior to auto-sizing the column.", e); } - + // get the best-fit width of rows currently in the random access window final int activeWidth = (int) (256 * SheetUtil.getColumnWidth(this, column, useMergedCells)); // the best-fit width for both flushed rows and random access window rows // flushedWidth or activeWidth may be negative if column contains only blank cells final int bestFitWidth = Math.max(flushedWidth, activeWidth); - + if (bestFitWidth > 0) { final int maxColumnWidth = 255*256; // The maximum column width for an individual cell is 255 characters final int width = Math.min(bestFitWidth, maxColumnWidth); setColumnWidth(column, width); } } - + /** * Returns cell comment for the specified row and column * @@ -1615,7 +1622,7 @@ public XSSFComment getCellComment(CellAddress ref) { return _sh.getCellComment(ref); } - + /** * Returns all cell comments on this sheet. * @return A map of each Comment in the sheet, keyed on the cell address where @@ -1625,7 +1632,7 @@ public XSSFComment getCellComment(CellAddress ref) public Map getCellComments() { return _sh.getCellComments(); } - + /** * Get a Hyperlink in this sheet anchored at row, column * @@ -1637,7 +1644,7 @@ public Map getCellComments() { public XSSFHyperlink getHyperlink(int row, int column) { return _sh.getHyperlink(row, column); } - + /** * Get a Hyperlink in this sheet located in a cell specified by {code addr} * @@ -1649,7 +1656,7 @@ public XSSFHyperlink getHyperlink(int row, int column) { public XSSFHyperlink getHyperlink(CellAddress addr) { return _sh.getHyperlink(addr); } - + /** * Get a list of Hyperlinks in this sheet * @@ -1659,7 +1666,7 @@ public XSSFHyperlink getHyperlink(CellAddress addr) { public List getHyperlinkList() { return _sh.getHyperlinkList(); } - + /** * {@inheritDoc} */ @@ -1744,7 +1751,7 @@ public CellRange removeArrayFormula(Cell cell) { throw new RuntimeException("Not Implemented"); } - + @Override public DataValidationHelper getDataValidationHelper() { @@ -1769,7 +1776,7 @@ public void addValidationData(DataValidation dataValidation) /** * Enable filtering for a range of cells - * + * * @param range the range of cells to filter */ @Override @@ -1782,30 +1789,30 @@ public AutoFilter setAutoFilter(CellRangeAddress range) public SheetConditionalFormatting getSheetConditionalFormatting(){ return _sh.getSheetConditionalFormatting(); } - - + + @Override public CellRangeAddress getRepeatingRows() { - return _sh.getRepeatingRows(); + return _sh.getRepeatingRows(); } - + @Override public CellRangeAddress getRepeatingColumns() { - return _sh.getRepeatingColumns(); + return _sh.getRepeatingColumns(); } - + @Override public void setRepeatingRows(CellRangeAddress rowRangeRef) { - _sh.setRepeatingRows(rowRangeRef); + _sh.setRepeatingRows(rowRangeRef); } - + @Override public void setRepeatingColumns(CellRangeAddress columnRangeRef) { - _sh.setRepeatingColumns(columnRangeRef); + _sh.setRepeatingColumns(columnRangeRef); } - - - + + + //end of interface implementation /** * Specifies how many rows can be accessed at most via getRow(). @@ -1821,12 +1828,12 @@ public void setRepeatingColumns(CellRangeAddress columnRangeRef) { */ public void setRandomAccessWindowSize(int value) { - if(value == 0 || value < -1) { - throw new IllegalArgumentException("RandomAccessWindowSize must be either -1 or a positive integer"); - } - _randomAccessWindowSize = value; + if(value == 0 || value < -1) { + throw new IllegalArgumentException("RandomAccessWindowSize must be either -1 or a positive integer"); + } + _randomAccessWindowSize = value; } - + /** * Are all rows flushed to disk? */ @@ -1880,7 +1887,7 @@ private void flushOneRow() throws IOException } public void changeRowNum(SXSSFRow row, int newRowNum) { - + removeRow(row); _rows.put(newRowNum,row); } @@ -1904,7 +1911,7 @@ boolean dispose() throws IOException { if (!allFlushed) { flushRows(); } - return _writer.dispose(); + return _writer == null || _writer.dispose(); } @Override @@ -1935,21 +1942,21 @@ public XSSFColor getTabColor() { public void setTabColor(XSSFColor color) { _sh.setTabColor(color); } - + /** * Enable sheet protection */ public void enableLocking() { safeGetProtectionField().setSheet(true); } - + /** * Disable sheet protection */ public void disableLocking() { safeGetProtectionField().setSheet(false); } - + /** * Enable or disable Autofilters locking. * This does not modify sheet protection status. @@ -1958,7 +1965,7 @@ public void disableLocking() { public void lockAutoFilter(boolean enabled) { safeGetProtectionField().setAutoFilter(enabled); } - + /** * Enable or disable Deleting columns locking. * This does not modify sheet protection status. @@ -1967,7 +1974,7 @@ public void lockAutoFilter(boolean enabled) { public void lockDeleteColumns(boolean enabled) { safeGetProtectionField().setDeleteColumns(enabled); } - + /** * Enable or disable Deleting rows locking. * This does not modify sheet protection status. @@ -1976,7 +1983,7 @@ public void lockDeleteColumns(boolean enabled) { public void lockDeleteRows(boolean enabled) { safeGetProtectionField().setDeleteRows(enabled); } - + /** * Enable or disable Formatting cells locking. * This does not modify sheet protection status. @@ -1985,7 +1992,7 @@ public void lockDeleteRows(boolean enabled) { public void lockFormatCells(boolean enabled) { safeGetProtectionField().setFormatCells(enabled); } - + /** * Enable or disable Formatting columns locking. * This does not modify sheet protection status. @@ -1994,7 +2001,7 @@ public void lockFormatCells(boolean enabled) { public void lockFormatColumns(boolean enabled) { safeGetProtectionField().setFormatColumns(enabled); } - + /** * Enable or disable Formatting rows locking. * This does not modify sheet protection status. @@ -2003,7 +2010,7 @@ public void lockFormatColumns(boolean enabled) { public void lockFormatRows(boolean enabled) { safeGetProtectionField().setFormatRows(enabled); } - + /** * Enable or disable Inserting columns locking. * This does not modify sheet protection status. @@ -2012,7 +2019,7 @@ public void lockFormatRows(boolean enabled) { public void lockInsertColumns(boolean enabled) { safeGetProtectionField().setInsertColumns(enabled); } - + /** * Enable or disable Inserting hyperlinks locking. * This does not modify sheet protection status. @@ -2021,7 +2028,7 @@ public void lockInsertColumns(boolean enabled) { public void lockInsertHyperlinks(boolean enabled) { safeGetProtectionField().setInsertHyperlinks(enabled); } - + /** * Enable or disable Inserting rows locking. * This does not modify sheet protection status. @@ -2030,7 +2037,7 @@ public void lockInsertHyperlinks(boolean enabled) { public void lockInsertRows(boolean enabled) { safeGetProtectionField().setInsertRows(enabled); } - + /** * Enable or disable Pivot Tables locking. * This does not modify sheet protection status. @@ -2039,7 +2046,7 @@ public void lockInsertRows(boolean enabled) { public void lockPivotTables(boolean enabled) { safeGetProtectionField().setPivotTables(enabled); } - + /** * Enable or disable Sort locking. * This does not modify sheet protection status. @@ -2048,7 +2055,7 @@ public void lockPivotTables(boolean enabled) { public void lockSort(boolean enabled) { safeGetProtectionField().setSort(enabled); } - + /** * Enable or disable Objects locking. * This does not modify sheet protection status. @@ -2057,7 +2064,7 @@ public void lockSort(boolean enabled) { public void lockObjects(boolean enabled) { safeGetProtectionField().setObjects(enabled); } - + /** * Enable or disable Scenarios locking. * This does not modify sheet protection status. @@ -2066,7 +2073,7 @@ public void lockObjects(boolean enabled) { public void lockScenarios(boolean enabled) { safeGetProtectionField().setScenarios(enabled); } - + /** * Enable or disable Selection of locked cells locking. * This does not modify sheet protection status. @@ -2075,7 +2082,7 @@ public void lockScenarios(boolean enabled) { public void lockSelectLockedCells(boolean enabled) { safeGetProtectionField().setSelectLockedCells(enabled); } - + /** * Enable or disable Selection of unlocked cells locking. * This does not modify sheet protection status. @@ -2085,7 +2092,7 @@ public void lockSelectUnlockedCells(boolean enabled) { safeGetProtectionField().setSelectUnlockedCells(enabled); } - + private CTSheetProtection safeGetProtectionField() { CTWorksheet ct = _sh.getCTWorksheet(); if (!isSheetProtectionEnabled()) { @@ -2093,12 +2100,12 @@ private CTSheetProtection safeGetProtectionField() { } return ct.getSheetProtection(); } - + /* package */ boolean isSheetProtectionEnabled() { CTWorksheet ct = _sh.getCTWorksheet(); return (ct.isSetSheetProtection()); } - + /** * Set background color of the sheet tab * @@ -2112,10 +2119,10 @@ public void setTabColor(int colorIndex){ color.setIndexed(colorIndex); pr.setTabColor(color); } - - @NotImplemented - @Override - public void shiftColumns(int startColumn, int endColumn, int n){ - throw new UnsupportedOperationException("Not Implemented"); + + @NotImplemented + @Override + public void shiftColumns(int startColumn, int endColumn, int n){ + throw new UnsupportedOperationException("Not Implemented"); } } diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java index 2074a70b4ac..176986d479d 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java @@ -96,13 +96,17 @@ public class SXSSFWorkbook implements Workbook { public static final int DEFAULT_WINDOW_SIZE = 100; private static final POILogger logger = POILogFactory.getLogger(SXSSFWorkbook.class); - private final XSSFWorkbook _wb; + protected final XSSFWorkbook _wb; private final Map _sxFromXHash = new HashMap<>(); private final Map _xFromSxHash = new HashMap<>(); private int _randomAccessWindowSize = DEFAULT_WINDOW_SIZE; + protected static interface ISheetInjector { + void writeSheetData(OutputStream out) throws IOException; + } + /** * whether temp files should be compressed. */ @@ -111,23 +115,23 @@ public class SXSSFWorkbook implements Workbook { /** * shared string table - a cache of strings in this workbook */ - private final SharedStringsTable _sharedStringSource; + protected final SharedStringsTable _sharedStringSource; /** * controls whether Zip64 mode is used - Always became the default in POI 5.0.0 */ - private Zip64Mode zip64Mode = Zip64Mode.Always; + protected Zip64Mode zip64Mode = Zip64Mode.Always; /** * Construct a new workbook with default row window size */ public SXSSFWorkbook(){ - this(null /*workbook*/); + this(null /*workbook*/); } /** *

Construct a workbook from a template.

- * + * * There are three use-cases to use SXSSFWorkbook(XSSFWorkbook) : *
    *
  1. @@ -144,7 +148,7 @@ public SXSSFWorkbook(){ *
  2. *
* All three use cases can work in a combination. - * + * * What is not supported: *
    *
  • @@ -161,9 +165,9 @@ public SXSSFWorkbook(){ * @param workbook the template workbook */ public SXSSFWorkbook(XSSFWorkbook workbook){ - this(workbook, DEFAULT_WINDOW_SIZE); + this(workbook, DEFAULT_WINDOW_SIZE); } - + /** * Constructs an workbook from an existing workbook. @@ -186,7 +190,7 @@ public SXSSFWorkbook(XSSFWorkbook workbook){ * @param rowAccessWindowSize the number of rows that are kept in memory until flushed out, see above. */ public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize){ - this(workbook,rowAccessWindowSize, false); + this(workbook,rowAccessWindowSize, false); } /** @@ -210,8 +214,8 @@ public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize){ * @param rowAccessWindowSize the number of rows that are kept in memory until flushed out, see above. * @param compressTmpFiles whether to use gzip compression for temporary files */ - public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize, boolean compressTmpFiles){ - this(workbook,rowAccessWindowSize, compressTmpFiles, false); + public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize, boolean compressTmpFiles) { + this(workbook,rowAccessWindowSize, compressTmpFiles, false); } /** @@ -237,11 +241,11 @@ public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize, boolean com * @param compressTmpFiles whether to use gzip compression for temporary files * @param useSharedStringsTable whether to use a shared strings table */ - public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize, boolean compressTmpFiles, boolean useSharedStringsTable){ + public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize, boolean compressTmpFiles, boolean useSharedStringsTable) { setRandomAccessWindowSize(rowAccessWindowSize); setCompressTempFiles(compressTmpFiles); if (workbook == null) { - _wb=new XSSFWorkbook(); + _wb = new XSSFWorkbook(); _sharedStringSource = useSharedStringsTable ? _wb.getSharedStringSource() : null; } else { _wb=workbook; @@ -273,7 +277,7 @@ public SXSSFWorkbook(XSSFWorkbook workbook, int rowAccessWindowSize, boolean com * @param rowAccessWindowSize the number of rows that are kept in memory until flushed out, see above. */ public SXSSFWorkbook(int rowAccessWindowSize){ - this(null /*workbook*/, rowAccessWindowSize); + this(null /*workbook*/, rowAccessWindowSize); } /** @@ -282,10 +286,10 @@ public SXSSFWorkbook(int rowAccessWindowSize){ * @return The number of rows that are kept in memory at once before flushing them out. */ public int getRandomAccessWindowSize() { - return _randomAccessWindowSize; + return _randomAccessWindowSize; } - private void setRandomAccessWindowSize(int rowAccessWindowSize) { + protected void setRandomAccessWindowSize(int rowAccessWindowSize) { if(rowAccessWindowSize == 0 || rowAccessWindowSize < -1) { throw new IllegalArgumentException("rowAccessWindowSize must be greater than 0 or -1"); } @@ -323,7 +327,7 @@ public boolean isCompressTempFiles() { * Please note the the "compress" option may cause performance penalty. *

    *

    - * Setting this option only affects compression for subsequent createSheet() + * Setting this option only affects compression for subsequent createSheet() * calls. *

    * @param compress whether to compress temp files @@ -331,7 +335,7 @@ public boolean isCompressTempFiles() { public void setCompressTempFiles(boolean compress) { _compressTmpFiles = compress; } - + @Internal protected SharedStringsTable getSharedStringSource() { return _sharedStringSource; @@ -341,7 +345,7 @@ protected SheetDataWriter createSheetDataWriter() throws IOException { if(_compressTmpFiles) { return new GZIPSheetDataWriter(_sharedStringSource); } - + return new SheetDataWriter(_sharedStringSource); } @@ -364,20 +368,20 @@ void registerSheetMapping(SXSSFSheet sxSheet,XSSFSheet xSheet) void deregisterSheetMapping(XSSFSheet xSheet) { SXSSFSheet sxSheet=getSXSSFSheet(xSheet); - + // ensure that the writer is closed in all cases to not have lingering writers try { sxSheet.getSheetDataWriter().close(); } catch (IOException e) { // ignore exception here } - + _sxFromXHash.remove(sxSheet); _xFromSxHash.remove(xSheet); } - private XSSFSheet getSheetFromZipEntryName(String sheetRef) + protected XSSFSheet getSheetFromZipEntryName(String sheetRef) { for(XSSFSheet sheet : _sxFromXHash.values()) { @@ -408,9 +412,7 @@ protected void injectData(ZipEntrySource zipEntrySource, OutputStream out) throw // See bug 56557, we should not inject data into the special ChartSheets if (xSheet != null && !(xSheet instanceof XSSFChartSheet)) { SXSSFSheet sxSheet = getSXSSFSheet(xSheet); - try (InputStream xis = sxSheet.getWorksheetXMLInputStream()) { - copyStreamAndInjectWorksheet(is, zos, xis); - } + copyStreamAndInjectWorksheet(is, zos, createSheetInjector(sxSheet)); } else { IOUtils.copy(is, zos); } @@ -434,7 +436,17 @@ protected ZipArchiveOutputStream createArchiveOutputStream(OutputStream out) { } } - private static void copyStreamAndInjectWorksheet(InputStream in, OutputStream out, InputStream worksheetData) throws IOException { + protected ISheetInjector createSheetInjector(SXSSFSheet sxSheet) throws IOException { + return (output) -> { + try (InputStream xis = sxSheet.getWorksheetXMLInputStream()) { + // Copy the worksheet data to "output". + IOUtils.copy(xis, output); + } + }; + } + + // private static void copyStreamAndInjectWorksheet(InputStream in, OutputStream out, InputStream worksheetData) throws IOException { + private static void copyStreamAndInjectWorksheet(InputStream in, OutputStream out, ISheetInjector sheetInjector) throws IOException { InputStreamReader inReader = new InputStreamReader(in, StandardCharsets.UTF_8); OutputStreamWriter outWriter = new OutputStreamWriter(out, StandardCharsets.UTF_8); boolean needsStartTag = true; @@ -450,58 +462,58 @@ private static void copyStreamAndInjectWorksheet(InputStream in, OutputStream ou pos++; if(pos==n) { - if ("') - { - // Found - outWriter.write(s); - outWriter.write(c); - s = ""; - n = s.length(); - pos = 0; - needsStartTag = false; - continue; - } - if (c == '/') - { - // Found ') - { - // Found - break; - } - - outWriter.write(s); - outWriter.write('/'); - outWriter.write(c); - pos = 0; - continue; - } - - outWriter.write(s); - outWriter.write('/'); - outWriter.write(c); - pos = 0; - continue; - } - else - { - // Found - break; - } + if ("') + { + // Found + outWriter.write(s); + outWriter.write(c); + s = ""; + n = s.length(); + pos = 0; + needsStartTag = false; + continue; + } + if (c == '/') + { + // Found ') + { + // Found + break; + } + + outWriter.write(s); + outWriter.write('/'); + outWriter.write(c); + pos = 0; + continue; + } + + outWriter.write(s); + outWriter.write('/'); + outWriter.write(c); + pos = 0; + continue; + } + else + { + // Found + break; + } } } else @@ -523,11 +535,10 @@ private static void copyStreamAndInjectWorksheet(InputStream in, OutputStream ou outWriter.flush(); if (needsStartTag) { - outWriter.write("\n"); - outWriter.flush(); + outWriter.write("\n"); + outWriter.flush(); } - //Copy the worksheet data to "out". - IOUtils.copy(worksheetData,out); + sheetInjector.writeSheetData(out); outWriter.write(""); outWriter.flush(); //Copy the rest of "in" to "out". @@ -732,7 +743,7 @@ public int getNumberOfSheets() { return _wb.getNumberOfSheets(); } - + /** * Returns an iterator of the sheets in the workbook * in sheet order. Includes hidden and very hidden sheets. @@ -743,7 +754,7 @@ public int getNumberOfSheets() public Iterator sheetIterator() { return new SheetIterator<>(); } - + private final class SheetIterator implements Iterator { final private Iterator it; @SuppressWarnings("unchecked") @@ -771,7 +782,7 @@ public void remove() throws IllegalStateException { "Use Sheet.removeSheetAt(int) instead."); } } - + /** * Alias for {@link #sheetIterator()} to allow * foreach loops @@ -816,11 +827,11 @@ public void removeSheetAt(int index) // Get the sheet to be removed XSSFSheet xSheet = _wb.getSheetAt(index); SXSSFSheet sxSheet = getSXSSFSheet(xSheet); - + // De-register it _wb.removeSheetAt(index); deregisterSheetMapping(xSheet); - + // Clean up temporary resources try { sxSheet.dispose(); @@ -839,7 +850,7 @@ public Font createFont() { return _wb.createFont(); } - + /** * Finds a font that matches the one with the supplied attributes * @@ -905,7 +916,7 @@ public CellStyle getCellStyleAt(int idx) } /** - * Closes the underlying {@link XSSFWorkbook} and {@link OPCPackage} + * Closes the underlying {@link XSSFWorkbook} and {@link OPCPackage} * on which this Workbook is based, if any. * *

    Once this has been called, no further @@ -918,20 +929,21 @@ public void close() throws IOException { for (SXSSFSheet sheet : _xFromSxHash.values()) { try { - sheet.getSheetDataWriter().close(); + SheetDataWriter _writer = sheet.getSheetDataWriter(); + if (_writer != null) _writer.close(); } catch (IOException e) { logger.log(POILogger.WARN, "An exception occurred while closing sheet data writer for sheet " - + sheet.getSheetName() + ".", e); + + sheet.getSheetName() + ".", e); } } - - // Tell the base workbook to close, does nothing if + + // Tell the base workbook to close, does nothing if // it's a newly created one _wb.close(); } - + /** * Write out this workbook to an OutputStream. * @@ -962,14 +974,14 @@ public void write(OutputStream stream) throws IOException { throw new IOException("Could not delete temporary file after processing: " + tmplFile); } } - + protected void flushSheets() throws IOException { for (SXSSFSheet sheet : _xFromSxHash.values()) { sheet.flushRows(); } } - + /** * Dispose of temporary files backing this workbook on disk. * Calling this method will render the workbook unusable. @@ -1053,7 +1065,7 @@ public void removeName(Name name) _wb.removeName(name); } - /** + /** * Sets the printarea for the sheet provided *

    * i.e. Reference = $A$1:$B$2 @@ -1188,7 +1200,7 @@ public CreationHelper getCreationHelper() { protected boolean isDate1904() { return _wb.isDate1904(); } - + @Override @NotImplemented("XSSFWorkbook#isHidden is not implemented") public boolean isHidden() @@ -1214,7 +1226,7 @@ public boolean isSheetVeryHidden(int sheetIx) { return _wb.isSheetVeryHidden(sheetIx); } - + @Override public SheetVisibility getSheetVisibility(int sheetIx) { return _wb.getSheetVisibility(sheetIx); @@ -1230,13 +1242,13 @@ public void setSheetHidden(int sheetIx, boolean hidden) public void setSheetVisibility(int sheetIx, SheetVisibility visibility) { _wb.setSheetVisibility(sheetIx, visibility); } - + /** * Not implemented for SXSSFWorkbook * * Adds the LinkTable records required to allow formulas referencing * the specified external workbook to be added to this one. Allows - * formulas such as "[MyOtherWorkbook]Sheet3!$A$5" to be added to the + * formulas such as "[MyOtherWorkbook]Sheet3!$A$5" to be added to the * file, for workbooks not already referenced. * * Note: this is not implemented and thus currently throws an Exception stating this. @@ -1251,7 +1263,7 @@ public void setSheetVisibility(int sheetIx, SheetVisibility visibility) { public int linkExternalWorkbook(String name, Workbook workbook) { throw new RuntimeException("Not Implemented"); } - + /** * Register a new toolpack in this workbook. * @@ -1290,7 +1302,7 @@ public boolean getForceFormulaRecalculation(){ /** * Returns the spreadsheet version (EXCLE2007) of this workbook - * + * * @return EXCEL2007 SpreadsheetVersion enum * @since 3.14 beta 2 */ @@ -1303,6 +1315,6 @@ public SpreadsheetVersion getSpreadsheetVersion() { public int addOlePackage(byte[] oleData, String label, String fileName, String command) throws IOException { return _wb.addOlePackage(oleData, label, fileName, command); } - + //end of interface implementation } diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SheetDataWriter.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SheetDataWriter.java index cbac3b8822d..8cf5cbc0e2b 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SheetDataWriter.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SheetDataWriter.java @@ -55,7 +55,7 @@ public class SheetDataWriter implements Closeable { private static final POILogger logger = POILogFactory.getLogger(SheetDataWriter.class); private final File _fd; - private final Writer _out; + protected final Writer _out; private int _rownum; private int _numberOfFlushedRows; private int _lowestIndexOfFlushedRows; // meaningful only of _numberOfFlushedRows>0 @@ -72,6 +72,11 @@ public SheetDataWriter() throws IOException { _fd = createTempFile(); _out = createWriter(_fd); } + + public SheetDataWriter(Writer writer) throws IOException { + _fd = null; + _out = writer; + } public SheetDataWriter(SharedStringsTable sharedStringsTable) throws IOException { this(); diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/StreamingSheetWriter.java b/src/ooxml/java/org/apache/poi/xssf/streaming/StreamingSheetWriter.java new file mode 100644 index 00000000000..c4f64712198 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/StreamingSheetWriter.java @@ -0,0 +1,84 @@ +/* ==================================================================== + 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.streaming; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import org.apache.poi.util.Beta; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * Unlike SheetDataWriter, this writer does not create a temporary file, it writes data directly + * to the provided OutputStream. + * @since 5.0.0 + */ +@Beta +public class StreamingSheetWriter extends SheetDataWriter { + private static final POILogger logger = POILogFactory.getLogger(StreamingSheetWriter.class); + + public StreamingSheetWriter() throws IOException { + throw new RuntimeException("StreamingSheetWriter requires OutputStream"); + } + + public StreamingSheetWriter(OutputStream out) throws IOException { + super(createWriter(out)); + logger.log(POILogger.DEBUG, "Preparing SSXSSF sheet writer"); + } + + @Override + public File createTempFile() throws IOException { + throw new RuntimeException("Not supported with StreamingSheetWriter"); + } + + @Override + public Writer createWriter(File fd) throws IOException { + throw new RuntimeException("Not supported with StreamingSheetWriter"); + } + + /** + * Create a writer for the sheet data. + * + * @param out the output stream to write to + */ + protected static Writer createWriter(OutputStream out) throws IOException { + return new BufferedWriter(new OutputStreamWriter(out, "UTF-8")); + } + + @Override + public void close() throws IOException { + _out.flush(); + } + + @Override + public InputStream getWorksheetXMLInputStream() throws IOException { + throw new RuntimeException("Not supported with StreamingSheetWriter"); + } + + @Override + boolean dispose() throws IOException { + _out.close(); + return true; + } +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestEmittingSXSSFWorkbook.java b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestEmittingSXSSFWorkbook.java new file mode 100644 index 00000000000..df526b1b541 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/streaming/TestEmittingSXSSFWorkbook.java @@ -0,0 +1,488 @@ +/* + * ==================================================================== + * 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.streaming; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackageAccess; +import org.apache.poi.ss.usermodel.BaseTestXWorkbook; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellReference; +import org.apache.poi.util.NullOutputStream; +import org.apache.poi.xssf.SXSSFITestDataProvider; +import org.apache.poi.xssf.XSSFTestDataSamples; +import org.apache.poi.xssf.usermodel.XSSFCell; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.After; +import org.junit.Ignore; +import org.junit.Test; + +public final class TestEmittingSXSSFWorkbook extends BaseTestXWorkbook { + + public TestEmittingSXSSFWorkbook() { + super(SXSSFITestDataProvider.instance); + } + + @After + public void tearDown() { + ((SXSSFITestDataProvider) _testDataProvider).cleanup(); + } + + /** + * cloning of sheets is not supported in SXSSF + */ + @Override + @Test + public void cloneSheet() throws IOException { + try { + super.cloneSheet(); + fail("expected exception"); + } catch (RuntimeException e) { + assertEquals("Not Implemented", e.getMessage()); + } + } + + /** + * cloning of sheets is not supported in SXSSF + */ + @Override + @Test + public void sheetClone() throws IOException { + try { + super.sheetClone(); + fail("expected exception"); + } catch (RuntimeException e) { + assertEquals("Not Implemented", e.getMessage()); + } + } + + /** + * Skip this test, as SXSSF doesn't update formulas on sheet name changes. + */ + @Override + @Ignore("SXSSF doesn't update formulas on sheet name changes, as most cells probably aren't in memory at the time") + @Test + public void setSheetName() { + } + + @Test + public void existingWorkbook() throws IOException { + XSSFWorkbook xssfWb1 = new XSSFWorkbook(); + xssfWb1.createSheet("S1"); + EmittingSXSSFWorkbook wb1 = new EmittingSXSSFWorkbook(xssfWb1); + XSSFWorkbook xssfWb2 = SXSSFITestDataProvider.instance.writeOutAndReadBack(wb1); + assertTrue(wb1.dispose()); + + EmittingSXSSFWorkbook wb2 = new EmittingSXSSFWorkbook(xssfWb2); + assertEquals(1, wb2.getNumberOfSheets()); + Sheet sheet = wb2.getStreamingSheetAt(0); + assertNotNull(sheet); + assertEquals("S1", sheet.getSheetName()); + assertTrue(wb2.dispose()); + xssfWb2.close(); + xssfWb1.close(); + + wb2.close(); + wb1.close(); + } + + @Test + public void useSharedStringsTable() throws Exception { + // not supported with EmittingSXSSF + } + + @Test + public void addToExistingWorkbook() throws IOException { + XSSFWorkbook xssfWb1 = new XSSFWorkbook(); + xssfWb1.createSheet("S1"); + Sheet sheet = xssfWb1.createSheet("S2"); + Row row = sheet.createRow(1); + Cell cell = row.createCell(1); + cell.setCellValue("value 2_1_1"); + EmittingSXSSFWorkbook wb1 = new EmittingSXSSFWorkbook(xssfWb1); + XSSFWorkbook xssfWb2 = SXSSFITestDataProvider.instance.writeOutAndReadBack(wb1); + assertTrue(wb1.dispose()); + xssfWb1.close(); + + EmittingSXSSFWorkbook wb2 = new EmittingSXSSFWorkbook(xssfWb2); + // Add a row to the existing empty sheet + EmittingSXSSFSheet ssheet1 = wb2.getStreamingSheetAt(0); + ssheet1.setRowGenerator((ssxSheet) -> { + Row row1_1 = ssxSheet.createRow(1); + Cell cell1_1_1 = row1_1.createCell(1); + cell1_1_1.setCellValue("value 1_1_1"); + }); + + // Add a row to the existing non-empty sheet + EmittingSXSSFSheet ssheet2 = wb2.getStreamingSheetAt(1); + ssheet2.setRowGenerator((ssxSheet) -> { + Row row2_2 = ssxSheet.createRow(2); + Cell cell2_2_1 = row2_2.createCell(1); + cell2_2_1.setCellValue("value 2_2_1"); + }); + // Add a sheet with one row + EmittingSXSSFSheet ssheet3 = wb2.createSheet("S3"); + ssheet3.setRowGenerator((ssxSheet) -> { + Row row3_1 = ssxSheet.createRow(1); + Cell cell3_1_1 = row3_1.createCell(1); + cell3_1_1.setCellValue("value 3_1_1"); + }); + + XSSFWorkbook xssfWb3 = SXSSFITestDataProvider.instance.writeOutAndReadBack(wb2); + wb2.close(); + + assertEquals(3, xssfWb3.getNumberOfSheets()); + // Verify sheet 1 + XSSFSheet sheet1 = xssfWb3.getSheetAt(0); + assertEquals("S1", sheet1.getSheetName()); + assertEquals(1, sheet1.getPhysicalNumberOfRows()); + XSSFRow row1_1 = sheet1.getRow(1); + assertNotNull(row1_1); + XSSFCell cell1_1_1 = row1_1.getCell(1); + assertNotNull(cell1_1_1); + assertEquals("value 1_1_1", cell1_1_1.getStringCellValue()); + // Verify sheet 2 + XSSFSheet sheet2 = xssfWb3.getSheetAt(1); + assertEquals("S2", sheet2.getSheetName()); + assertEquals(2, sheet2.getPhysicalNumberOfRows()); + Row row2_1 = sheet2.getRow(1); + assertNotNull(row2_1); + Cell cell2_1_1 = row2_1.getCell(1); + assertNotNull(cell2_1_1); + assertEquals("value 2_1_1", cell2_1_1.getStringCellValue()); + XSSFRow row2_2 = sheet2.getRow(2); + assertNotNull(row2_2); + XSSFCell cell2_2_1 = row2_2.getCell(1); + assertNotNull(cell2_2_1); + assertEquals("value 2_2_1", cell2_2_1.getStringCellValue()); + // Verify sheet 3 + XSSFSheet sheet3 = xssfWb3.getSheetAt(2); + assertEquals("S3", sheet3.getSheetName()); + assertEquals(1, sheet3.getPhysicalNumberOfRows()); + XSSFRow row3_1 = sheet3.getRow(1); + assertNotNull(row3_1); + XSSFCell cell3_1_1 = row3_1.getCell(1); + assertNotNull(cell3_1_1); + assertEquals("value 3_1_1", cell3_1_1.getStringCellValue()); + + xssfWb2.close(); + xssfWb3.close(); + wb1.close(); + } + + @Test + public void sheetdataWriter() throws IOException { + EmittingSXSSFWorkbook wb = new EmittingSXSSFWorkbook(); + SXSSFSheet sh = wb.createSheet(); + assertSame(sh.getClass(), EmittingSXSSFSheet.class); + SheetDataWriter wr = sh.getSheetDataWriter(); + assertNull(wr); + wb.close(); + } + + @Test + public void gzipSheetdataWriter() throws IOException { + EmittingSXSSFWorkbook wb = new EmittingSXSSFWorkbook(); + wb.setCompressTempFiles(true); + + final int rowNum = 1000; + final int sheetNum = 5; + populateData(wb, 1000, 5); + + XSSFWorkbook xwb = SXSSFITestDataProvider.instance.writeOutAndReadBack(wb); + for (int i = 0; i < sheetNum; i++) { + Sheet sh = xwb.getSheetAt(i); + assertEquals("sheet" + i, sh.getSheetName()); + for (int j = 0; j < rowNum; j++) { + Row row = sh.getRow(j); + assertNotNull("row[" + j + "]", row); + Cell cell1 = row.getCell(0); + assertEquals(new CellReference(cell1).formatAsString(), cell1.getStringCellValue()); + + Cell cell2 = row.getCell(1); + assertEquals(i, (int) cell2.getNumericCellValue()); + + Cell cell3 = row.getCell(2); + assertEquals(j, (int) cell3.getNumericCellValue()); + } + } + + assertTrue(wb.dispose()); + xwb.close(); + wb.close(); + } + + private static void assertWorkbookDispose(EmittingSXSSFWorkbook wb) { + populateData(wb, 1000, 5); + + for (Sheet sheet : wb) { + EmittingSXSSFSheet sxSheet = (EmittingSXSSFSheet) sheet; + assertNull(sxSheet.getSheetDataWriter()); + } + + assertTrue(wb.dispose()); + + for (Sheet sheet : wb) { + EmittingSXSSFSheet sxSheet = (EmittingSXSSFSheet) sheet; + assertNull(sxSheet.getSheetDataWriter()); + } + } + + private static void populateData(EmittingSXSSFWorkbook wb, final int rowNum, final int sheetNum) { + for (int i = 0; i < sheetNum; i++) { + EmittingSXSSFSheet sheet = wb.createSheet("sheet" + i); + int index = i; + sheet.setRowGenerator((sh) -> { + for (int j = 0; j < rowNum; j++) { + Row row = sh.createRow(j); + Cell cell1 = row.createCell(0); + cell1.setCellValue(new CellReference(cell1).formatAsString()); + + Cell cell2 = row.createCell(1); + cell2.setCellValue(index); + + Cell cell3 = row.createCell(2); + cell3.setCellValue(j); + } + }); + } + } + + @Test + public void workbookDispose() throws IOException { + EmittingSXSSFWorkbook wb1 = new EmittingSXSSFWorkbook(); + // the underlying writer is SheetDataWriter + assertWorkbookDispose(wb1); + wb1.close(); + + EmittingSXSSFWorkbook wb2 = new EmittingSXSSFWorkbook(); + wb2.setCompressTempFiles(true); + // the underlying writer is GZIPSheetDataWriter + assertWorkbookDispose(wb2); + wb2.close(); + } + + @Ignore("currently writing the same sheet multiple times is not supported...") + @Test + public void bug53515() throws Exception { + Workbook wb1 = new SXSSFWorkbook(10); + populateWorkbook(wb1); + saveTwice(wb1); + Workbook wb2 = new XSSFWorkbook(); + populateWorkbook(wb2); + saveTwice(wb2); + wb2.close(); + wb1.close(); + } + + @Ignore("Crashes the JVM because of documented JVM behavior with concurrent writing/reading of zip-files, " + + "see http://www.oracle.com/technetwork/java/javase/documentation/overview-156328.html") + @Test + public void bug53515a() throws Exception { + File out = new File("Test.xlsx"); + assertTrue(!out.exists() || out.delete()); + for (int i = 0; i < 2; i++) { + final SXSSFWorkbook wb; + if (out.exists()) { + wb = new SXSSFWorkbook((XSSFWorkbook) WorkbookFactory.create(out)); + } else { + wb = new SXSSFWorkbook(10); + } + + try { + FileOutputStream outSteam = new FileOutputStream(out); + if (i == 0) { + populateWorkbook(wb); + } else { + System.gc(); + System.gc(); + System.gc(); + } + + wb.write(outSteam); + // assertTrue(wb.dispose()); + outSteam.close(); + } finally { + assertTrue(wb.dispose()); + } + wb.close(); + } + assertTrue(out.exists()); + assertTrue(out.delete()); + } + + private static void populateWorkbook(Workbook wb) { + Sheet sh = wb.createSheet(); + for (int rownum = 0; rownum < 100; rownum++) { + Row row = sh.createRow(rownum); + for (int cellnum = 0; cellnum < 10; cellnum++) { + Cell cell = row.createCell(cellnum); + String address = new CellReference(cell).formatAsString(); + cell.setCellValue(address); + } + } + } + + private static void saveTwice(Workbook wb) throws Exception { + for (int i = 0; i < 2; i++) { + try { + NullOutputStream out = new NullOutputStream(); + wb.write(out); + out.close(); + } catch (Exception e) { + throw new Exception("ERROR: failed on " + (i + 1) + "th time calling " + wb.getClass().getName() + + ".write() with exception " + e.getMessage(), e); + } + } + } + + @Test + public void closeDoesNotModifyWorkbook() throws IOException { + final String filename = "SampleSS.xlsx"; + final File file = POIDataSamples.getSpreadSheetInstance().getFile(filename); + + // Some tests commented out because close() modifies the file + // See bug 58779 + + // String + // wb = new SXSSFWorkbook(new XSSFWorkbook(file.getPath())); + // assertCloseDoesNotModifyFile(filename, wb); + + // File + // wb = new SXSSFWorkbook(new XSSFWorkbook(file)); + // assertCloseDoesNotModifyFile(filename, wb); + + // InputStream + + try (FileInputStream fis = new FileInputStream(file); + XSSFWorkbook xwb = new XSSFWorkbook(fis); + SXSSFWorkbook wb = new SXSSFWorkbook(xwb)) { + assertCloseDoesNotModifyFile(filename, wb); + } + + // OPCPackage + // wb = new SXSSFWorkbook(new XSSFWorkbook(OPCPackage.open(file))); + // assertCloseDoesNotModifyFile(filename, wb); + } + + /** + * Bug #59743 + * + * this is only triggered on other files apart of sheet[1,2,...].xml as those are either copied uncompressed or with + * the use of GZIPInputStream so we use shared strings + */ + @Test + public void testZipBombNotTriggeredOnUselessContent() throws IOException { + SXSSFWorkbook swb = new SXSSFWorkbook(null, 1, true, true); + SXSSFSheet s = swb.createSheet(); + char[] useless = new char[32767]; + Arrays.fill(useless, ' '); + + for (int row = 0; row < 1; row++) { + Row r = s.createRow(row); + for (int col = 0; col < 10; col++) { + char[] prefix = Integer.toHexString(row * 1000 + col).toCharArray(); + Arrays.fill(useless, 0, 10, ' '); + System.arraycopy(prefix, 0, useless, 0, prefix.length); + String ul = new String(useless); + r.createCell(col, CellType.STRING).setCellValue(ul); + } + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + swb.write(bos); + swb.dispose(); + swb.close(); + } + + /** + * To avoid accident changes to the template, you should be able to create a SXSSFWorkbook from a read-only XSSF + * one, then change + save that (only). See bug #60010 TODO Fix this to work! + */ + @Test + @Ignore + public void createFromReadOnlyWorkbook() throws Exception { + String sheetName = "Test SXSSF"; + File input = XSSFTestDataSamples.getSampleFile("sample.xlsx"); + + try (OPCPackage pkg = OPCPackage.open(input, PackageAccess.READ)) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (XSSFWorkbook xssf = new XSSFWorkbook(pkg)) { + try (SXSSFWorkbook wb = new SXSSFWorkbook(xssf, 2)) { + Sheet s = wb.createSheet(sheetName); + for (int i = 0; i < 10; i++) { + Row r = s.createRow(i); + r.createCell(0).setCellValue(true); + r.createCell(1).setCellValue(2.4); + r.createCell(2).setCellValue("Test Row " + i); + } + assertEquals(10, s.getLastRowNum()); + + wb.write(bos); + wb.dispose(); + } + } + + try (XSSFWorkbook xssf = new XSSFWorkbook(new ByteArrayInputStream(bos.toByteArray()))) { + Sheet s = xssf.getSheet(sheetName); + assertEquals(10, s.getLastRowNum()); + assertTrue(s.getRow(0).getCell(0).getBooleanCellValue()); + assertEquals("Test Row 9", s.getRow(9).getCell(2).getStringCellValue()); + } + } + } + + @Test + public void test56557() throws IOException { + Workbook wb = XSSFTestDataSamples.openSampleWorkbook("56557.xlsx"); + + // Using streaming XSSFWorkbook makes the output file invalid + wb = new SXSSFWorkbook(((XSSFWorkbook) wb)); + + // Should not throw POIXMLException: java.io.IOException: Unable to parse xml bean when reading back + Workbook wbBack = XSSFTestDataSamples.writeOutAndReadBack(wb); + assertNotNull(wbBack); + wbBack.close(); + + wb.close(); + } +}