From 4ad4d76241eae44386e52c3a23ca1b7ca28d1458 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sun, 25 Feb 2018 17:06:19 +0000 Subject: [PATCH] [bug-62055] Fix XSSFImportFromXML table resize. Thanks to Leonard Kappe. This closes #99 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1825315 13f79535-47bb-0310-9956-ffa450edef68 --- .../xssf/usermodel/examples/CreateTable.java | 6 +- .../poi/xssf/extractor/XSSFExportToXml.java | 30 +- .../poi/xssf/extractor/XSSFImportFromXML.java | 26 +- .../apache/poi/xssf/usermodel/XSSFSheet.java | 27 +- .../apache/poi/xssf/usermodel/XSSFTable.java | 464 +++++++++++++++--- .../poi/xssf/usermodel/XSSFTableColumn.java | 134 +++++ .../usermodel/helpers/XSSFXmlColumnPr.java | 133 +++-- .../xssf/extractor/TestXSSFImportFromXML.java | 313 ++++++------ .../poi/xssf/usermodel/TestXSSFTable.java | 143 +++++- .../xssf/usermodel/TestXSSFTableColumn.java | 79 +++ .../CustomXMLMappings-complex-type.xlsx | Bin 13348 -> 14979 bytes 11 files changed, 1053 insertions(+), 302 deletions(-) create mode 100644 src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFTableColumn.java create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFTableColumn.java diff --git a/src/examples/src/org/apache/poi/xssf/usermodel/examples/CreateTable.java b/src/examples/src/org/apache/poi/xssf/usermodel/examples/CreateTable.java index e1905f5be2..d1acce5f72 100644 --- a/src/examples/src/org/apache/poi/xssf/usermodel/examples/CreateTable.java +++ b/src/examples/src/org/apache/poi/xssf/usermodel/examples/CreateTable.java @@ -75,9 +75,9 @@ public class CreateTable { } } // Create the columns - table.addColumn(); - table.addColumn(); - table.addColumn(); + table.createColumn("Column 1"); + table.createColumn("Column 2"); + table.createColumn("Column 3"); // Set which area the table should be placed in AreaReference reference = wb.getCreationHelper().createAreaReference( diff --git a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java index c7e211d145..f95823f423 100644 --- a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java +++ b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFExportToXml.java @@ -50,9 +50,9 @@ import org.apache.poi.xssf.usermodel.XSSFMap; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFTable; +import org.apache.poi.xssf.usermodel.XSSFTableColumn; import org.apache.poi.xssf.usermodel.helpers.XSSFSingleXmlCell; import org.apache.poi.xssf.usermodel.helpers.XSSFXmlColumnPr; -import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; @@ -175,35 +175,27 @@ public class XSSFExportToXml implements Comparator{ // Exports elements and attributes mapped with tables if (table!=null) { - List tableColumns = table.getCTTable().getTableColumns().getTableColumnList(); + List tableColumns = table.getColumns(); XSSFSheet sheet = table.getXSSFSheet(); - int startRow = table.getStartCellReference().getRow(); - // In mappings created with Microsoft Excel the first row contains the table header and must be skipped - startRow +=1; - + int startRow = table.getStartCellReference().getRow() + table.getHeaderRowCount(); int endRow = table.getEndCellReference().getRow(); for(int i = startRow; i<= endRow; i++) { XSSFRow row = sheet.getRow(i); - Node tableRootNode = getNodeByXPath(table.getCommonXpath(),doc.getFirstChild(),doc,true); + Node tableRootNode = getNodeByXPath(table.getCommonXpath(), doc.getFirstChild(), doc, true); short startColumnIndex = table.getStartCellReference().getCol(); - for (int j = startColumnIndex; j <= table.getEndCellReference().getCol(); j++) { - XSSFCell cell = row.getCell(j); + for (XSSFTableColumn tableColumn : tableColumns) { + XSSFCell cell = row.getCell(startColumnIndex + tableColumn.getColumnIndex()); if (cell != null) { - int tableColumnIndex = j - startColumnIndex; - if (tableColumnIndex < tableColumns.size()) { - CTTableColumn ctTableColumn = tableColumns.get(tableColumnIndex); - if (ctTableColumn.getXmlColumnPr() != null) { - XSSFXmlColumnPr pointer = new XSSFXmlColumnPr(table, ctTableColumn, - ctTableColumn.getXmlColumnPr()); - String localXPath = pointer.getLocalXPath(); - Node currentNode = getNodeByXPath(localXPath,tableRootNode,doc,false); - mapCellOnNode(cell,currentNode); - } + XSSFXmlColumnPr xmlColumnPr = tableColumn.getXmlColumnPr(); + if (xmlColumnPr != null) { + String localXPath = xmlColumnPr.getLocalXPath(); + Node currentNode = getNodeByXPath(localXPath,tableRootNode,doc,false); + mapCellOnNode(cell, currentNode); } } } diff --git a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFImportFromXML.java b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFImportFromXML.java index 8ea6f1a8a4..d32dae26fc 100644 --- a/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFImportFromXML.java +++ b/src/ooxml/java/org/apache/poi/xssf/extractor/XSSFImportFromXML.java @@ -47,6 +47,7 @@ import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFMap; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFTable; +import org.apache.poi.xssf.usermodel.XSSFTableColumn; import org.apache.poi.xssf.usermodel.helpers.XSSFSingleXmlCell; import org.apache.poi.xssf.usermodel.helpers.XSSFXmlColumnPr; import org.openxmlformats.schemas.spreadsheetml.x2006.main.STXmlDataType; @@ -124,25 +125,32 @@ public class XSSFImportFromXML { String commonXPath = table.getCommonXpath(); NodeList result = (NodeList) xpath.evaluate(commonXPath, doc, XPathConstants.NODESET); - int rowOffset = table.getStartCellReference().getRow() + 1;// the first row contains the table header - int columnOffset = table.getStartCellReference().getCol() - 1; + int rowOffset = table.getStartCellReference().getRow() + table.getHeaderRowCount(); + int columnOffset = table.getStartCellReference().getCol(); + + table.setDataRowCount(result.getLength()); for (int i = 0; i < result.getLength(); i++) { // TODO: implement support for denormalized XMLs (see // OpenOffice part 4: chapter 3.5.1.7) - Node singleNode = result.item(i).cloneNode(true); - for (XSSFXmlColumnPr xmlColumnPr : table.getXmlColumnPrs()) { + Node singleNode = result.item(i).cloneNode(true); + + for (XSSFTableColumn tableColum : table.getColumns()) { + + XSSFXmlColumnPr xmlColumnPr = tableColum.getXmlColumnPr(); + if(xmlColumnPr == null) { + continue; + } - int localColumnId = (int) xmlColumnPr.getId(); int rowId = rowOffset + i; - int columnId = columnOffset + localColumnId; + int columnId = columnOffset + tableColum.getColumnIndex(); String localXPath = xmlColumnPr.getLocalXPath(); - localXPath = localXPath.substring(localXPath.substring(1).indexOf('/') + 2); + localXPath = localXPath.substring(localXPath.indexOf('/', 1) + 1); // TODO: convert the data to the cell format - String value = (String) xpath.evaluate(localXPath, singleNode, XPathConstants.STRING); + String value = (String) xpath.evaluate(localXPath, singleNode, XPathConstants.STRING); logger.log(POILogger.DEBUG, "Extracting with xpath " + localXPath + " : value is '" + value + "'"); XSSFRow row = table.getXSSFSheet().getRow(rowId); if (row == null) { @@ -161,6 +169,8 @@ public class XSSFImportFromXML { } } + + private static enum DataType { BOOLEAN(STXmlDataType.BOOLEAN), // DOUBLE(STXmlDataType.DOUBLE), // 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 f4329abdc4..417d1e44e3 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java @@ -80,6 +80,7 @@ import org.apache.poi.util.Beta; import org.apache.poi.util.Internal; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; +import org.apache.poi.util.Removal; import org.apache.poi.util.Units; import org.apache.poi.xssf.model.CommentsTable; import org.apache.poi.xssf.usermodel.XSSFPivotTable.PivotTableReferenceConfigurator; @@ -4057,10 +4058,28 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { } /** - * Creates a new Table, and associates it with this Sheet + * Creates a new Table, and associates it with this Sheet. The table does + * not yet have an area defined and needs to be initialized by calling + * {@link XSSFTable#setArea(AreaReference)}. + * + * @deprecated Use {@link #createTable(AreaReference))} instead */ + @Deprecated + @Removal(version = "4.2.0") public XSSFTable createTable() { - if(! worksheet.isSetTableParts()) { + return createTable(null); + } + + /** + * Creates a new Table, and associates it with this Sheet. + * + * @param tableArea + * the area that the table should cover, should not be {@null} + * @return the created table + * @since 4.0.0 + */ + public XSSFTable createTable(AreaReference tableArea) { + if (!worksheet.isSetTableParts()) { worksheet.addNewTableParts(); } @@ -4093,6 +4112,10 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { tables.put(tbl.getId(), table); + if(tableArea != null) { + table.setArea(tableArea); + } + return table; } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFTable.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFTable.java index 5d84662405..0fbd8d333b 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFTable.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFTable.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -32,12 +33,14 @@ import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.ss.SpreadsheetVersion; import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.Table; import org.apache.poi.ss.usermodel.TableStyleInfo; import org.apache.poi.ss.util.AreaReference; import org.apache.poi.ss.util.CellReference; import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; import org.apache.poi.util.StringUtil; import org.apache.poi.xssf.usermodel.helpers.XSSFXmlColumnPr; import org.apache.xmlbeans.XmlException; @@ -48,12 +51,12 @@ import org.openxmlformats.schemas.spreadsheetml.x2006.main.TableDocument; /** * - * This class implements the Table Part (Open Office XML Part 4: - * chapter 3.5.1) + * This class implements the Table Part (Open Office XML Part 4: chapter 3.5.1) * - * This implementation works under the assumption that a table contains mappings to a subtree of an XML. - * The root element of this subtree an occur multiple times (one for each row of the table). The child nodes - * of the root element can be only attributes or element with maxOccurs=1 property set + * Columns of this table may contains mappings to a subtree of an XML. The root + * element of this subtree can occur multiple times (one for each row of the + * table). The child nodes of the root element can be only attributes or + * elements with maxOccurs=1 property set. * * * @author Roberto Manicardi @@ -61,8 +64,8 @@ import org.openxmlformats.schemas.spreadsheetml.x2006.main.TableDocument; public class XSSFTable extends POIXMLDocumentPart implements Table { private CTTable ctTable; - private transient List xmlColumnPr; - private transient CTTableColumn[] ctColumns; + private transient List xmlColumnPrs; + private transient List tableColumns; private transient HashMap columnMap; private transient CellReference startCellReference; private transient CellReference endCellReference; @@ -155,18 +158,6 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { return false; } - - /** - * caches table columns for performance. - * Updated via updateHeaders - * @since 3.15 beta 2 - */ - private CTTableColumn[] getTableColumns() { - if (ctColumns == null) { - ctColumns = ctTable.getTableColumns().getTableColumnArray(); - } - return ctColumns; - } /** * @@ -179,9 +170,9 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { public String getCommonXpath() { if (commonXPath == null) { String[] commonTokens = {}; - for (CTTableColumn column : getTableColumns()) { + for (XSSFTableColumn column : getColumns()) { if (column.getXmlColumnPr()!=null) { - String xpath = column.getXmlColumnPr().getXpath(); + String xpath = column.getXmlColumnPr().getXPath(); String[] tokens = xpath.split("/"); if (commonTokens.length==0) { commonTokens = tokens; @@ -210,45 +201,166 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { return commonXPath; } + /** + * Note this list is static - once read, it does not notice later changes to the underlying column structures + * To clear the cache, call {@link #updateHeaders} + * @return List of XSSFTableColumn + * @since 4.0.0 + */ + public List getColumns() { + if (tableColumns == null) { + List columns = new ArrayList<>(); + CTTableColumns ctTableColumns = ctTable.getTableColumns(); + if (ctTableColumns != null) { + for (CTTableColumn column : ctTableColumns.getTableColumnList()) { + XSSFTableColumn tableColumn = new XSSFTableColumn(this, column); + columns.add(tableColumn); + } + } + tableColumns = Collections.unmodifiableList(columns); + } + return tableColumns; + } /** * Note this list is static - once read, it does not notice later changes to the underlying column structures * To clear the cache, call {@link #updateHeaders} + * + * @deprecated Use {@link XSSFTableColumn#getXmlColumnPr()} instead. + * * @return List of XSSFXmlColumnPr */ + @Deprecated + @Removal(version="4.2.0") public List getXmlColumnPrs() { - if (xmlColumnPr==null) { - xmlColumnPr = new ArrayList<>(); - for (CTTableColumn column: getTableColumns()) { - if (column.getXmlColumnPr()!=null) { - XSSFXmlColumnPr columnPr = new XSSFXmlColumnPr(this,column,column.getXmlColumnPr()); - xmlColumnPr.add(columnPr); + if (xmlColumnPrs == null) { + xmlColumnPrs = new ArrayList<>(); + for (XSSFTableColumn column: getColumns()) { + XSSFXmlColumnPr xmlColumnPr = column.getXmlColumnPr(); + if (xmlColumnPr != null) { + xmlColumnPrs.add(xmlColumnPr); } } } - return xmlColumnPr; + return xmlColumnPrs; } /** - * Adds another column to the table. + * Add a new column to the right end of the table. * - * Warning - Return type likely to change! + * @param columnName + * the unique name of the column, must not be {@code null} + * @return the created table column + * @since 4.0.0 */ - @Internal("Return type likely to change") - public void addColumn() { + public XSSFTableColumn createColumn(String columnName) { + return createColumn(columnName, getColumnCount()); + } + + /** + * Adds a new column to the table. + * + * @param columnName + * the unique name of the column, or {@code null} for a generated name + * @param columnIndex + * the 0-based position of the column in the table + * @return the created table column + * @throws IllegalArgumentException + * if the column name is not unique or missing or if the column + * can't be created at the given index + * @since 4.0.0 + */ + public XSSFTableColumn createColumn(String columnName, int columnIndex) { + + int columnCount = getColumnCount(); + if(columnIndex < 0 || columnIndex > columnCount) { + throw new IllegalArgumentException("Column index out of bounds"); + } + // Ensure we have Table Columns CTTableColumns columns = ctTable.getTableColumns(); if (columns == null) { columns = ctTable.addNewTableColumns(); } - // Add another Column, and give it a sensible ID - CTTableColumn column = columns.addNewTableColumn(); - int num = columns.sizeOfTableColumnArray(); - columns.setCount(num); - column.setId(num); + // check if name is unique and calculate unique column id + long nextColumnId = 1; + for (XSSFTableColumn tableColumn : getColumns()) { + if (columnName != null && columnName.equalsIgnoreCase(tableColumn.getName())) { + throw new IllegalArgumentException("Column '" + columnName + + "' already exists. Column names must be unique per table."); + } + nextColumnId = Math.max(nextColumnId, tableColumn.getId()); + } - // Have the Headers updated if possible + // Add the new Column + CTTableColumn column = columns.insertNewTableColumn(columnIndex); + columns.setCount(columns.sizeOfTableColumnArray()); + + column.setId(nextColumnId); + if(columnName != null) { + column.setName(columnName); + } else { + column.setName("Column " + nextColumnId); + } + + if (ctTable.getRef() != null) { + // calculate new area + int newColumnCount = columnCount + 1; + CellReference tableStart = getStartCellReference(); + CellReference tableEnd = getEndCellReference(); + SpreadsheetVersion version = getXSSFSheet().getWorkbook().getSpreadsheetVersion(); + CellReference newTableEnd = new CellReference(tableEnd.getRow(), + tableStart.getCol() + newColumnCount - 1); + AreaReference newTableArea = new AreaReference(tableStart, newTableEnd, version); + + setCellRef(newTableArea); + } + + updateHeaders(); + + return getColumns().get(columnIndex); + } + + /** + * Remove a column from the table. + * + * @param column + * the column to remove + * @since 4.0.0 + */ + public void removeColumn(XSSFTableColumn column) { + int columnIndex = getColumns().indexOf(column); + if (columnIndex >= 0) { + ctTable.getTableColumns().removeTableColumn(columnIndex); + updateReferences(); + updateHeaders(); + } + } + + /** + * Remove a column from the table. + * + * @param columnIndex + * the 0-based position of the column in the table + * @throws IllegalArgumentException + * if no column at the index exists or if the table has only a + * single column + * @since 4.0.0 + */ + public void removeColumn(int columnIndex) { + if (columnIndex < 0 || columnIndex > getColumnCount() - 1) { + throw new IllegalArgumentException("Column index out of bounds"); + } + + if(getColumnCount() == 1) { + throw new IllegalArgumentException("Table must have at least one column"); + } + + CTTableColumns tableColumns = ctTable.getTableColumns(); + tableColumns.removeTableColumn(columnIndex); + tableColumns.setCount(tableColumns.getTableColumnList().size()); + updateReferences(); updateHeaders(); } @@ -323,20 +435,25 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { } /** + * @deprecated Use {@link #getColumnCount()} instead. + * * @return the number of mapped table columns (see Open Office XML Part 4: chapter 3.5.1.4) */ + @Deprecated + @Removal(version = "4.2.0") public long getNumberOfMappedColumns() { return ctTable.getTableColumns().getCount(); } /** - * @return The reference for the cells of the table - * (see Open Office XML Part 4: chapter 3.5.1.2, attribute ref) + * Get the area reference for the cells which this table covers. The area + * includes header rows and totals rows. * - * Does not track updates to underlying changes to CTTable - * To synchronize with changes to the underlying CTTable, - * call {@link #updateReferences()}. + * Does not track updates to underlying changes to CTTable To synchronize + * with changes to the underlying CTTable, call {@link #updateReferences()}. * + * @return the area of the table + * @see "Open Office XML Part 4: chapter 3.5.1.2, attribute ref" * @since 3.17 beta 1 */ public AreaReference getCellReferences() { @@ -346,16 +463,35 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { SpreadsheetVersion.EXCEL2007 ); } + /** - * Updates the reference for the cells of the table - * (see Open Office XML Part 4: chapter 3.5.1.2, attribute ref) - * and synchronizes any changes - * @param refs table range + * Set the area reference for the cells which this table covers. The area + * includes includes header rows and totals rows. Automatically synchronizes + * any changes by calling {@link #updateHeaders()}. * + * Note: The area's width should be identical to the amount of columns in + * the table or the table may be invalid. All header rows, totals rows and + * at least one data row must fit inside the area. Updating the area with + * this method does not create or remove any columns and does not change any + * cell values. + * + * @deprecated Use {@link #setTableArea} instead, which will ensure that the + * the amount of columns always matches table area always width. + * + * @see "Open Office XML Part 4: chapter 3.5.1.2, attribute ref" * @since 3.17 beta 1 */ + @Deprecated + @Removal(version="4.2.0") public void setCellReferences(AreaReference refs) { - // Strip the Sheet name + setCellRef(refs); + } + + @Internal + protected void setCellRef(AreaReference refs) { + + // Strip the sheet name, + // CTWorksheet.getTableParts defines in which sheet the table is String ref = refs.formatAsString(); if (ref.indexOf('!') != -1) { ref = ref.substring(ref.indexOf('!')+1); @@ -383,6 +519,87 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { updateHeaders(); } + /** + * Set the area reference for the cells which this table covers. The area + * includes includes header rows and totals rows. + * + * Updating the area with this method will create new column as necessary to + * the right side of the table but will not modify any cell values. + * + * @param refs + * the new area of the table + * @throws IllegalArgumentException + * if the area is {@code null} or not + * @since 4.0.0 + */ + public void setArea(AreaReference tableArea) { + + if (tableArea == null) { + throw new IllegalArgumentException("AreaReference must not be null"); + } + + String areaSheetName = tableArea.getFirstCell().getSheetName(); + if (areaSheetName != null && !areaSheetName.equals(getXSSFSheet().getSheetName())) { + // TODO to move a table from one sheet to another + // CTWorksheet.getTableParts needs to be updated on both sheets + throw new IllegalArgumentException( + "The AreaReference must not reference a different sheet"); + } + + int rowCount = (tableArea.getLastCell().getRow() - tableArea.getFirstCell().getRow()) + 1; + int minimumRowCount = 1 + getHeaderRowCount() + getTotalsRowCount(); + if (rowCount < minimumRowCount) { + throw new IllegalArgumentException("AreaReference needs at least " + minimumRowCount + + " rows, to cover at least one data row and all header rows and totals rows"); + } + + // Strip the sheet name, + // CTWorksheet.getTableParts defines in which sheet the table is + String ref = tableArea.formatAsString(); + if (ref.indexOf('!') != -1) { + ref = ref.substring(ref.indexOf('!') + 1); + } + + // Update + ctTable.setRef(ref); + if (ctTable.isSetAutoFilter()) { + ctTable.getAutoFilter().setRef(ref); + } + updateReferences(); + + // add or remove columns on the right side of the table + int columnCount = getColumnCount(); + int newColumnCount = (tableArea.getLastCell().getCol() - tableArea.getFirstCell().getCol()) + 1; + if (newColumnCount > columnCount) { + for (int i = columnCount; i < newColumnCount; i++) { + createColumn(null, i); + } + } else if (newColumnCount < columnCount) { + for (int i = columnCount; i > newColumnCount; i--) { + removeColumn(i -1); + } + } + + updateHeaders(); + } + + /** + * Get the area that this table covers. + * + * @return the table's area or {@code null} if the area has not been + * initialized + * @since 4.0.0 + */ + public AreaReference getArea() { + String ref = ctTable.getRef(); + if (ref != null) { + SpreadsheetVersion version = getXSSFSheet().getWorkbook().getSpreadsheetVersion(); + return new AreaReference(ctTable.getRef(), version); + } else { + return null; + } + } + /** * @return The reference for the cell in the top-left part of the table * (see Open Office XML Part 4: chapter 3.5.1.2, attribute ref) @@ -445,12 +662,17 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { /** - * @return the total number of rows in the selection. (Note: in this version autofiltering is ignored) + * Get the total number of rows in this table, including all + * {@linkplain #getHeaderRowCount() header rows} and all + * {@linkplain #getTotalsRowCount() totals rows}. (Note: in this version + * autofiltering is ignored) + * * Returns 0 if the start or end cell references are not set. * - * Does not track updates to underlying changes to CTTable - * To synchronize with changes to the underlying CTTable, - * call {@link #updateReferences()}. + * Does not track updates to underlying changes to CTTable To synchronize + * with changes to the underlying CTTable, call {@link #updateReferences()}. + * + * @return the total number of rows */ public int getRowCount() { CellReference from = getStartCellReference(); @@ -462,6 +684,117 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { } return rowCount; } + + /** + * Get the number of data rows in this table. This does not include any + * header rows or totals rows. + * + * Returns 0 if the start or end cell references are not set. + * + * Does not track updates to underlying changes to CTTable To synchronize + * with changes to the underlying CTTable, call {@link #updateReferences()}. + * + * @return the number of data rows + * @since 4.0.0 + */ + public int getDataRowCount() { + CellReference from = getStartCellReference(); + CellReference to = getEndCellReference(); + + int rowCount = 0; + if (from != null && to != null) { + rowCount = (to.getRow() - from.getRow() + 1) - getHeaderRowCount() + - getTotalsRowCount(); + } + return rowCount; + } + + /** + * Set the number of rows in the data area of the table. This does not + * affect any header rows or totals rows. + * + * If the new row count is less than the current row count, superfluous rows + * will be cleared. If the new row count is greater than the current row + * count, cells below the table will be overwritten by the table. + * + * To resize the table without overwriting cells, use + * {@link #setArea(AreaReference)} instead. + * + * @param newDataRowCount + * new row count for the table + * @throws IllegalArgumentException + * if the row count is less than 1 + * @since 4.0.0 + */ + public void setDataRowCount(int newDataRowCount) { + + if (newDataRowCount < 1) { + throw new IllegalArgumentException("Table must have at least one data row"); + } + + updateReferences(); + int dataRowCount = getDataRowCount(); + if (dataRowCount == newDataRowCount) { + return; + } + + CellReference tableStart = getStartCellReference(); + CellReference tableEnd = getEndCellReference(); + SpreadsheetVersion version = getXSSFSheet().getWorkbook().getSpreadsheetVersion(); + + // calculate new area + int newTotalRowCount = getHeaderRowCount() + newDataRowCount + getTotalsRowCount(); + CellReference newTableEnd = new CellReference(tableStart.getRow() + newTotalRowCount - 1, + tableEnd.getCol()); + AreaReference newTableArea = new AreaReference(tableStart, newTableEnd, version); + + // clear cells + CellReference clearAreaStart; + CellReference clearAreaEnd; + if (newDataRowCount < dataRowCount) { + // table size reduced - + // clear all table cells that are outside of the new area + clearAreaStart = new CellReference(newTableArea.getLastCell().getRow() + 1, + newTableArea.getFirstCell().getCol()); + clearAreaEnd = tableEnd; + } else { + // table size increased - + // clear all cells below the table that are inside the new area + clearAreaStart = new CellReference(tableEnd.getRow() + 1, + newTableArea.getFirstCell().getCol()); + clearAreaEnd = newTableEnd; + } + AreaReference areaToClear = new AreaReference(clearAreaStart, clearAreaEnd, version); + for (CellReference cellRef : areaToClear.getAllReferencedCells()) { + XSSFRow row = getXSSFSheet().getRow(cellRef.getRow()); + if (row != null) { + XSSFCell cell = row.getCell(cellRef.getCol()); + if (cell != null) { + cell.setCellType(CellType.BLANK); + cell.setCellStyle(null); + } + } + } + + // update table area + setCellRef(newTableArea); + } + + /** + * Get the total number of columns in this table. + * + * @return the column count + * @since 4.0.0 + */ + public int getColumnCount() { + CTTableColumns tableColumns = ctTable.getTableColumns(); + if(tableColumns == null) { + return 0; + } + // Casting to int should be safe here - tables larger than the + // sheet (which holds the actual data of the table) can't exists. + return (int) tableColumns.getCount(); + } /** * Synchronize table headers with cell values in the parent sheet. @@ -479,7 +812,7 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { public void updateHeaders() { XSSFSheet sheet = (XSSFSheet)getParent(); CellReference ref = getStartCellReference(); - if(ref == null) return; + if (ref == null) return; int headerRow = ref.getRow(); int firstHeaderColumn = ref.getCol(); @@ -488,18 +821,21 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { if (row != null && row.getCTRow().validate()) { int cellnum = firstHeaderColumn; - for (CTTableColumn col : getCTTable().getTableColumns().getTableColumnList()) { - XSSFCell cell = row.getCell(cellnum); - if (cell != null) { - col.setName(formatter.formatCellValue(cell)); + CTTableColumns ctTableColumns = getCTTable().getTableColumns(); + if(ctTableColumns != null) { + for (CTTableColumn col : ctTableColumns.getTableColumnList()) { + XSSFCell cell = row.getCell(cellnum); + if (cell != null) { + col.setName(formatter.formatCellValue(cell)); + } + cellnum++; } - cellnum++; } - ctColumns = null; - columnMap = null; - xmlColumnPr = null; - commonXPath = null; } + tableColumns = null; + columnMap = null; + xmlColumnPrs = null; + commonXPath = null; } private static String caseInsensitive(String s) { @@ -522,11 +858,11 @@ public class XSSFTable extends POIXMLDocumentPart implements Table { if (columnHeader == null) return -1; if (columnMap == null) { // FIXME: replace with org.apache.commons.collections.map.CaseInsensitiveMap - final int count = getTableColumns().length; + final int count = getColumnCount(); columnMap = new HashMap<>(count * 3 / 2); int i = 0; - for (CTTableColumn column : getTableColumns()) { + for (XSSFTableColumn column : getColumns()) { String columnName = column.getName(); columnMap.put(caseInsensitive(columnName), i); i++; diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFTableColumn.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFTableColumn.java new file mode 100644 index 0000000000..6cb483db13 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFTableColumn.java @@ -0,0 +1,134 @@ +/* ==================================================================== + 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 org.apache.poi.util.Internal; +import org.apache.poi.xssf.usermodel.helpers.XSSFXmlColumnPr; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn; +import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTXmlColumnPr; + +/** + * A table column of an {@link XSSFTable}. Use {@link XSSFTable#createColumn} to + * create new table columns. + * + * @author Leonard Kappe + * @since 4.0.0 + */ +public class XSSFTableColumn { + + private final XSSFTable table; + private final CTTableColumn ctTableColumn; + private XSSFXmlColumnPr xmlColumnPr; + + /** + * Create a new table column. + * + * @param table + * the table which contains the column + * @param ctTableColumn + * the table column xmlbean to wrap + * @since 4.0.0 + */ + @Internal + protected XSSFTableColumn(XSSFTable table, CTTableColumn ctTableColumn) { + this.table = table; + this.ctTableColumn = ctTableColumn; + } + + /** + * Get the table which contains this column + * + * @return the table containing this column + * @since 4.0.0 + */ + public XSSFTable getTable() { + return table; + } + + /** + * Get the identifier of this column, which is is unique per table. + * + * @return the column id + * @since 4.0.0 + */ + public long getId() { + return ctTableColumn.getId(); + } + + /** + * Set the identifier of this column, which must be unique per table. + * + * It is up to the caller to enforce the uniqueness of the id. + * + * @return the column id + * @since 4.0.0 + */ + public void setId(long columnId) { + ctTableColumn.setId(columnId); + } + + /** + * Get the name of the column, which is is unique per table. + * + * @return the column name + * @since 4.0.0 + */ + public String getName() { + return ctTableColumn.getName(); + } + + /** + * Get the name of the column, which is is unique per table. + * + * @return the column name + * @since 4.0.0 + */ + public void setName(String columnName) { + ctTableColumn.setName(columnName); + } + + /** + * Get the XmlColumnPr (XML column properties) if this column has an XML + * mapping. + * + * @return the XmlColumnPr or null if this column has no XML + * mapping + * @since 4.0.0 + */ + public XSSFXmlColumnPr getXmlColumnPr() { + if (xmlColumnPr == null) { + CTXmlColumnPr ctXmlColumnPr = ctTableColumn.getXmlColumnPr(); + if (ctXmlColumnPr != null) { + xmlColumnPr = new XSSFXmlColumnPr(this, ctXmlColumnPr); + } + } + return xmlColumnPr; + } + + /** + * Get the column's position in its table, staring with zero from left to + * right. + * + * @return the column index + * @since 4.0.0 + */ + public int getColumnIndex() { + return table.findColumnIndex(getName()); + } + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFXmlColumnPr.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFXmlColumnPr.java index 574d64013a..47d34f4bb4 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFXmlColumnPr.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/helpers/XSSFXmlColumnPr.java @@ -17,12 +17,14 @@ package org.apache.poi.xssf.usermodel.helpers; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; import org.apache.poi.xssf.usermodel.XSSFTable; +import org.apache.poi.xssf.usermodel.XSSFTableColumn; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTTableColumn; import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTXmlColumnPr; import org.openxmlformats.schemas.spreadsheetml.x2006.main.STXmlDataType.Enum; - /** * * This class is a wrapper around the CTXmlColumnPr (Open Office XML Part 4: @@ -32,56 +34,85 @@ import org.openxmlformats.schemas.spreadsheetml.x2006.main.STXmlDataType.Enum; * @author Roberto Manicardi */ public class XSSFXmlColumnPr { - - private XSSFTable table; - private CTTableColumn ctTableColumn; - private CTXmlColumnPr ctXmlColumnPr; - - public XSSFXmlColumnPr(XSSFTable table ,CTTableColumn ctTableColum,CTXmlColumnPr ctXmlColumnPr){ - this.table = table; - this.ctTableColumn = ctTableColum; - this.ctXmlColumnPr = ctXmlColumnPr; - } - - public long getMapId(){ - return ctXmlColumnPr.getMapId(); - } - - public String getXPath(){ - return ctXmlColumnPr.getXpath(); - } - /** - * (see Open Office XML Part 4: chapter 3.5.1.3) - * @return An integer representing the unique identifier of this column. - */ - public long getId(){ - return ctTableColumn.getId(); - } - - - /** - * If the XPath is, for example, /Node1/Node2/Node3 and /Node1/Node2 is the common XPath for the table, the local XPath is /Node3 - * - * @return the local XPath - */ - public String getLocalXPath(){ - StringBuilder localXPath = new StringBuilder(); - int numberOfCommonXPathAxis = table.getCommonXpath().split("/").length-1; - - String[] xPathTokens = ctXmlColumnPr.getXpath().split("/"); - for(int i=numberOfCommonXPathAxis; i" + name + "" + - "" + teacher + "" + - "" + tutor + "" + - "" + cdl + "" + - "" + duration + "" + - "" + topic + "" + - "" + project + "" + - "" + credits + "" + - "\u0000"; + @Test + public void testImportFromXML() throws IOException, XPathExpressionException, SAXException{ + try (XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("CustomXMLMappings.xlsx")) { + String name = "name"; + String teacher = "teacher"; + String tutor = "tutor"; + String cdl = "cdl"; + String duration = "duration"; + String topic = "topic"; + String project = "project"; + String credits = "credits"; - XSSFMap map = wb.getMapInfo().getXSSFMapByName("CORSO_mapping"); - assertNotNull(map); - XSSFImportFromXML importer = new XSSFImportFromXML(map); + String testXML = "" + + "" + name + "" + + "" + teacher + "" + + "" + tutor + "" + + "" + cdl + "" + + "" + duration + "" + + "" + topic + "" + + "" + project + "" + + "" + credits + "" + + "\u0000"; - importer.importFromXML(testXML); + XSSFMap map = wb.getMapInfo().getXSSFMapByName("CORSO_mapping"); + assertNotNull(map); + XSSFImportFromXML importer = new XSSFImportFromXML(map); - XSSFSheet sheet = wb.getSheetAt(0); + importer.importFromXML(testXML); - XSSFRow row = sheet.getRow(0); - assertTrue(row.getCell(0).getStringCellValue().equals(name)); - assertTrue(row.getCell(1).getStringCellValue().equals(teacher)); - assertTrue(row.getCell(2).getStringCellValue().equals(tutor)); - assertTrue(row.getCell(3).getStringCellValue().equals(cdl)); - assertTrue(row.getCell(4).getStringCellValue().equals(duration)); - assertTrue(row.getCell(5).getStringCellValue().equals(topic)); - assertTrue(row.getCell(6).getStringCellValue().equals(project)); - assertTrue(row.getCell(7).getStringCellValue().equals(credits)); - } - } - - @Test(timeout=60000) - public void testMultiTable() throws IOException, XPathExpressionException, SAXException{ - try (XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("CustomXMLMappings-complex-type.xlsx")) { - String cellC6 = "c6"; - String cellC7 = "c7"; - String cellC8 = "c8"; - String cellC9 = "c9"; + XSSFSheet sheet = wb.getSheetAt(0); - StringBuilder testXML = new StringBuilder("" + - "" + - "" + - "" + - ""); + XSSFRow row = sheet.getRow(0); + assertTrue(row.getCell(0).getStringCellValue().equals(name)); + assertTrue(row.getCell(1).getStringCellValue().equals(teacher)); + assertTrue(row.getCell(2).getStringCellValue().equals(tutor)); + assertTrue(row.getCell(3).getStringCellValue().equals(cdl)); + assertTrue(row.getCell(4).getStringCellValue().equals(duration)); + assertTrue(row.getCell(5).getStringCellValue().equals(topic)); + assertTrue(row.getCell(6).getStringCellValue().equals(project)); + assertTrue(row.getCell(7).getStringCellValue().equals(credits)); + } + } - for (int i = 10; i < 10010; i++) { - testXML.append(""); - } + @Test(timeout=60000) + public void testMultiTable() throws IOException, XPathExpressionException, SAXException{ + try (XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("CustomXMLMappings-complex-type.xlsx")) { + String cellC6 = "c6"; + String cellC7 = "c7"; + String cellC8 = "c8"; + String cellC9 = "c9"; - testXML.append("" + "" + "" + "" + "" + "" + "" + "" + "" + "\u0000"); + StringBuilder testXML = new StringBuilder("" + + "" + + "" + + "" + + ""); - XSSFMap map = wb.getMapInfo().getXSSFMapByName("MapInfo_mapping"); - assertNotNull(map); - XSSFImportFromXML importer = new XSSFImportFromXML(map); + int cellOffset = 10; // cell C10 + for (int i = 0; i < 10000; i++) { + testXML.append(""); + } - importer.importFromXML(testXML.toString()); + testXML.append("" + "" + "" + "" + "" + "" + "" + "" + "" + "\u0000"); - //Check for Schema element - XSSFSheet sheet = wb.getSheetAt(1); + XSSFMap map = wb.getMapInfo().getXSSFMapByName("MapInfo_mapping"); + assertNotNull(map); + XSSFImportFromXML importer = new XSSFImportFromXML(map); - assertEquals(cellC6, sheet.getRow(5).getCell(2).getStringCellValue()); - assertEquals(cellC7, sheet.getRow(6).getCell(2).getStringCellValue()); - assertEquals(cellC8, sheet.getRow(7).getCell(2).getStringCellValue()); - assertEquals(cellC9, sheet.getRow(8).getCell(2).getStringCellValue()); - assertEquals("c5001", sheet.getRow(5000).getCell(2).getStringCellValue()); - } - } + importer.importFromXML(testXML.toString()); - - @Test - public void testSingleAttributeCellWithNamespace() throws IOException, XPathExpressionException, SAXException{ - try (XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("CustomXMLMapping-singleattributenamespace.xlsx")) { - int id = 1; - String displayName = "dispName"; - String ref = "19"; - int count = 21; + //Check for Schema element + XSSFSheet sheet = wb.getSheetAt(1); - String testXML = "" + - "" + - "" + - "\u0000"; - XSSFMap map = wb.getMapInfo().getXSSFMapByName("table_mapping"); - assertNotNull(map); - XSSFImportFromXML importer = new XSSFImportFromXML(map); - importer.importFromXML(testXML); - //Check for Schema element - XSSFSheet sheet = wb.getSheetAt(0); + // check table size (+1 for the header row) + assertEquals(3 + 1, wb.getTable("Tabella1").getRowCount()); + assertEquals(10004 + 1, wb.getTable("Tabella2").getRowCount()); - assertEquals(new Double(id), sheet.getRow(28).getCell(1).getNumericCellValue(), 0); - assertEquals(displayName, sheet.getRow(11).getCell(5).getStringCellValue()); - assertEquals(ref, sheet.getRow(14).getCell(7).getStringCellValue()); - assertEquals(new Double(count), sheet.getRow(18).getCell(3).getNumericCellValue(), 0); - } - } - - @Test - public void testOptionalFields_Bugzilla_55864() throws IOException, XPathExpressionException, SAXException { - try (XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("55864.xlsx")) { - String testXML = "" + - "" + - "" + - "Albert" + - "Einstein" + - "1879-03-14" + - "" + - ""; + // table1 size was reduced, check that former table cells have been cleared + assertEquals(CellType.BLANK, wb.getSheetAt(0).getRow(8).getCell(5).getCellType()); - XSSFMap map = wb.getMapInfo().getXSSFMapByName("PersonInfoRoot_Map"); - assertNotNull(map); - XSSFImportFromXML importer = new XSSFImportFromXML(map); + // table2 size was increased, check that new table cells have been cleared + assertEquals(CellType.BLANK, sheet.getRow(10).getCell(3).getCellType()); - importer.importFromXML(testXML); + assertEquals(cellC6, sheet.getRow(5).getCell(2).getStringCellValue()); + assertEquals(cellC7, sheet.getRow(6).getCell(2).getStringCellValue()); + assertEquals(cellC8, sheet.getRow(7).getCell(2).getStringCellValue()); + assertEquals(cellC9, sheet.getRow(8).getCell(2).getStringCellValue()); + assertEquals("c5001", sheet.getRow(5000).getCell(2).getStringCellValue()); + } + } - XSSFSheet sheet = wb.getSheetAt(0); - XSSFRow rowHeadings = sheet.getRow(0); - XSSFRow rowData = sheet.getRow(1); + @Test + public void testSingleAttributeCellWithNamespace() throws IOException, XPathExpressionException, SAXException{ + try (XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("CustomXMLMapping-singleattributenamespace.xlsx")) { + int id = 1; + String displayName = "dispName"; + String ref = "19"; + int count = 21; - assertEquals("FirstName", rowHeadings.getCell(0).getStringCellValue()); - assertEquals("Albert", rowData.getCell(0).getStringCellValue()); + String testXML = "" + + "" + + "" + + "\u0000"; + XSSFMap map = wb.getMapInfo().getXSSFMapByName("table_mapping"); + assertNotNull(map); + XSSFImportFromXML importer = new XSSFImportFromXML(map); + importer.importFromXML(testXML); - assertEquals("LastName", rowHeadings.getCell(1).getStringCellValue()); - assertEquals("Einstein", rowData.getCell(1).getStringCellValue()); + //Check for Schema element + XSSFSheet sheet = wb.getSheetAt(0); - assertEquals("BirthDate", rowHeadings.getCell(2).getStringCellValue()); - assertEquals("1879-03-14", rowData.getCell(2).getStringCellValue()); + assertEquals(new Double(id), sheet.getRow(28).getCell(1).getNumericCellValue(), 0); + assertEquals(displayName, sheet.getRow(11).getCell(5).getStringCellValue()); + assertEquals(ref, sheet.getRow(14).getCell(7).getStringCellValue()); + assertEquals(new Double(count), sheet.getRow(18).getCell(3).getNumericCellValue(), 0); + } + } - // Value for OptionalRating is declared optional (minOccurs=0) in 55864.xlsx - assertEquals("OptionalRating", rowHeadings.getCell(3).getStringCellValue()); - assertNull("", rowData.getCell(3)); - } - } + @Test + public void testOptionalFields_Bugzilla_55864() throws IOException, XPathExpressionException, SAXException { + try (XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("55864.xlsx")) { + String testXML = "" + + "" + + "" + + "Albert" + + "Einstein" + + "1879-03-14" + + "" + + ""; - @Test - public void testOptionalFields_Bugzilla_57890() throws IOException, ParseException, XPathExpressionException, SAXException { - XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("57890.xlsx"); + XSSFMap map = wb.getMapInfo().getXSSFMapByName("PersonInfoRoot_Map"); + assertNotNull(map); + XSSFImportFromXML importer = new XSSFImportFromXML(map); - String testXML = "" + "" - + "" + "" + Integer.MIN_VALUE + "" + "12345" - + "1.0000123" + "1991-03-14" + "" + ""; + importer.importFromXML(testXML); - XSSFMap map = wb.getMapInfo().getXSSFMapByName("TestInfoRoot_Map"); - assertNotNull(map); - XSSFImportFromXML importer = new XSSFImportFromXML(map); + XSSFSheet sheet = wb.getSheetAt(0); - importer.importFromXML(testXML); + XSSFRow rowHeadings = sheet.getRow(0); + XSSFRow rowData = sheet.getRow(1); - XSSFSheet sheet = wb.getSheetAt(0); + assertEquals("FirstName", rowHeadings.getCell(0).getStringCellValue()); + assertEquals("Albert", rowData.getCell(0).getStringCellValue()); - XSSFRow rowHeadings = sheet.getRow(0); - XSSFRow rowData = sheet.getRow(1); + assertEquals("LastName", rowHeadings.getCell(1).getStringCellValue()); + assertEquals("Einstein", rowData.getCell(1).getStringCellValue()); - assertEquals("Date", rowHeadings.getCell(0).getStringCellValue()); - Date date = new SimpleDateFormat("yyyy-MM-dd", DateFormatSymbols.getInstance(Locale.ROOT)).parse("1991-3-14"); - assertEquals(date, rowData.getCell(0).getDateCellValue()); + assertEquals("BirthDate", rowHeadings.getCell(2).getStringCellValue()); + assertEquals("1879-03-14", rowData.getCell(2).getStringCellValue()); + + // Value for OptionalRating is declared optional (minOccurs=0) in 55864.xlsx + assertEquals("OptionalRating", rowHeadings.getCell(3).getStringCellValue()); + assertNull("", rowData.getCell(3)); + } + } + + @Test + public void testOptionalFields_Bugzilla_57890() throws IOException, ParseException, XPathExpressionException, SAXException { + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("57890.xlsx"); + + String testXML = "" + "" + + "" + "" + Integer.MIN_VALUE + "" + "12345" + + "1.0000123" + "1991-03-14" + "" + ""; + + XSSFMap map = wb.getMapInfo().getXSSFMapByName("TestInfoRoot_Map"); + assertNotNull(map); + XSSFImportFromXML importer = new XSSFImportFromXML(map); + + importer.importFromXML(testXML); + + XSSFSheet sheet = wb.getSheetAt(0); + + XSSFRow rowHeadings = sheet.getRow(0); + XSSFRow rowData = sheet.getRow(1); + + assertEquals("Date", rowHeadings.getCell(0).getStringCellValue()); + Date date = new SimpleDateFormat("yyyy-MM-dd", DateFormatSymbols.getInstance(Locale.ROOT)).parse("1991-3-14"); + assertEquals(date, rowData.getCell(0).getDateCellValue()); + + assertEquals("Amount Int", rowHeadings.getCell(1).getStringCellValue()); + assertEquals(new Double(Integer.MIN_VALUE), rowData.getCell(1).getNumericCellValue(), 0); + + assertEquals("Amount Double", rowHeadings.getCell(2).getStringCellValue()); + assertEquals(1.0000123, rowData.getCell(2).getNumericCellValue(), 0); + + assertEquals("Amount UnsignedInt", rowHeadings.getCell(3).getStringCellValue()); + assertEquals(new Double(12345), rowData.getCell(3).getNumericCellValue(), 0); + + wb.close(); + } - assertEquals("Amount Int", rowHeadings.getCell(1).getStringCellValue()); - assertEquals(new Double(Integer.MIN_VALUE), rowData.getCell(1).getNumericCellValue(), 0); - assertEquals("Amount Double", rowHeadings.getCell(2).getStringCellValue()); - assertEquals(1.0000123, rowData.getCell(2).getNumericCellValue(), 0); - assertEquals("Amount UnsignedInt", rowHeadings.getCell(3).getStringCellValue()); - assertEquals(new Double(12345), rowData.getCell(3).getNumericCellValue(), 0); - - wb.close(); - } - - - } diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFTable.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFTable.java index 6ff8b8b007..9e2f9246dd 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFTable.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFTable.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.List; import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.util.AreaReference; import org.apache.poi.ss.util.CellReference; import org.apache.poi.util.IOUtils; import org.apache.poi.util.TempFile; @@ -223,6 +224,14 @@ public final class TestXSSFTable { wb.close(); } + @Test + public void getColumnCount() throws IOException { + XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("StructuredReferences.xlsx"); + XSSFTable table = wb.getTable("\\_Prime.1"); + assertEquals(3, table.getColumnCount()); + wb.close(); + } + @Test public void getAndSetDisplayName() throws IOException { XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("StructuredReferences.xlsx"); @@ -291,7 +300,131 @@ public final class TestXSSFTable { IOUtils.closeQuietly(wb); } + + @Test + public void testGetDataRowCount() { + XSSFWorkbook wb = new XSSFWorkbook(); + XSSFSheet sh = wb.createSheet(); + AreaReference tableArea = new AreaReference("B2:B6", wb.getSpreadsheetVersion()); + XSSFTable table = sh.createTable(tableArea); + assertEquals(5, table.getRowCount()); // includes column header + assertEquals(4, table.getDataRowCount()); + + table.setArea(new AreaReference("B2:B7", wb.getSpreadsheetVersion())); + + assertEquals(6, table.getRowCount()); + assertEquals(5, table.getDataRowCount()); + + IOUtils.closeQuietly(wb); + } + + @Test + public void testSetDataRowCount() throws IOException { + XSSFWorkbook wb = new XSSFWorkbook(); + XSSFSheet sh = wb.createSheet(); + + // 1 header row + 1 data row + AreaReference tableArea = new AreaReference("C10:C11", wb.getSpreadsheetVersion()); + XSSFTable table = sh.createTable(tableArea); + + assertEquals(2, table.getRowCount()); // includes all data and header/footer rows + + assertEquals(1, table.getHeaderRowCount()); + assertEquals(1, table.getDataRowCount()); + assertEquals(0, table.getTotalsRowCount()); + + table.setDataRowCount(5); + + assertEquals(6, table.getRowCount()); + + assertEquals(1, table.getHeaderRowCount()); + assertEquals(5, table.getDataRowCount()); + assertEquals(0, table.getTotalsRowCount()); + + assertEquals("C10:C15", table.getArea().formatAsString()); + + + IOUtils.closeQuietly(wb); + } + + @Test + public void testSetArea() throws IOException { + XSSFWorkbook wb = new XSSFWorkbook(); + XSSFSheet sh = wb.createSheet(); + + AreaReference tableArea = new AreaReference("B10:D12", wb.getSpreadsheetVersion()); + XSSFTable table = sh.createTable(tableArea); + + assertEquals(3, table.getColumnCount()); + assertEquals(3, table.getRowCount()); + + // move table without resizing, shouldn't change row or column count + AreaReference tableArea2 = new AreaReference("B11:D13", wb.getSpreadsheetVersion()); + table.setArea(tableArea2); + + assertEquals(3, table.getColumnCount()); + assertEquals(3, table.getRowCount()); + + // increase size by 1 row and 1 column + AreaReference tableArea3 = new AreaReference("B11:E14", wb.getSpreadsheetVersion()); + table.setArea(tableArea3); + + assertEquals(4, table.getColumnCount()); + assertEquals(4, table.getRowCount()); + + // reduce size by 2 rows and 2 columns + AreaReference tableArea4 = new AreaReference("C12:D13", wb.getSpreadsheetVersion()); + table.setArea(tableArea4); + + assertEquals(2, table.getColumnCount()); + assertEquals(2, table.getRowCount()); + + IOUtils.closeQuietly(wb); + } + + @Test + public void testCreateColumn() { + XSSFWorkbook wb = new XSSFWorkbook(); + XSSFSheet sh = wb.createSheet(); + + AreaReference tableArea = new AreaReference("A2:A3", wb.getSpreadsheetVersion()); + XSSFTable table = sh.createTable(tableArea); + + assertEquals(1, table.getColumnCount()); + assertEquals(2, table.getRowCount()); + + // add columns + table.createColumn("Column B"); + table.createColumn("Column D"); + table.createColumn("Column C", 2); // add between B and D + table.updateReferences(); + table.updateHeaders(); + + assertEquals(4, table.getColumnCount()); + assertEquals(2, table.getRowCount()); + + assertEquals("Column 1", table.getColumns().get(0).getName()); // generated name + assertEquals("Column B", table.getColumns().get(1).getName()); + assertEquals("Column C", table.getColumns().get(2).getName()); + assertEquals("Column D", table.getColumns().get(3).getName()); + + IOUtils.closeQuietly(wb); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateColumnInvalidIndex() throws IOException { + try (XSSFWorkbook wb = new XSSFWorkbook();) { + XSSFSheet sh = wb.createSheet(); + AreaReference tableArea = new AreaReference("D2:D3", wb.getSpreadsheetVersion()); + XSSFTable table = sh.createTable(tableArea); + + // add columns + table.createColumn("Column 2", 1); + table.createColumn("Column 3", 3); // out of bounds + } + } + @Test public void testDifferentHeaderTypes() throws IOException { XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("TablesWithDifferentHeaders.xlsx"); @@ -345,13 +478,13 @@ public final class TestXSSFTable { c5.setCellValue("CD"); c6.setCellValue("EF"); - // Setting up the CTTable - XSSFTable t = s.createTable(); + // Setting up the table + XSSFTable t = s.createTable(new AreaReference("A1:C3", wb.getSpreadsheetVersion())); t.setName("TableTest"); t.setDisplayName("CT_Table_Test"); - t.addColumn(); - t.addColumn(); - t.addColumn(); + t.createColumn("Column 1"); + t.createColumn("Column 2"); + t.createColumn("Column 3"); t.setCellReferences(wb.getCreationHelper().createAreaReference( new CellReference(c1), new CellReference(c6) )); diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFTableColumn.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFTableColumn.java new file mode 100644 index 0000000000..83cf5634a2 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFTableColumn.java @@ -0,0 +1,79 @@ +/* ==================================================================== + 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.util.List; + +import org.apache.poi.xssf.XSSFTestDataSamples; +import org.junit.Test; + +public final class TestXSSFTableColumn { + + @Test + public void testGetColumnName() throws IOException { + try (XSSFWorkbook wb = XSSFTestDataSamples + .openSampleWorkbook("CustomXMLMappings-complex-type.xlsx")) { + XSSFTable table = wb.getTable("Tabella2"); + + List tableColumns = table.getColumns(); + + assertEquals("ID", tableColumns.get(0).getName()); + assertEquals("Unmapped Column", tableColumns.get(1).getName()); + assertEquals("SchemaRef", tableColumns.get(2).getName()); + assertEquals("Namespace", tableColumns.get(3).getName()); + + } + } + + @Test + public void testGetColumnIndex() throws IOException { + try (XSSFWorkbook wb = XSSFTestDataSamples + .openSampleWorkbook("CustomXMLMappings-complex-type.xlsx")) { + XSSFTable table = wb.getTable("Tabella2"); + + List tableColumns = table.getColumns(); + + assertEquals(0, tableColumns.get(0).getColumnIndex()); + assertEquals(1, tableColumns.get(1).getColumnIndex()); + assertEquals(2, tableColumns.get(2).getColumnIndex()); + assertEquals(3, tableColumns.get(3).getColumnIndex()); + + } + } + + @Test + public void testGetXmlColumnPrs() throws IOException { + try (XSSFWorkbook wb = XSSFTestDataSamples + .openSampleWorkbook("CustomXMLMappings-complex-type.xlsx")) { + XSSFTable table = wb.getTable("Tabella2"); + + List tableColumns = table.getColumns(); + + assertNotNull(tableColumns.get(0).getXmlColumnPr()); + assertNull(tableColumns.get(1).getXmlColumnPr()); // unmapped column + assertNotNull(tableColumns.get(2).getColumnIndex()); + assertNotNull(tableColumns.get(3).getColumnIndex()); + + } + } +} diff --git a/test-data/spreadsheet/CustomXMLMappings-complex-type.xlsx b/test-data/spreadsheet/CustomXMLMappings-complex-type.xlsx index 0d6d8d00990b979ddb0a7092c2320c2a3c9486a2..7cd51014798388b687b249abd6e230f673ff2d57 100644 GIT binary patch delta 11375 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 IW%{%CKU=63G5`Po