From 18f9a3e88e27fdac034734d1ce62959fb0d8df77 Mon Sep 17 00:00:00 2001 From: Dominik Stadler Date: Mon, 2 Dec 2013 19:14:03 +0000 Subject: [PATCH] Bug 55640, added reproducer, a fix for the Exception cases and some verify-tests to ensure future changes are checked via unit tests. There might be more work pending to make grouping fully work the same way as Excel does. git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1547153 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/xssf/usermodel/XSSFSheet.java | 15 +- .../usermodel/TestXSSFSheetRowGrouping.java | 441 ++++++++++++++++++ test-data/spreadsheet/55640.xlsx | Bin 0 -> 6184 bytes test-data/spreadsheet/GroupTest.xlsx | Bin 0 -> 8149 bytes 4 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetRowGrouping.java create mode 100644 test-data/spreadsheet/55640.xlsx create mode 100644 test-data/spreadsheet/GroupTest.xlsx diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java index 0ebe113a26..afa56a5771 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java @@ -2154,6 +2154,12 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { int level = xRow.getCTRow().getOutlineLevel(); for (Iterator it = rowIterator(); it.hasNext();) { xRow = (XSSFRow) it.next(); + + // skip rows before the start of this group + if(xRow.getRowNum() < rowIndex) { + continue; + } + if (xRow.getCTRow().getOutlineLevel() >= level) { xRow.getCTRow().setHidden(hidden); rowIndex++; @@ -2171,8 +2177,9 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { return; XSSFRow row = getRow(rowNumber); // If it is already expanded do nothing. - if (!row.getCTRow().isSetHidden()) + if (!row.getCTRow().isSetHidden()) { return; + } // Find the start of the group. int startIdx = findStartOfRowOutlineGroup(rowNumber); @@ -2202,7 +2209,11 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { } } // Write collapse field - getRow(endIdx).getCTRow().unsetCollapsed(); + CTRow ctRow = getRow(endIdx).getCTRow(); + // This avoids an IndexOutOfBounds if multiple nested groups are collapsed/expanded + if(ctRow.getCollapsed()) { + ctRow.unsetCollapsed(); + } } /** diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetRowGrouping.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetRowGrouping.java new file mode 100644 index 0000000000..cb8fc60c0e --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFSheetRowGrouping.java @@ -0,0 +1,441 @@ +/* ==================================================================== + 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.usermodel; + +import java.io.IOException; + +import junit.framework.TestCase; + +import org.apache.poi.ss.usermodel.Cell; +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.xssf.XSSFTestDataSamples; + +public final class TestXSSFSheetRowGrouping extends TestCase { + + private static final int ROWS_NUMBER = 200; + private static final int GROUP_SIZE = 5; + + //private int o_groupsNumber = 0; + + public void test55640() throws IOException { + //long startTime = System.currentTimeMillis(); + Workbook wb = new XSSFWorkbook(); + fillData(wb); + writeToFile(wb); + + //System.out.println("Number of groups: " + o_groupsNumber); + //System.out.println("Execution time: " + (System.currentTimeMillis()-startTime) + " ms"); + } + + + private void fillData(Workbook p_wb) { + Sheet sheet = p_wb.createSheet("sheet123"); + sheet.setRowSumsBelow(false); + + for (int i = 0; i < ROWS_NUMBER; i++) { + Row row = sheet.createRow(i); + Cell cell = row.createCell(0); + cell.setCellValue(i+1); + } + + int i = 1; + while (i < ROWS_NUMBER) { + int end = i+(GROUP_SIZE-2); + int start = i; // natural order +// int start = end - 1; // reverse order + while (start < end) { // natural order +// while (start >= i) { // reverse order + sheet.groupRow(start, end); + //o_groupsNumber++; + boolean collapsed = isCollapsed(); + //System.out.println("Set group " + start + "->" + end + " to " + collapsed); + sheet.setRowGroupCollapsed(start, collapsed); + start++; // natural order +// start--; // reverse order + } + i += GROUP_SIZE; + } + } + + private boolean isCollapsed() { + return Math.random() > 0.5d; + } + + private void writeToFile(Workbook p_wb) throws IOException { +// FileOutputStream fileOut = new FileOutputStream("/tmp/55640.xlsx"); +// try { +// p_wb.write(fileOut); +// } finally { +// fileOut.close(); +// } + assertNotNull(XSSFTestDataSamples.writeOutAndReadBack(p_wb)); + } + + public void test55640reduce1() throws IOException { + Workbook wb = new XSSFWorkbook(); + Sheet sheet = wb.createSheet("sheet123"); + sheet.setRowSumsBelow(false); + + for (int i = 0; i < ROWS_NUMBER; i++) { + Row row = sheet.createRow(i); + Cell cell = row.createCell(0); + cell.setCellValue(i+1); + } + + int i = 1; + while (i < ROWS_NUMBER) { + int end = i+(GROUP_SIZE-2); + int start = i; // natural order + while (start < end) { // natural order + sheet.groupRow(start, end); + //o_groupsNumber++; + boolean collapsed = start % 2 == 0 ? false : true; + //System.out.println("Set group " + start + "->" + end + " to " + collapsed); + sheet.setRowGroupCollapsed(start, collapsed); + start++; // natural order + } + i += GROUP_SIZE; + } + writeToFile(wb); + } + + + public void test55640_VerifyCases() throws IOException { + // NOTE: This is currently based on current behavior of POI, somehow + // what POI returns in the calls to collapsed/hidden is not fully matching + // the examples in the spec or I did not fully understand how POI stores the data internally... + + // all expanded + verifyGroupCollapsed( + // level1, level2, level3 + false, false, false, + // collapsed: + new Boolean[] { false, false, false, false, false}, + // hidden: + new boolean[] { false, false, false, false, false}, + // outlineLevel + new int[] { 1, 2, 3, 3, 3 } + ); + + + // Level 1 collapsed, others expanded, should only have 4 rows, all hidden: + verifyGroupCollapsed( + // level1, level2, level3 + true, false, false, + // collapsed: + new Boolean[] { false, false, false, false, false}, + // hidden: + new boolean[] { true, true, true, true, true}, + // outlineLevel + new int[] { 1, 2, 3, 3, 3 } + ); + + // Level 1 and 2 collapsed, Level 3 expanded, + verifyGroupCollapsed( + // level1, level2, level3 + true, true, false, + // collapsed: + new Boolean[] { false, false, false, false, true, false}, + // hidden: + new boolean[] { true, true, true, true, true, false}, + // outlineLevel + new int[] { 1, 2, 3, 3, 3, 0 } + ); + + // Level 1 collapsed, Level 2 expanded, Level 3 collapsed + verifyGroupCollapsed( + // level1, level2, level3 + true, false, true, + // collapsed: + new Boolean[] { false, false, false, false, false, true}, + // hidden: + new boolean[] { true, true, true, true, true, false}, + // outlineLevel + new int[] { 1, 2, 3, 3, 3, 0 } + ); + + // Level 2 collapsed, others expanded: + verifyGroupCollapsed( + // level1, level2, level3 + false, true, false, + // collapsed: + new Boolean[] { false, false, false, false, false, false}, + // hidden: + new boolean[] { false, true, true, true, true, false}, + // outlineLevel + new int[] { 1, 2, 3, 3, 3, 0 } + ); + + // Level 3 collapsed, others expanded + verifyGroupCollapsed( + // level1, level2, level3 + false, false, true, + // collapsed: + new Boolean[] { false, false, false, false, false, true}, + // hidden: + new boolean[] { false, false, true, true, true, false}, + // outlineLevel + new int[] { 1, 2, 3, 3, 3, 0 } + ); + + // All collapsed + verifyGroupCollapsed( + // level1, level2, level3 + true, true, true, + // collapsed: + new Boolean[] { false, false, false, false, true, true}, + // hidden: + new boolean[] { true, true, true, true, true, false}, + // outlineLevel + new int[] { 1, 2, 3, 3, 3, 0 } + ); + } + + + private void verifyGroupCollapsed(boolean level1, boolean level2, boolean level3, + Boolean[] collapsed, boolean[] hidden, int[] outlineLevel) throws IOException { + Workbook wb = new XSSFWorkbook(); + Sheet sheet = wb.createSheet("sheet123"); + + for (int i = 0; i < 4; i++) { + sheet.createRow(i); + } + + sheet.groupRow(0, 4); + sheet.groupRow(1, 4); + sheet.groupRow(2, 4); + + sheet.setRowGroupCollapsed(0, level1); + sheet.setRowGroupCollapsed(1, level2); + sheet.setRowGroupCollapsed(2, level3); + + checkWorkbookGrouping(wb, collapsed, hidden, outlineLevel); + } + + public void test55640_VerifyCasesSpec() throws IOException { + // NOTE: This is currently based on current behavior of POI, somehow + // what POI returns in the calls to collapsed/hidden is not fully matching + // the examples in the spec or I did not fully understand how POI stores the data internally... + + // all expanded + verifyGroupCollapsedSpec( + // level3, level2, level1 + false, false, false, + // collapsed: + new Boolean[] { false, false, false, false}, + // hidden: + new boolean[] { false, false, false, false}, + // outlineLevel + new int[] { 3, 3, 2, 1 } + ); + + + verifyGroupCollapsedSpec( + // level3, level2, level1 + false, false, true, + // collapsed: + new Boolean[] { false, false, false, true}, + // hidden: + new boolean[] { true, true, true, false}, + // outlineLevel + new int[] { 3, 3, 2, 1 } + ); + + verifyGroupCollapsedSpec( + // level3, level2, level1 + false, true, false, + // collapsed: + new Boolean[] { false, false, true, false}, + // hidden: + new boolean[] { true, true, true, false}, + // outlineLevel + new int[] { 3, 3, 2, 1 } + ); + + verifyGroupCollapsedSpec( + // level3, level2, level1 + false, true, true, + // collapsed: + new Boolean[] { false, false, true, true}, + // hidden: + new boolean[] { true, true, true, false}, + // outlineLevel + new int[] { 3, 3, 2, 1 } + ); + } + + private void verifyGroupCollapsedSpec(boolean level1, boolean level2, boolean level3, + Boolean[] collapsed, boolean[] hidden, int[] outlineLevel) throws IOException { + Workbook wb = new XSSFWorkbook(); + Sheet sheet = wb.createSheet("sheet123"); + + for (int i = 5; i < 9; i++) { + sheet.createRow(i); + } + + sheet.groupRow(5, 6); + sheet.groupRow(5, 7); + sheet.groupRow(5, 8); + + sheet.setRowGroupCollapsed(6, level1); + sheet.setRowGroupCollapsed(7, level2); + sheet.setRowGroupCollapsed(8, level3); + + checkWorkbookGrouping(wb, collapsed, hidden, outlineLevel); + } + + private void checkWorkbookGrouping(Workbook wb, Boolean[] collapsed, boolean[] hidden, int[] outlineLevel) throws IOException { + printWorkbook(wb); + Sheet sheet = wb.getSheetAt(0); + + assertEquals(collapsed.length, hidden.length); + assertEquals(collapsed.length, outlineLevel.length); + assertEquals("Expected " + collapsed.length + " rows with collapsed state, but had " + (sheet.getLastRowNum()-sheet.getFirstRowNum()+1) + " rows (" + + sheet.getFirstRowNum() + "-" + sheet.getLastRowNum() + ")", + collapsed.length, sheet.getLastRowNum()-sheet.getFirstRowNum()+1); + for(int i = sheet.getFirstRowNum(); i < sheet.getLastRowNum();i++) { + if(collapsed[i-sheet.getFirstRowNum()] == null) { + continue; + } + XSSFRow row = (XSSFRow) sheet.getRow(i); + assertNotNull("Could not read row " + i, row); + assertNotNull("Could not read row " + i, row.getCTRow()); + assertEquals("Row: " + i + ": collapsed", collapsed[i-sheet.getFirstRowNum()].booleanValue(), row.getCTRow().getCollapsed()); + assertEquals("Row: " + i + ": hidden", hidden[i-sheet.getFirstRowNum()], row.getCTRow().getHidden()); + + assertEquals("Row: " + i + ": level", outlineLevel[i-sheet.getFirstRowNum()], row.getCTRow().getOutlineLevel()); + } + + writeToFile(wb); + } + + + public void test55640working() throws IOException { + Workbook wb = new XSSFWorkbook(); + Sheet sheet = wb.createSheet("sheet123"); + + sheet.groupRow(1, 4); + sheet.groupRow(2, 5); + sheet.groupRow(3, 6); + + sheet.setRowGroupCollapsed(1, true); + sheet.setRowGroupCollapsed(2, false); + sheet.setRowGroupCollapsed(3, false); + + writeToFile(wb); + } + + // just used for printing out contents of spreadsheets + public void notRuntest55640printSample() { + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("55640.xlsx"); + printWorkbook(wb); + + wb = XSSFTestDataSamples.openSampleWorkbook("GroupTest.xlsx"); + printWorkbook(wb); + } + + private void printWorkbook(Workbook wb) { + // disable all output for now... +// Sheet sheet = wb.getSheetAt(0); +// +// for(Iterator it = sheet.rowIterator();it.hasNext();) { +// XSSFRow row = (XSSFRow) it.next(); +// boolean collapsed = row.getCTRow().getCollapsed(); +// boolean hidden = row.getCTRow().getHidden(); +// short level = row.getCTRow().getOutlineLevel(); +// +// System.out.println("Row: " + row.getRowNum() + ": Level: " + level + " Collapsed: " + collapsed + " Hidden: " + hidden); +// } + } + + public void testGroupingTest() throws IOException { + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("GroupTest.xlsx"); + + assertEquals(31, wb.getSheetAt(0).getLastRowNum()); + + // NOTE: This is currently based on current behavior of POI, somehow + // what POI returns in the calls to collapsed/hidden is not fully matching + // the examples in the spec or I did not fully understand how POI stores the data internally... + checkWorkbookGrouping(wb, + new Boolean [] { + // 0-4 + false, false, false, false, false, null, null, + // 7-11 + false, false, true, true, true, null, null, + // 14-18 + false, false, true, false, false, null, + // 20-24 + false, false, true, true, false, null, null, + // 27-31 + false, false, false, true, false }, + new boolean[] { + // 0-4 + false, false, false, false, false, false, false, + // 7-11 + true, true, true, true, false, false, false, + // 14-18 + true, true, false, false, false, false, + // 20-24 + true, true, true, false, false, false, false, + // 27-31 + true, true, true, true, false }, + // outlineLevel + new int[] { + // 0-4 + 3, 3, 2, 1, 0, 0, 0, + // 7-11 + 3, 3, 2, 1, 0, 0, 0, + // 14-18 + 3, 3, 2, 1, 0, 0, + // 20-24 + 3, 3, 2, 1, 0, 0, 0, + // 27-31 + 3, 3, 2, 1, 0, + } + ); + /* +Row: 0: Level: 3 Collapsed: false Hidden: false +Row: 1: Level: 3 Collapsed: false Hidden: false +Row: 2: Level: 2 Collapsed: false Hidden: false +Row: 3: Level: 1 Collapsed: false Hidden: false +Row: 4: Level: 0 Collapsed: false Hidden: false +Row: 7: Level: 3 Collapsed: false Hidden: true +Row: 8: Level: 3 Collapsed: false Hidden: true +Row: 9: Level: 2 Collapsed: true Hidden: true +Row: 10: Level: 1 Collapsed: true Hidden: true +Row: 11: Level: 0 Collapsed: true Hidden: false +Row: 14: Level: 3 Collapsed: false Hidden: true +Row: 15: Level: 3 Collapsed: false Hidden: true +Row: 16: Level: 2 Collapsed: true Hidden: false +Row: 17: Level: 1 Collapsed: false Hidden: false +Row: 18: Level: 0 Collapsed: false Hidden: false +Row: 20: Level: 3 Collapsed: false Hidden: true +Row: 21: Level: 3 Collapsed: false Hidden: true +Row: 22: Level: 2 Collapsed: true Hidden: true +Row: 23: Level: 1 Collapsed: true Hidden: false +Row: 24: Level: 0 Collapsed: false Hidden: false +Row: 27: Level: 3 Collapsed: false Hidden: true +Row: 28: Level: 3 Collapsed: false Hidden: true +Row: 29: Level: 2 Collapsed: false Hidden: true +Row: 30: Level: 1 Collapsed: true Hidden: true +Row: 31: Level: 0 Collapsed: true Hidden: false + */ + } +} \ No newline at end of file diff --git a/test-data/spreadsheet/55640.xlsx b/test-data/spreadsheet/55640.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d353bb38007971a3109b2853a7b897d21728e1d8 GIT binary patch literal 6184 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 F^gr5~l>`6) literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/GroupTest.xlsx b/test-data/spreadsheet/GroupTest.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5feb37e88064da4711da38621d4477054cbb98f3 GIT binary patch literal 8149 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