| /* ==================================================================== |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| ==================================================================== */ |
| |
| package org.apache.poi.hssf.record.aggregates; |
| |
| import static org.junit.jupiter.api.Assertions.assertArrayEquals; |
| import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; |
| import static org.junit.jupiter.api.Assertions.assertEquals; |
| import static org.junit.jupiter.api.Assertions.assertNotEquals; |
| import static org.junit.jupiter.api.Assertions.assertNotNull; |
| import static org.junit.jupiter.api.Assertions.assertTrue; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.zip.CRC32; |
| |
| import org.apache.poi.hssf.HSSFTestDataSamples; |
| import org.apache.poi.hssf.model.RecordStream; |
| import org.apache.poi.hssf.model.RowBlocksReader; |
| import org.apache.poi.hssf.record.BlankRecord; |
| import org.apache.poi.hssf.record.CellValueRecordInterface; |
| import org.apache.poi.hssf.record.FormulaRecord; |
| import org.apache.poi.hssf.record.MulBlankRecord; |
| import org.apache.poi.hssf.record.Record; |
| import org.apache.poi.hssf.record.SharedFormulaRecord; |
| import org.apache.poi.hssf.record.WindowTwoRecord; |
| import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor; |
| import org.apache.poi.hssf.usermodel.HSSFRow; |
| import org.apache.poi.hssf.usermodel.HSSFSheet; |
| import org.apache.poi.hssf.usermodel.HSSFWorkbook; |
| import org.apache.poi.util.HexRead; |
| import org.junit.jupiter.api.Test; |
| |
| /** |
| * Tests for {@link ValueRecordsAggregate} |
| */ |
| final class TestValueRecordsAggregate { |
| private static final String ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE = "AbnormalSharedFormulaFlag.xls"; |
| private final ValueRecordsAggregate valueRecord = new ValueRecordsAggregate(); |
| |
| private List<CellValueRecordInterface> getValueRecords() { |
| List<CellValueRecordInterface> list = new ArrayList<>(); |
| for ( CellValueRecordInterface rec : valueRecord ) { |
| list.add(rec); |
| } |
| return Collections.unmodifiableList(list); |
| } |
| |
| /** |
| * Make sure the shared formula DOESNT makes it to the FormulaRecordAggregate when being parsed |
| * as part of the value records |
| */ |
| @Test |
| void testSharedFormula() { |
| List<org.apache.poi.hssf.record.Record> records = new ArrayList<>(); |
| records.add(new FormulaRecord()); |
| records.add(new SharedFormulaRecord()); |
| records.add(new WindowTwoRecord()); |
| |
| constructValueRecord(records); |
| List<CellValueRecordInterface> cvrs = getValueRecords(); |
| //Ensure that the SharedFormulaRecord has been converted |
| assertEquals(1, cvrs.size()); |
| |
| CellValueRecordInterface record = cvrs.get(0); |
| assertNotNull( record, "Row contains a value" ); |
| assertTrue( ( record instanceof FormulaRecordAggregate ), "First record is a FormulaRecordsAggregate"); |
| } |
| |
| private void constructValueRecord(List<org.apache.poi.hssf.record.Record> records) { |
| RowBlocksReader rbr = new RowBlocksReader(new RecordStream(records, 0)); |
| SharedValueManager sfrh = rbr.getSharedFormulaManager(); |
| RecordStream rs = rbr.getPlainRecordStream(); |
| while(rs.hasNext()) { |
| Record rec = rs.getNext(); |
| valueRecord.construct((CellValueRecordInterface)rec, rs, sfrh); |
| } |
| } |
| |
| private static List<org.apache.poi.hssf.record.Record> testData() { |
| List<org.apache.poi.hssf.record.Record> records = new ArrayList<>(); |
| FormulaRecord formulaRecord = new FormulaRecord(); |
| BlankRecord blankRecord = new BlankRecord(); |
| formulaRecord.setRow(1); |
| formulaRecord.setColumn((short) 1); |
| blankRecord.setRow(2); |
| blankRecord.setColumn((short) 2); |
| records.add(formulaRecord); |
| records.add(blankRecord); |
| records.add(new WindowTwoRecord()); |
| return records; |
| } |
| |
| @Test |
| void testInsertCell() { |
| assertEquals(0, getValueRecords().size()); |
| |
| BlankRecord blankRecord = newBlankRecord(); |
| valueRecord.insertCell( blankRecord ); |
| assertEquals(1, getValueRecords().size()); |
| } |
| |
| @Test |
| void testRemoveCell() { |
| BlankRecord blankRecord1 = newBlankRecord(); |
| valueRecord.insertCell( blankRecord1 ); |
| BlankRecord blankRecord2 = newBlankRecord(); |
| valueRecord.removeCell( blankRecord2 ); |
| assertEquals(0, getValueRecords().size()); |
| |
| // removing an already empty cell just falls through |
| valueRecord.removeCell( blankRecord2 ); |
| } |
| |
| @Test |
| void testGetPhysicalNumberOfCells() { |
| assertEquals(0, valueRecord.getPhysicalNumberOfCells()); |
| BlankRecord blankRecord1 = newBlankRecord(); |
| valueRecord.insertCell( blankRecord1 ); |
| assertEquals(1, valueRecord.getPhysicalNumberOfCells()); |
| valueRecord.removeCell( blankRecord1 ); |
| assertEquals(0, valueRecord.getPhysicalNumberOfCells()); |
| } |
| |
| @Test |
| void testGetFirstCellNum() { |
| assertEquals( -1, valueRecord.getFirstCellNum() ); |
| valueRecord.insertCell( newBlankRecord( 2, 2 ) ); |
| assertEquals( 2, valueRecord.getFirstCellNum() ); |
| valueRecord.insertCell( newBlankRecord( 3, 3 ) ); |
| assertEquals( 2, valueRecord.getFirstCellNum() ); |
| |
| // Note: Removal doesn't currently reset the first column. It probably should but it doesn't. |
| valueRecord.removeCell( newBlankRecord( 2, 2 ) ); |
| assertEquals( 2, valueRecord.getFirstCellNum() ); |
| } |
| |
| @Test |
| void testGetLastCellNum() { |
| assertEquals( -1, valueRecord.getLastCellNum() ); |
| valueRecord.insertCell( newBlankRecord( 2, 2 ) ); |
| assertEquals( 2, valueRecord.getLastCellNum() ); |
| valueRecord.insertCell( newBlankRecord( 3, 3 ) ); |
| assertEquals( 3, valueRecord.getLastCellNum() ); |
| |
| // Note: Removal doesn't currently reset the last column. It probably should but it doesn't. |
| valueRecord.removeCell( newBlankRecord( 3, 3 ) ); |
| assertEquals( 3, valueRecord.getLastCellNum() ); |
| |
| } |
| |
| |
| private static final class SerializerVisitor implements RecordVisitor { |
| private final byte[] _buf; |
| private int _writeIndex; |
| public SerializerVisitor(byte[] buf) { |
| _buf = buf; |
| _writeIndex = 0; |
| |
| } |
| @Override |
| public void visitRecord(org.apache.poi.hssf.record.Record r) { |
| r.serialize(_writeIndex, _buf); |
| _writeIndex += r.getRecordSize(); |
| } |
| public int getWriteIndex() { |
| return _writeIndex; |
| } |
| } |
| |
| @Test |
| void testSerialize() { |
| byte[] expectedArray = HexRead.readFromString("" |
| + "06 00 16 00 " // Formula |
| + "01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " |
| + "01 02 06 00 " // Blank |
| + "02 00 02 00 00 00"); |
| byte[] actualArray = new byte[expectedArray.length]; |
| List<org.apache.poi.hssf.record.Record> records = testData(); |
| constructValueRecord(records); |
| |
| SerializerVisitor sv = new SerializerVisitor(actualArray); |
| valueRecord.visitCellsForRow(1, sv); |
| valueRecord.visitCellsForRow(2, sv); |
| assertEquals(actualArray.length, sv.getWriteIndex()); |
| assertArrayEquals(expectedArray, actualArray); |
| } |
| |
| private static BlankRecord newBlankRecord() { |
| return newBlankRecord( 2, 2 ); |
| } |
| |
| private static BlankRecord newBlankRecord(int col, int row) { |
| BlankRecord blankRecord = new BlankRecord(); |
| blankRecord.setRow( row ); |
| blankRecord.setColumn( (short) col ); |
| return blankRecord; |
| } |
| |
| /** |
| * Sometimes the 'shared formula' flag (<tt>FormulaRecord.isSharedFormula()</tt>) is set when |
| * there is no corresponding SharedFormulaRecord available. SharedFormulaRecord definitions do |
| * not span multiple sheets. They are are only defined within a sheet, and thus they do not |
| * have a sheet index field (only row and column range fields).<br> |
| * So it is important that the code which locates the SharedFormulaRecord for each |
| * FormulaRecord does not allow matches across sheets.</br> |
| * |
| * Prior to bugzilla 44449 (Feb 2008), POI <tt>ValueRecordsAggregate.construct(int, List)</tt> |
| * allowed <tt>SharedFormulaRecord</tt>s to be erroneously used across sheets. That incorrect |
| * behaviour is shown by this test.<p> |
| * |
| * <b>Notes on how to produce the test spreadsheet</b>:</p> |
| * The setup for this test (AbnormalSharedFormulaFlag.xls) is rather fragile, insomuchas |
| * re-saving the file (either with Excel or POI) clears the flag.<br> |
| * <ol> |
| * <li>A new spreadsheet was created in Excel (File | New | Blank Workbook).</li> |
| * <li>Sheet3 was deleted.</li> |
| * <li>Sheet2!A1 formula was set to '="second formula"', and fill-dragged through A1:A8.</li> |
| * <li>Sheet1!A1 formula was set to '="first formula"', and also fill-dragged through A1:A8.</li> |
| * <li>Four rows on Sheet1 "5" through "8" were deleted ('delete rows' alt-E D, not 'clear' Del).</li> |
| * <li>The spreadsheet was saved as AbnormalSharedFormulaFlag.xls.</li> |
| * </ol> |
| * Prior to the row delete action the spreadsheet has two <tt>SharedFormulaRecord</tt>s. One |
| * for each sheet. To expose the bug, the shared formulas have been made to overlap.<br> |
| * The row delete action (as described here) seems to to delete the |
| * <tt>SharedFormulaRecord</tt> from Sheet1 (but not clear the 'shared formula' flags.<br> |
| * There are other variations on this theme to create the same effect. |
| * |
| */ |
| @Test |
| void testSpuriousSharedFormulaFlag() throws Exception { |
| |
| long actualCRC = getFileCRC(HSSFTestDataSamples.openSampleFileStream(ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE)); |
| long expectedCRC = 2277445406L; |
| if(actualCRC != expectedCRC) { |
| System.err.println("Expected crc " + expectedCRC + " but got " + actualCRC); |
| throw failUnexpectedTestFileChange(); |
| } |
| HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook(ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE); |
| |
| HSSFSheet s = wb.getSheetAt(0); // Sheet1 |
| |
| String cellFormula; |
| cellFormula = getFormulaFromFirstCell(s, 0); // row "1" |
| // the problem is not observable in the first row of the shared formula |
| assertEquals("\"first formula\"", cellFormula, "Something else wrong with this test case"); |
| |
| // but the problem is observable in rows 2,3,4 |
| cellFormula = getFormulaFromFirstCell(s, 1); // row "2" |
| assertNotEquals("\"second formula\"", cellFormula, "found bug 44449 (Wrong SharedFormulaRecord was used)."); |
| |
| assertEquals("\"first formula\"", cellFormula, "Something else wrong with this test case"); |
| |
| wb.close(); |
| } |
| private static String getFormulaFromFirstCell(HSSFSheet s, int rowIx) { |
| return s.getRow(rowIx).getCell(0).getCellFormula(); |
| } |
| |
| /** |
| * If someone opened this particular test file in Excel and saved it, the peculiar condition |
| * which causes the target bug would probably disappear. This test would then just succeed |
| * regardless of whether the fix was present. So a CRC check is performed to make it less easy |
| * for that to occur. |
| */ |
| private static RuntimeException failUnexpectedTestFileChange() { |
| String msg = "Test file '" + ABNORMAL_SHARED_FORMULA_FLAG_TEST_FILE + "' has changed. " |
| + "This junit may not be properly testing for the target bug. " |
| + "Either revert the test file or ensure that the new version " |
| + "has the right characteristics to test the target bug."; |
| // A breakpoint in ValueRecordsAggregate.handleMissingSharedFormulaRecord(FormulaRecord) |
| // should get hit during parsing of Sheet1. |
| // If the test spreadsheet is created as directed, this condition should occur. |
| // It is easy to upset the test spreadsheet (for example re-saving will destroy the |
| // peculiar condition we are testing for). |
| throw new RuntimeException(msg); |
| } |
| |
| /** |
| * gets a CRC checksum for the content of a file |
| */ |
| private static long getFileCRC(InputStream is) { |
| CRC32 crc = new CRC32(); |
| byte[] buf = new byte[2048]; |
| try { |
| while(true) { |
| int bytesRead = is.read(buf); |
| if(bytesRead < 1) { |
| break; |
| } |
| crc.update(buf, 0, bytesRead); |
| } |
| is.close(); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| |
| return crc.getValue(); |
| } |
| |
| @Test |
| void testRemoveNewRow_bug46312() throws IOException { |
| // To make bug occur, rowIndex needs to be >= ValueRecordsAggregate.records.length |
| int rowIndex = 30; |
| |
| ValueRecordsAggregate vra = new ValueRecordsAggregate(); |
| // bug 46312 - Specified rowIndex 30 is outside the allowable range (0..30) |
| assertDoesNotThrow(() -> vra.removeAllCellsValuesForRow(rowIndex)); |
| |
| // same bug as demonstrated through usermodel API |
| try (HSSFWorkbook wb = new HSSFWorkbook()) { |
| HSSFSheet sheet = wb.createSheet(); |
| HSSFRow row = sheet.createRow(rowIndex); |
| |
| // must not add any cells to the new row if we want to see the bug |
| // row.createCell(0); // this causes ValueRecordsAggregate.records to auto-extend |
| assertDoesNotThrow(() -> sheet.createRow(rowIndex)); |
| } |
| } |
| |
| /** |
| * Tests various manipulations of blank cells, to make sure that {@link MulBlankRecord}s |
| * are use appropriately |
| */ |
| @Test |
| void testMultipleBlanks() { |
| BlankRecord brA2 = newBlankRecord(0, 1); |
| BlankRecord brB2 = newBlankRecord(1, 1); |
| BlankRecord brC2 = newBlankRecord(2, 1); |
| BlankRecord brD2 = newBlankRecord(3, 1); |
| BlankRecord brE2 = newBlankRecord(4, 1); |
| BlankRecord brB3 = newBlankRecord(1, 2); |
| BlankRecord brC3 = newBlankRecord(2, 2); |
| |
| valueRecord.insertCell(brA2); |
| valueRecord.insertCell(brB2); |
| valueRecord.insertCell(brD2); |
| confirmMulBlank(3, 1, 1); |
| |
| valueRecord.insertCell(brC3); |
| confirmMulBlank(4, 1, 2); |
| |
| valueRecord.insertCell(brB3); |
| valueRecord.insertCell(brE2); |
| confirmMulBlank(6, 3, 0); |
| |
| valueRecord.insertCell(brC2); |
| confirmMulBlank(7, 2, 0); |
| |
| valueRecord.removeCell(brA2); |
| confirmMulBlank(6, 2, 0); |
| |
| valueRecord.removeCell(brC2); |
| confirmMulBlank(5, 2, 1); |
| |
| valueRecord.removeCell(brC3); |
| confirmMulBlank(4, 1, 2); |
| } |
| |
| private void confirmMulBlank(int expectedTotalBlankCells, |
| int expectedNumberOfMulBlankRecords, int expectedNumberOfSingleBlankRecords) { |
| // assumed row ranges set-up by caller: |
| final int firstRow = 1; |
| final int lastRow = 2; |
| |
| |
| final class BlankStats { |
| public int countBlankCells; |
| public int countMulBlankRecords; |
| public int countSingleBlankRecords; |
| } |
| |
| final BlankStats bs = new BlankStats(); |
| RecordVisitor rv = r -> { |
| if (r instanceof MulBlankRecord) { |
| MulBlankRecord mbr = (MulBlankRecord) r; |
| bs.countMulBlankRecords++; |
| bs.countBlankCells += mbr.getNumColumns(); |
| } else if (r instanceof BlankRecord) { |
| bs.countSingleBlankRecords++; |
| bs.countBlankCells++; |
| } |
| }; |
| |
| for (int rowIx = firstRow; rowIx <=lastRow; rowIx++) { |
| if (valueRecord.rowHasCells(rowIx)) { |
| valueRecord.visitCellsForRow(rowIx, rv); |
| } |
| } |
| assertEquals(expectedTotalBlankCells, bs.countBlankCells); |
| assertEquals(expectedNumberOfMulBlankRecords, bs.countMulBlankRecords); |
| assertEquals(expectedNumberOfSingleBlankRecords, bs.countSingleBlankRecords); |
| } |
| } |