a .NET library that can read/write Office formats without Microsoft Office installed. No COM+, no interop.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

765 lines
25 KiB

/* ====================================================================
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.
==================================================================== */
using System.Xml;
using System.Collections.Generic;
using NPOI.SS.Util;
using System;
using NPOI.OpenXml4Net.OPC;
using System.IO;
using NPOI.OpenXmlFormats.Spreadsheet;
using NPOI.Util;
using System.Collections;
using NPOI.XSSF.UserModel.Helpers;
using NPOI.SS.UserModel;
using System.Text.RegularExpressions;
using System.Globalization;
using NPOI.SS;
using System.Linq;
using NPOI.OOXML.XSSF.UserModel;
namespace NPOI.XSSF.UserModel
{
/**
*
* This class : 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
*
*
* @author Roberto Manicardi
*/
public class XSSFTable : POIXMLDocumentPart, ITable
{
private CT_Table ctTable;
private List<XSSFXmlColumnPr> xmlColumnPrs;
private List<XSSFTableColumn> tableColumns;
private Dictionary<String, int> columnMap;
private CellReference startCellReference;
private CellReference endCellReference;
private String commonXPath;
public XSSFTable()
: base()
{
ctTable = new CT_Table();
}
internal XSSFTable(PackagePart part)
: base(part)
{
XmlDocument xml = ConvertStreamToXml(part.GetInputStream());
ReadFrom(xml);
}
[Obsolete("deprecated in POI 3.14, scheduled for removal in POI 3.16")]
protected XSSFTable(PackagePart part, PackageRelationship rel)
: this(part)
{
}
public void ReadFrom(XmlDocument xmlDoc)
{
try
{
TableDocument doc = TableDocument.Parse(xmlDoc, NamespaceManager);
ctTable = doc.GetTable();
}
catch (XmlException e)
{
throw new IOException(e.Message);
}
}
public XSSFSheet GetXSSFSheet()
{
return (XSSFSheet)GetParent();
}
public void WriteTo(Stream out1)
{
UpdateHeaders();
TableDocument doc = new TableDocument();
doc.SetTable(ctTable);
doc.Save(out1);
}
protected internal override void Commit()
{
PackagePart part = GetPackagePart();
Stream out1 = part.GetOutputStream();
WriteTo(out1);
out1.Close();
}
public CT_Table GetCTTable()
{
return ctTable;
}
/**
* Checks if this Table element Contains even a single mapping to the map identified by id
* @param id the XSSFMap ID
* @return true if the Table element contain mappings
*/
public bool MapsTo(long id)
{
bool maps = false;
List<XSSFXmlColumnPr> pointers = GetXmlColumnPrs();
foreach (XSSFXmlColumnPr pointer in pointers)
{
if (pointer.MapId == id)
{
maps = true;
break;
}
}
return maps;
}
/**
*
* Calculates the xpath of the root element for the table. This will be the common part
* of all the mapping's xpaths
*
* @return the xpath of the table's root element
*/
public String GetCommonXpath()
{
if (commonXPath == null)
{
Array commonTokens = null;
foreach (XSSFTableColumn column in GetColumns())
{
if (column.GetXmlColumnPr() != null)
{
String xpath = column.GetXmlColumnPr().XPath;
String[] tokens = xpath.Split('/');
if (commonTokens==null)
{
commonTokens = tokens;
}
else
{
int maxLenght = commonTokens.Length > tokens.Length ? tokens.Length : commonTokens.Length;
for (int i = 0; i < maxLenght; i++)
{
if (!commonTokens.GetValue(i).Equals(tokens[i]))
{
ArrayList subCommonTokens = Arrays.AsList(commonTokens).GetRange(0, i);
commonTokens = subCommonTokens.ToArray(typeof(string));
break;
}
}
}
}
}
commonXPath = "";
for (int i = 1; i < commonTokens.Length; i++)
{
commonXPath += "/" + commonTokens.GetValue(i);
}
}
return commonXPath;
}
/**
* Note this list is static - once read, it does not notice later changes to the underlying column structures
* @return List of XSSFXmlColumnPr
*/
[Obsolete]
public List<XSSFXmlColumnPr> GetXmlColumnPrs()
{
if (xmlColumnPrs == null)
{
xmlColumnPrs = new List<XSSFXmlColumnPr>();
foreach (CT_TableColumn column in ctTable.tableColumns.tableColumn)
{
if (column.xmlColumnPr != null)
{
XSSFXmlColumnPr columnPr = new XSSFXmlColumnPr(this, column, column.xmlColumnPr);
xmlColumnPrs.Add(columnPr);
}
}
}
return xmlColumnPrs;
}
private string name;
/**
* @return the name of the Table, if set
*/
public String Name
{
get
{
if (name == null)
{
Name = ctTable.name;
}
return name;
}
set
{
if (value == null)
{
ctTable.name=null;
name = null;
return;
}
ctTable.name = value;
name = value;
}
}
public XSSFTableColumn CreateColumn(String columnName)
{
return CreateColumn(columnName, this.ColumnCount);
}
public XSSFTableColumn CreateColumn(String columnName, int columnIndex)
{
int columnCount = ColumnCount;
if (columnIndex < 0 || columnIndex > columnCount)
{
throw new ArgumentException("Column index out of bounds");
}
// Ensure we have Table Columns
CT_TableColumns columns = ctTable.tableColumns;
if (columns == null)
{
columns = ctTable.AddNewTableColumns();
}
// check if name is unique and calculate unique column id
long nextColumnId = 0;
foreach (XSSFTableColumn tableColumn in this.GetColumns())
{
if (columnName != null && columnName.Equals(tableColumn.Name,StringComparison.InvariantCultureIgnoreCase))
{
throw new ArgumentException("Column '" + columnName
+ "' already exists. Column names must be unique per table.");
}
nextColumnId = Math.Max(nextColumnId, tableColumn.Id);
}
// Bug #62740, the logic was just re-using the existing max ID, not incrementing beyond it.
nextColumnId++;
// Add the new Column
CT_TableColumn column = columns.InsertNewTableColumn(columnIndex);
columns.count = columns.count;
column.id = (uint)nextColumnId;
if (columnName != null)
{
column.name = columnName;
}
else
{
column.name = "Column " + nextColumnId;
}
/*if (ctTable.@ref != null)
{
// calculate new area
int newColumnCount = columnCount + 1;
CellReference tableStart = StartCellReference;
CellReference tableEnd = EndCellReference;
SpreadsheetVersion version = GetXSSFSheet().GetWorkbook().SpreadsheetVersion;
CellReference newTableEnd = new CellReference(tableEnd.Row,
tableStart.Col + newColumnCount - 1);
AreaReference newTableArea = new AreaReference(tableStart, newTableEnd, version);
SetCellRef(newTableArea);
}*/
UpdateHeaders();
return GetColumns()[columnIndex];
}
/**
* 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()}.
*
* @return the area of the table
* @see "Open Office XML Part 4: chapter 3.5.1.2, attribute ref"
*/
public AreaReference GetCellReferences()
{
return new AreaReference(
StartCellReference,
EndCellReference,
SpreadsheetVersion.EXCEL2007
);
}
/**
* 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.
*
* @see "Open Office XML Part 4: chapter 3.5.1.2, attribute ref"
*/
public void SetCellReferences(AreaReference refs)
{
SetCellRef(refs);
}
protected void SetCellRef(AreaReference refs)
{
// Strip the sheet name,
// CTWorksheet.getTableParts defines in which sheet the table is
String reference = refs.FormatAsString();
if (reference.Contains('!'))
{
reference = reference.Substring(reference.IndexOf('!') + 1);
}
// Update
ctTable.@ref = reference;
if (ctTable.IsSetAutoFilter)
{
String filterRef;
int totalsRowCount = TotalsRowCount;
if (totalsRowCount == 0)
{
filterRef = reference;
}
else
{
CellReference start = new CellReference(refs.FirstCell.Row, refs.FirstCell.Col);
// account for footer row(s) in auto-filter range, which doesn't include footers
CellReference end = new CellReference(refs.LastCell.Row - totalsRowCount, refs.LastCell.Col);
// this won't have sheet references because we built the cell references without them
filterRef = new AreaReference(start, end, SpreadsheetVersion.EXCEL2007).FormatAsString();
}
ctTable.autoFilter.@ref =filterRef;
}
// Have everything recomputed
UpdateReferences();
UpdateHeaders();
}
private String styleName;
public string StyleName
{
get {
if (styleName == null && ctTable.IsSetTableStyleInfo())
{
StyleName = ctTable.tableStyleInfo.name;
}
return styleName;
}
set
{
if (value == null)
{
if (ctTable.IsSetTableStyleInfo())
{
ctTable.tableStyleInfo.name =null;
}
styleName = null;
return;
}
if (!ctTable.IsSetTableStyleInfo())
{
ctTable.AddNewTableStyleInfo();
}
ctTable.tableStyleInfo.name = value;
styleName = value;
}
}
public ITableStyleInfo Style
{
get
{
if (!ctTable.IsSetTableStyleInfo()) return null;
return new XSSFTableStyleInfo(((XSSFWorkbook)((XSSFSheet)GetParent()).Workbook).GetStylesSource(), ctTable.tableStyleInfo);
}
}
/**
* @return the display name of the Table, if set
*/
public string DisplayName
{
get
{
return ctTable.displayName;
}
set
{
ctTable.displayName = value;
}
}
/**
* @return the number of mapped table columns (see Open Office XML Part 4: chapter 3.5.1.4)
*/
[Obsolete]
public long NumberOfMappedColumns
{
get
{
return ctTable.tableColumns.count;
}
}
public int ColumnCount
{
get
{
CT_TableColumns tableColumns = ctTable.tableColumns;
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.tableColumn.Count;
}
}
/// <summary>
/// 0 for no totals rows, 1 for totals row shown.
/// Values > 1 are not currently used by Excel up through 2016, and the OOXML spec
/// doesn't define how they would be implemented.
/// </summary>
public int TotalsRowCount
{
get {
return (int)ctTable.totalsRowCount;
}
}
/// <summary>
/// 0 for no header rows, 1 for table headers shown.
/// Values > 1 might be used by Excel for pivot tables?
/// </summary>
public int HeaderRowCount
{
get
{
return (int)ctTable.headerRowCount;
}
}
/**
* @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)
*
* To synchronize with changes to the underlying CTTable,
* call {@link #updateReferences()}.
*/
public CellReference StartCellReference
{
get
{
if (startCellReference == null)
{
SetCellReferences();
}
return startCellReference;
}
}
/**
* @return The reference for the cell in the bottom-right part of the table
* (see Open Office XML Part 4: chapter 3.5.1.2, attribute ref)
*
* Does not track updates to underlying changes to CTTable
* To synchronize with changes to the underlying CTTable,
* call {@link #updateReferences()}.
*/
public CellReference EndCellReference
{
get
{
if (endCellReference == null)
{
SetCellReferences();
}
return endCellReference;
}
}
/**
* @since POI 3.15 beta 3
*/
private void SetCellReferences()
{
string ref1 = ctTable.@ref;
if (ref1 != null) {
string[] boundaries = ref1.Split([':'], 2);
string from = boundaries[0];
string to = boundaries.Length == 2 ? boundaries[1] : boundaries[0];
startCellReference = new CellReference(from);
endCellReference = new CellReference(to);
}
}
/**
* Clears the cached values set by {@link #getStartCellReference()}
* and {@link #getEndCellReference()}.
* The next call to {@link #getStartCellReference()} and
* {@link #getEndCellReference()} will synchronize the
* cell references with the underlying <code>CTTable</code>.
* Thus, {@link #updateReferences()} is inexpensive.
*
* @since POI 3.15 beta 3
*/
public void UpdateReferences()
{
startCellReference = null;
endCellReference = null;
}
/**
* @return the total number of rows in the selection. (Note: in this version autofiltering is ignored)
* Returns 0 if the start or end cell references are not set.
*
* To synchronize with changes to the underlying CTTable,
* call {@link #updateReferences()}.
*/
public int RowCount
{
get
{
CellReference from = StartCellReference;
CellReference to = EndCellReference;
int rowCount = 0;
if (from != null && to != null)
{
rowCount = to.Row - from.Row + 1;
}
return rowCount;
}
}
/**
* Synchronize table headers with cell values in the parent sheet.
* Headers <em>must</em> be in sync, otherwise Excel will display a
* "Found unreadable content" message on startup.
*
* If calling both {@link #updateReferences()} and
* {@link #updateHeaders()}, {@link #updateReferences()}
* should be called first.
*/
public void UpdateHeaders()
{
XSSFSheet sheet = (XSSFSheet)GetParent();
CellReference ref1 = StartCellReference;
if (ref1 == null) return;
int headerRow = ref1.Row;
int firstHeaderColumn = ref1.Col;
XSSFRow row = sheet.GetRow(headerRow) as XSSFRow;
if (row != null && row.GetCTRow() != null)
{
int cellnum = firstHeaderColumn;
CT_TableColumns tableColumns = GetCTTable().tableColumns;
if (tableColumns != null)
{
foreach (CT_TableColumn col in tableColumns.tableColumn)
{
if (row.GetCell(cellnum) is XSSFCell cell)
{
col.name = cell.StringCellValue;
}
cellnum++;
}
}
}
tableColumns = null;
columnMap = null;
xmlColumnPrs = null;
commonXPath = null;
}
/**
* Gets the relative column index of a column in this table having the header name <code>column</code>.
* The column index is relative to the left-most column in the table, 0-indexed.
* Returns <code>-1</code> if <code>column</code> is not a header name in table.
*
* Column Header names are case-insensitive
*
* Note: this function caches column names for performance. To flush the cache (because columns
* have been moved or column headers have been changed), {@link #updateHeaders()} must be called.
*
* @since 3.15 beta 2
*/
public int FindColumnIndex(String columnHeader)
{
if (columnHeader == null) return -1;
if (columnMap == null)
{
int count = ColumnCount;
columnMap = new Dictionary<string, int>(count * 3 / 2);
int i = 0;
foreach (XSSFTableColumn column in GetColumns())
{
columnMap.Add(column.Name.ToUpper(CultureInfo.CurrentCulture), i);
i++;
}
}
// Table column names with special characters need a single quote escape
// but the escape is not present in the column definition
int idx = -1;
string testKey = columnHeader.Replace("'", "").ToUpper(CultureInfo.CurrentCulture);
if (columnMap.TryGetValue(testKey, out int value))
idx = value;
return idx;
}
/// <summary>
/// Note this list is static - once read, it does not notice later changes to the underlying column structures
/// </summary>
/// <returns></returns>
public List<XSSFTableColumn> GetColumns()
{
if (tableColumns == null)
{
var columns = new List<XSSFTableColumn>();
CT_TableColumns ctTableColumns = ctTable.tableColumns;
if (ctTableColumns != null)
{
foreach (CT_TableColumn column in ctTableColumns.GetTableColumnList())
{
XSSFTableColumn tableColumn = new XSSFTableColumn(this, column);
columns.Add(tableColumn);
}
}
tableColumns = columns;
}
return tableColumns;
}
public void RemoveColumn(XSSFTableColumn column)
{
int columnIndex = GetColumns().IndexOf(column);
if (columnIndex >= 0)
{
ctTable.tableColumns.RemoveTableColumn(columnIndex);
UpdateReferences();
UpdateHeaders();
}
}
public String SheetName
{
get
{
return GetXSSFSheet().SheetName;
}
}
/// <summary>
/// This is misleading. The Spec indicates this is true if the totals row
/// has<b><i>ever</i></b> been shown, not whether or not it is currently displayed.
/// </summary>
public bool IsHasTotalsRow
{
get
{
return ctTable.totalsRowShown;
}
set
{
ctTable.totalsRowShown = value;
}
}
public int StartColIndex
{
get
{
return StartCellReference.Col;
}
}
public int StartRowIndex
{
get
{
return StartCellReference.Row;
}
}
public int EndColIndex
{
get
{
return EndCellReference.Col;
}
}
public int EndRowIndex
{
get
{
return EndCellReference.Row;
}
}
public bool Contains(CellReference cell)
{
if (cell == null) return false;
// check if cell is on the same sheet as the table
if (! SheetName.Equals(cell.SheetName)) return false;
// check if the cell is inside the table
if (cell.Row >= StartRowIndex
&& cell.Row <= EndRowIndex
&& cell.Col >= StartColIndex
&& cell.Col <= EndColIndex)
{
return true;
}
return false;
}
}
}