Dépôt officiel du code source de l'ERP OpenConcerto
Rev 93 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.openoffice.spreadsheet;
import org.openconcerto.openoffice.Length;
import org.openconcerto.openoffice.LengthUnit;
import org.openconcerto.openoffice.ODDocument;
import org.openconcerto.openoffice.Style;
import org.openconcerto.openoffice.StyleDesc;
import org.openconcerto.openoffice.StyleStyleDesc;
import org.openconcerto.openoffice.StyledNode;
import org.openconcerto.openoffice.XMLVersion;
import org.openconcerto.openoffice.spreadsheet.CellStyle.StyleTableCellProperties;
import org.openconcerto.openoffice.spreadsheet.SheetTableModel.MutableTableModel;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.Tuple2;
import org.openconcerto.xml.JDOMUtils;
import org.openconcerto.xml.SimpleXMLPath;
import java.awt.Point;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import javax.swing.table.TableModel;
import org.jdom.Attribute;
import org.jdom.Element;
/**
* A single sheet in a spreadsheet.
*
* @author Sylvain
* @param <D> type of table parent
*/
public class Table<D extends ODDocument> extends TableCalcNode<TableStyle, D> {
static Element createEmpty(XMLVersion ns) {
return createEmpty(ns, 1, 1);
}
static Element createEmpty(final XMLVersion ns, final int colCount, final int rowCount) {
return createEmpty(ns, colCount, rowCount, null);
}
// pass styleName to avoid creating possibly expensive Table instance (readCols/readRows) in
// order to use setStyleName()
static Element createEmpty(final XMLVersion ns, final int colCount, final int rowCount, final String styleName) {
// from the relaxNG
if (colCount < 1 || rowCount < 1)
throw new IllegalArgumentException("a table must have at least one cell");
final Element col = Column.createEmpty(ns, null);
Axis.COLUMN.setRepeated(col, colCount);
final Element row = Row.createEmpty(ns).addContent(Cell.createEmpty(ns, colCount));
Axis.ROW.setRepeated(row, rowCount);
final Element res = new Element("table", ns.getTABLE()).addContent(col).addContent(row);
StyledNode.setStyleName(res, styleName);
return res;
}
static final String getName(final Element elem) {
return elem.getAttributeValue("name", elem.getNamespace("table"));
}
// ATTN Row have their index as attribute
private final ArrayList<Row<D>> rows;
private TableGroup rowGroup;
private final ArrayList<Column<D>> cols;
private TableGroup columnGroup;
public Table(D parent, Element local) {
super(parent, local, TableStyle.class);
this.rows = new ArrayList<Row<D>>(64);
this.cols = new ArrayList<Column<D>>(32);
// read columns first since Row constructor needs it
this.readColumns();
this.readRows();
}
private void readColumns() {
this.read(Axis.COLUMN);
}
private final void readRows() {
this.read(Axis.ROW);
}
private final void read(final Axis axis) {
final boolean col = axis == Axis.COLUMN;
final Tuple2<TableGroup, List<Element>> r = TableGroup.createRoot(this, axis);
final ArrayList<?> l = col ? this.cols : this.rows;
final int oldSize = l.size();
l.clear();
final int newSize = r.get0().getSize();
l.ensureCapacity(newSize);
if (col) {
final StyleStyleDesc<ColumnStyle> colStyleDesc = getColumnStyleDesc();
for (final Element clone : r.get1())
this.addCol(clone, colStyleDesc);
this.columnGroup = r.get0();
} else {
final StyleStyleDesc<RowStyle> rowStyleDesc = getRowStyleDesc();
final StyleStyleDesc<CellStyle> cellStyleDesc = getCellStyleDesc();
for (final Element clone : r.get1())
this.addRow(clone, rowStyleDesc, cellStyleDesc);
this.rowGroup = r.get0();
}
// this always copy the array, so make sure we reclaim enough memory (~ 64k)
if (oldSize - newSize > 8192) {
l.trimToSize();
}
assert newSize == (col ? this.getColumnCount() : this.getRowCount());
}
private final void addCol(Element clone, StyleStyleDesc<ColumnStyle> colStyleDesc) {
this.cols.add(new Column<D>(this, clone, colStyleDesc));
}
static final int flattenChildren(final List<Element> res, final Element elem, final Axis axis) {
int count = 0;
// array so that flatten1() can modify an int
int[] index = new int[] { 0 };
// copy since we will change our children (don't use List.listIterator(int) since it
// re-filters all content)
@SuppressWarnings("unchecked")
final List<Element> children = new ArrayList<Element>(elem.getChildren(axis.getElemName(), elem.getNamespace()));
final int stop = children.size();
for (int i = 0; i < stop; i++) {
final Element row = children.get(i);
count += flatten1(res, row, axis, index);
}
return count;
}
static int flatten1(final List<Element> res, final Element row, final Axis axis) {
return flatten1(res, row, axis, null);
}
// add XML elements to res and return the logical count
private static int flatten1(final List<Element> res, final Element row, final Axis axis, final int[] parentIndex) {
final int resSize = res.size();
final Attribute repeatedAttr = axis.getRepeatedAttr(row);
final int repeated = repeatedAttr == null ? 1 : Integer.parseInt(repeatedAttr.getValue());
if (axis == Axis.COLUMN && repeated > 1) {
row.removeAttribute(repeatedAttr);
final Element parent = row.getParentElement();
final int index = (parentIndex == null ? parent.indexOf(row) : parentIndex[0]) + 1;
res.add(row);
// -1 : we keep the original row
for (int i = 0; i < repeated - 1; i++) {
final Element clone = (Element) row.clone();
res.add(clone);
parent.addContent(index + i, clone);
}
} else {
res.add(row);
}
if (parentIndex != null)
parentIndex[0] += res.size() - resSize;
return repeated;
}
public final String getName() {
return getName(this.getElement());
}
public final void setName(String name) {
this.getElement().setAttribute("name", name, this.getODDocument().getVersion().getTABLE());
}
public void detach() {
this.getElement().detach();
}
public final Object getPrintRanges() {
return this.getElement().getAttributeValue("print-ranges", this.getTABLE());
}
public final void setPrintRanges(String s) {
this.getElement().setAttribute("print-ranges", s, this.getTABLE());
}
public final void removePrintRanges() {
this.getElement().removeAttribute("print-ranges", this.getTABLE());
}
public final synchronized void duplicateFirstRows(int nbFirstRows, int nbDuplicate) {
this.duplicateRows(0, nbFirstRows, nbDuplicate);
}
public final synchronized void insertDuplicatedRows(int rowDuplicated, int nbDuplicate) {
this.duplicateRows(rowDuplicated, 1, nbDuplicate);
}
/**
* Clone a range of rows. Eg if you want to copy once rows 2 through 5, you call
* <code>duplicateRows(2, 4, 1)</code>.
*
* @param start the first row to clone.
* @param count the number of rows after <code>start</code> to clone, i.e. 1 means clone one
* row, must be greater or equal to 0.
* @param copies the number of copies of the range to make, must be greater or equal to 0.
* @throws IllegalArgumentException if arguments are not valid.
*/
public final void duplicateRows(final int start, final int count, final int copies) throws IllegalArgumentException {
this.duplicateRows(start, count, copies, true);
}
public final synchronized void duplicateRows(final int start, final int count, final int copies, final boolean updateCellAddresses) throws IllegalArgumentException {
if (start < 0)
throw new IllegalArgumentException("Negative start index : " + start);
if (count < 0)
throw new IllegalArgumentException("Negative count : " + count);
else if (count == 0)
return;
if (copies < 0)
throw new IllegalArgumentException("Negative copies : " + copies);
else if (copies == 0)
return;
// stop is excluded
final int stop = start + count;
if (stop > this.getRowCount())
throw new IllegalArgumentException("Last row to duplicate (" + (stop - 1) + ") doesn't exist, there's only : " + getRowCount());
// should not change merged status
final Map<Point, Integer> coverOrigins = new HashMap<Point, Integer>();
final List<Point> coverOriginsToUpdate = new ArrayList<Point>();
final int colCount = this.getColumnCount();
for (int x = 0; x < colCount;) {
int y = start;
while (y < stop) {
final Point coverOrigin = this.getCoverOrigin(x, y);
if (coverOrigin == null) {
y++;
} else {
final int lastCoveredCellRow;
// if we have already encountered this merged cell, skip it
if (coverOrigins.containsKey(coverOrigin)) {
lastCoveredCellRow = coverOrigins.get(coverOrigin);
} else {
final Cell<D> covering = this.getImmutableCellAt(coverOrigin.x, coverOrigin.y);
lastCoveredCellRow = coverOrigin.y + covering.getRowsSpanned() - 1;
if (coverOrigin.y < start) {
if (lastCoveredCellRow < stop - 1)
throw new IllegalArgumentException("Span starts before the duplicated rows and doesn't extend past the end of duplicated rows at " + getAddress(coverOrigin));
} else {
if (lastCoveredCellRow > stop - 1)
throw new IllegalArgumentException("Span starts in the duplicated rows and extend past the end of duplicated rows at " + getAddress(coverOrigin));
}
coverOrigins.put(coverOrigin, lastCoveredCellRow);
// merged cells inside the duplicated rows don't need to be updated
if (coverOrigin.y < start || lastCoveredCellRow > stop - 1)
coverOriginsToUpdate.add(coverOrigin);
}
y = lastCoveredCellRow + 1;
}
}
x++;
}
// clone xml elements and add them to our tree
final List<Element> clones = new ArrayList<Element>(count * copies);
for (int l = start; l < stop;) {
final Row<D> immutableRow = this.getRow(l);
final Row<D> toClone;
// MAYBE use something else than getMutableRow() since we don't need a single row.
// the repeated row starts before the copied range, split it at the beginning
if (immutableRow.getY() < l) {
toClone = this.getMutableRow(l);
} else {
assert immutableRow.getY() == l;
if (immutableRow.getLastY() >= stop) {
// the repeated row goes beyond the copied range, split it at the end
assert this.getRow(stop) == immutableRow;
this.getMutableRow(stop);
toClone = this.getRow(l);
} else {
toClone = immutableRow;
}
}
assert toClone.getY() == l;
assert toClone.getLastY() < stop : "Row goes to far";
l += toClone.getRepeated();
clones.add((Element) toClone.getElement().clone());
}
final int clonesSize = clones.size();
for (int i = 1; i < copies; i++) {
for (int j = 0; j < clonesSize; j++) {
clones.add((Element) clones.get(j).clone());
}
}
// works anywhere its XML element is
assert this.getRow(stop - 1).getLastY() == stop - 1 : "Adding XML element too far";
JDOMUtils.insertAfter(this.getRow(stop - 1).getElement(), clones);
for (final Point coverOrigin : coverOriginsToUpdate) {
final MutableCell<D> coveringCell = getCellAt(coverOrigin);
coveringCell.setRowsSpanned(coveringCell.getRowsSpanned() + count * copies);
}
// synchronize our rows with our new tree (rows' index have changed)
this.readRows();
// 19.627 in OpenDocument-v1.2-cs01-part1 : The table:end-cell-address attribute specifies
// end position of the shape if it is included in a spreadsheet document.
if (updateCellAddresses && getODDocument() instanceof SpreadSheet) {
final SpreadSheet ssheet = (SpreadSheet) getODDocument();
final SimpleXMLPath<Attribute> descAttrs = SimpleXMLPath.allAttributes("end-cell-address", "table");
for (final Attribute endCellAttr : descAttrs.selectNodes(getElement())) {
final Tuple2<Sheet, Point> resolved = ssheet.resolve(endCellAttr.getValue());
final Sheet endCellSheet = resolved.get0();
if (endCellSheet != this)
throw new UnsupportedOperationException("End sheet is not this : " + endCellSheet);
final Point endCellPoint = resolved.get1();
// if the end point is before the copied rows, nothing to do
if (endCellPoint.y >= start) {
final Element endCellParentElem = endCellAttr.getParent();
// find row index of the shape
final Element rowElem = JDOMUtils.getAncestor(endCellParentElem, "table-row", getTABLE());
if (rowElem == null)
throw new IllegalStateException("Not in a row : " + JDOMUtils.output(endCellParentElem));
int startRowIndex = -1;
final int rowCount = getRowCount();
for (int i = 0; i < rowCount; i++) {
if (getRow(i).getElement() == rowElem) {
startRowIndex = i;
break;
}
}
if (startRowIndex < 0)
throw new IllegalStateException("Row not found for " + JDOMUtils.output(endCellParentElem));
final int newEndY;
if (startRowIndex >= start + (copies + 1) * count) {
// if the shape doesn't span over the copied rows, only need to offset
// end-cell-address
newEndY = endCellPoint.y + copies * count;
} else if (startRowIndex >= start + count && endCellPoint.y < start + count) {
// if the shape was copied and its end cell too, translate it
// find in which copy the shape is in, ATTN the truncation is important
// since the shape might not be in the first copied row
final int nth = (startRowIndex - start) / count;
newEndY = endCellPoint.y + nth * count;
} else {
// we must use height to compute new values for end-cell-address and end-y
// find the height of the shape
final Length[] coordinates = getODDocument().getFormatVersion().getXML().getCoordinates(endCellParentElem, false, true);
if (coordinates == null)
throw new IllegalStateException("Couldn't find the height of the shape : " + JDOMUtils.output(endCellParentElem));
final Length endYFromAnchor = coordinates[3];
assert endYFromAnchor != null : "getCoordinates() should never return null BigDecimal (unless requested by horizontal/vertical)";
// find the end row
int rowIndex = startRowIndex;
Length cellEndYFromAnchor = getRow(rowIndex).getStyle().getTableRowProperties().getHeight();
while (endYFromAnchor.compareTo(cellEndYFromAnchor) > 0) {
rowIndex++;
cellEndYFromAnchor = cellEndYFromAnchor.add(getRow(rowIndex).getStyle().getTableRowProperties().getHeight());
}
// find the end-y
final Length cellStartYFromAnchor = cellEndYFromAnchor.subtract(getRow(rowIndex).getStyle().getTableRowProperties().getHeight());
final Length endY = endYFromAnchor.subtract(cellStartYFromAnchor);
assert endY.signum() >= 0;
newEndY = rowIndex;
endCellParentElem.setAttribute("end-y", endY.format(), getTABLE());
}
endCellAttr.setValue(SpreadSheet.formatSheetName(endCellSheet.getName()) + "." + Table.getAddress(new Point(endCellPoint.x, newEndY)));
}
}
}
}
private synchronized void addRow(Element child, StyleDesc<RowStyle> styleDesc, StyleDesc<CellStyle> cellStyleDesc) {
final Row<D> row = new Row<D>(this, child, this.rows.size(), styleDesc, cellStyleDesc);
final int toRepeat = row.getRepeated();
for (int i = 0; i < toRepeat; i++) {
this.rows.add(row);
}
}
public final Point resolveHint(String ref) {
final Point res = resolve(ref);
if (res != null) {
return res;
} else
throw new IllegalArgumentException(ref + " is not a cell ref, if it's a named range, you must use it on a SpreadSheet.");
}
// *** set cell
public final boolean isCellValid(int x, int y) {
if (x > this.getColumnCount())
return false;
else if (y > this.getRowCount())
return false;
else
return this.getImmutableCellAt(x, y).isValid();
}
/**
* Return a modifiable cell at the passed coordinates. This is slower than
* {@link #getImmutableCellAt(int, int)} since this method may modify the underlying XML (e.g.
* break up repeated cells to allow for modification of only the returned cell).
*
* @param x the column.
* @param y the row.
* @return the cell.
* @see #getImmutableCellAt(int, int)
*/
public final MutableCell<D> getCellAt(int x, int y) {
final Row<D> r = this.getMutableRow(y);
try {
return r.getMutableCellAt(x);
} catch (Exception e) {
throw new IllegalArgumentException("Couldn't get mutable cell at " + getAddress(x, y), e);
}
}
public final MutableCell<D> getCellAt(String ref) {
return this.getCellAt(resolveHint(ref));
}
final MutableCell<D> getCellAt(Point p) {
return this.getCellAt(p.x, p.y);
}
/**
* Sets the value at the specified coordinates.
*
* @param val the new value, <code>null</code> will be treated as "".
* @param x the column.
* @param y the row.
*/
public final void setValueAt(Object val, int x, int y) {
if (val == null)
val = "";
// ne pas casser les repeated pour rien
if (!val.equals(this.getValueAt(x, y)))
this.getCellAt(x, y).setValue(val);
}
// *** get cell
/**
* Return a non modifiable cell at the passed coordinates. This is faster than
* {@link #getCellAt(int, int)} since this method never modifies the underlying XML.
*
* @param x the column.
* @param y the row.
* @return the cell.
* @see #getCellAt(int, int)
*/
public final Cell<D> getImmutableCellAt(int x, int y) {
return this.getRow(y).getCellAt(x);
}
public final Cell<D> getImmutableCellAt(String ref) {
final Point p = resolveHint(ref);
return this.getImmutableCellAt(p.x, p.y);
}
/**
* Return the origin of a merged cell.
*
* @param x the column.
* @param y the row.
* @return the point of origin, <code>null</code> if there's no merged cell at the passed
* coordinates.
*/
public final Point getCoverOrigin(final int x, final int y) {
// can't return a Cell, since it has no x
// don't return a MutableCell since it is costly
final Cell<D> c = this.getImmutableCellAt(x, y);
if (c.coversOtherCells()) {
return new Point(x, y);
} else if (!c.isCovered()) {
return null;
} else {
final Row<D> row = this.getRow(y);
Cell<D> currentCell = c;
int currentX = x;
while (currentX > 0 && currentCell.isCovered()) {
currentX--;
currentCell = row.getCellAt(currentX);
}
if (currentCell.coversOtherCells())
return new Point(currentX, y);
if (!currentCell.isCovered()) {
currentX++;
currentCell = row.getCellAt(currentX);
}
assert currentCell.isCovered();
int currentY = y;
while (!currentCell.coversOtherCells()) {
currentY--;
currentCell = this.getImmutableCellAt(currentX, currentY);
}
return new Point(currentX, currentY);
}
}
/**
* The value of the cell at the passed coordinates.
*
* @param row row index from <code>0</code> to <code>{@link #getRowCount() row count}-1</code>
* @param column column index from <code>0</code> to
* <code>{@link #getColumnCount() column count}-1</code>
* @return the value at the passed coordinates.
* @see #getValueAt(String)
* @see Cell#getValue()
*/
public final Object getValueAt(int column, int row) {
return this.getImmutableCellAt(column, row).getValue();
}
/**
* Find the style name for the specified cell.
*
* @param column column index.
* @param row row index.
* @return the style name, can be <code>null</code>.
*/
public final String getStyleNameAt(int column, int row) {
// first the cell
String cellStyle = this.getImmutableCellAt(column, row).getStyleAttr();
if (cellStyle != null)
return cellStyle;
// then the row (as specified in §2 of section 8.1)
// in reality LO never generates table-row/@default-cell-style-name and its behavior is
// really unpredictable if it opens files with it
if (Style.isStandardStyleResolution()) {
cellStyle = this.getRow(row).getElement().getAttributeValue("default-cell-style-name", getTABLE());
if (cellStyle != null)
return cellStyle;
}
// and finally the column
return this.getColumn(column).getElement().getAttributeValue("default-cell-style-name", getTABLE());
}
public final CellStyle getStyleAt(int column, int row) {
return getCellStyleDesc().findStyleForNode(this.getImmutableCellAt(column, row), this.getStyleNameAt(column, row));
}
public final StyleTableCellProperties getTableCellPropertiesAt(int column, int row) {
final CellStyle styleAt = this.getStyleAt(column, row);
return styleAt == null ? null : styleAt.getTableCellProperties(this.getImmutableCellAt(column, row));
}
protected StyleStyleDesc<CellStyle> getCellStyleDesc() {
return Style.getStyleStyleDesc(CellStyle.class, getODDocument().getVersion());
}
public final CellStyle getDefaultCellStyle() {
return getCellStyleDesc().findDefaultStyle(this.getODDocument().getPackage());
}
/**
* Return the coordinates of cells using the passed style.
*
* @param cellStyleName a style name.
* @return the cells using <code>cellStyleName</code>.
*/
public final List<Tuple2<Integer, Integer>> getStyleReferences(final String cellStyleName) {
final List<Tuple2<Integer, Integer>> res = new ArrayList<Tuple2<Integer, Integer>>();
final Set<Integer> cols = new HashSet<Integer>();
final int columnCount = getColumnCount();
for (int i = 0; i < columnCount; i++) {
if (cellStyleName.equals(this.getColumn(i).getElement().getAttributeValue("default-cell-style-name", getTABLE())))
cols.add(i);
}
final int rowCount = getRowCount();
for (int y = 0; y < rowCount; y++) {
final Row<D> row = this.getRow(y);
final String rowStyle = row.getElement().getAttributeValue("default-cell-style-name", getTABLE());
for (int x = 0; x < columnCount; x++) {
final String cellStyle = row.getCellAt(x).getStyleAttr();
final boolean match;
// first the cell
if (cellStyle != null)
match = cellStyleName.equals(cellStyle);
// then the row (as specified in §2 of section 8.1)
else if (rowStyle != null)
match = cellStyleName.equals(rowStyle);
// and finally the column
else
match = cols.contains(x);
if (match)
res.add(Tuple2.create(x, y));
}
}
return res;
}
protected final StyleStyleDesc<ColumnStyle> getColumnStyleDesc() {
return Style.getStyleStyleDesc(ColumnStyle.class, XMLVersion.getVersion(getElement()));
}
protected final StyleStyleDesc<RowStyle> getRowStyleDesc() {
return Style.getStyleStyleDesc(RowStyle.class, XMLVersion.getVersion(getElement()));
}
/**
* The value of the cell at the passed coordinates.
*
* @param ref a reference, e.g. "A3".
* @return the value at the passed coordinates.
* @see #getValueAt(int, int)
* @see Cell#getValue()
*/
public final Object getValueAt(String ref) {
return this.getImmutableCellAt(ref).getValue();
}
// *** get count
// not public since Row represent the physical (XML) row, i.e. it can represent many logical
// rows. Ideally two classes should be created (as Cell/MutableCell), but this would only be
// useful for changing style (e.g. setHeight()).
final Row<D> getRow(int index) {
return this.rows.get(index);
}
final Row<D> getMutableRow(int y) {
final Row<D> c = this.getRow(y);
if (c.getRepeated() > 1) {
RepeatedBreaker.<D> getRowBreaker().breakRepeated(this, this.rows, y);
return this.getRow(y);
} else {
return c;
}
}
// OK to use immutable row since for now RowStyle has no setter (except for raw methods in super
// classes)
public final RowStyle getRowStyle(int index) {
return this.getRow(index).getStyle();
}
public final Column<D> getColumn(int i) {
return this.cols.get(i);
}
public final ColumnStyle getColumnStyle(int index) {
return this.getColumn(index).getStyle();
}
public final int getRowCount() {
return this.rows.size();
}
public final TableGroup getRowGroup() {
return this.rowGroup;
}
/**
* Return the deepest group at the passed row.
*
* @param y a row index.
* @return the group at the index, never <code>null</code>.
*/
public final TableGroup getRowGroupAt(final int y) {
return this.getRowGroup().getDescendentOrSelfContaining(y);
}
public final int getHeaderRowCount() {
return this.getRowGroup().getFollowingHeaderCount();
}
public final int getColumnCount() {
return this.cols.size();
}
public final TableGroup getColumnGroup() {
return this.columnGroup;
}
/**
* Return the deepest group at the passed column.
*
* @param x a column index.
* @return the group at the index, never <code>null</code>.
*/
public final TableGroup getColumnGroupAt(final int x) {
return this.getColumnGroup().getDescendentOrSelfContaining(x);
}
public final int getHeaderColumnCount() {
return this.getColumnGroup().getFollowingHeaderCount();
}
// *** set count
/**
* Changes the column count without keeping the table width.
*
* @param newSize the new column count.
* @see #setColumnCount(int, int, boolean)
*/
public final void setColumnCount(int newSize) {
this.setColumnCount(newSize, -1, false);
}
/**
* Assure that this sheet has at least <code>newSize</code> columns.
*
* @param newSize the minimum column count this table should have.
*/
public final void ensureColumnCount(int newSize) {
if (newSize > this.getColumnCount())
this.setColumnCount(newSize);
}
/**
* Changes the column count. If <code>newSize</code> is less than {@link #getColumnCount()}
* extra cells will be chopped off. Otherwise empty cells will be created.
*
* @param newSize the new column count.
* @param colIndex the index of the column to be copied, -1 for empty column (and no style).
* @param keepTableWidth <code>true</code> if the table should be same width after the column
* change.
*/
public final void setColumnCount(int newSize, int colIndex, final boolean keepTableWidth) {
this.setColumnCount(newSize, colIndex, null, keepTableWidth);
}
/**
* Changes the column count. If <code>newSize</code> is less than {@link #getColumnCount()}
* extra cells will be chopped off. Otherwise empty cells will be created.
*
* @param newSize the new column count.
* @param colStyle the style of the new columns, <code>null</code> for no style (throws
* exception if the table has a non-null width).
* @param keepTableWidth <code>true</code> if the table should be same width after the column
* change.
* @see #createColumnStyle(Number, LengthUnit)
*/
public final void setColumnCount(int newSize, ColumnStyle colStyle, final boolean keepTableWidth) {
this.setColumnCount(newSize, -1, colStyle, keepTableWidth);
}
private final void setColumnCount(int newSize, int colIndex, ColumnStyle colStyle, final boolean keepTableWidth) {
final int toGrow = newSize - this.getColumnCount();
if (toGrow < 0) {
this.removeColumn(newSize, this.getColumnCount(), keepTableWidth);
} else if (toGrow > 0) {
// the list of columns cannot be mixed with other elements
// so just keep adding after the last one
final int indexOfLastCol;
if (this.getColumnCount() == 0)
// from section 8.1.1 the only possible elements after cols are rows
// but there can't be rows w/o columns, so just add to the end
indexOfLastCol = this.getElement().getContentSize() - 1;
else
indexOfLastCol = this.getElement().getContent().indexOf(this.getColumn(this.getColumnCount() - 1).getElement());
final Element elemToClone;
if (colIndex < 0) {
elemToClone = Column.createEmpty(getODDocument().getVersion(), colStyle);
} else {
elemToClone = getColumn(colIndex).getElement();
}
final StyleStyleDesc<ColumnStyle> columnStyleDesc = getColumnStyleDesc();
for (int i = 0; i < toGrow; i++) {
final Element newElem = (Element) elemToClone.clone();
this.getElement().addContent(indexOfLastCol + 1 + i, newElem);
this.cols.add(new Column<D>(this, newElem, columnStyleDesc));
}
// now update widths
updateWidth(keepTableWidth);
// add needed cells
final StyleStyleDesc<CellStyle> cellStyleDesc = this.getCellStyleDesc();
final int rowCount = this.getRowCount();
for (int i = 0; i < rowCount;) {
final Row<D> r = this.getRow(i);
r.columnCountChanged(cellStyleDesc);
i += r.getRepeated();
}
}
}
public final void removeColumn(int colIndex, final boolean keepTableWidth) {
this.removeColumn(colIndex, colIndex + 1, keepTableWidth);
}
/**
* Remove columns from this. As with OpenOffice, no cell must be covered in the column to
* remove. ATTN <code>keepTableWidth</code> only works for tables in text document that are not
* aligned automatically (ie fill the entire page). ATTN spreadsheet applications may hide from
* you the real width of sheets, eg display only columns A to AJ when in reality there's
* hundreds of blank columns beyond. Thus if you pass <code>true</code> to
* <code>keepTableWidth</code> you'll end up with huge widths.
*
* @param firstIndex the first column to remove.
* @param lastIndex the last column to remove, exclusive.
* @param keepTableWidth <code>true</code> if the table should be same width after the column
* change.
*/
public final void removeColumn(int firstIndex, int lastIndex, final boolean keepTableWidth) {
// first check that removeCells() will succeed, so that we avoid an incoherent XML state
final int rowCount = this.getRowCount();
for (int i = 0; i < rowCount;) {
final Row<D> r = this.getRow(i);
r.checkRemove(firstIndex, lastIndex);
i += r.getRepeated();
}
// rm column element
remove(Axis.COLUMN, firstIndex, lastIndex - 1);
// update widths
updateWidth(keepTableWidth);
// rm cells
for (int i = 0; i < rowCount;) {
final Row<D> r = this.getRow(i);
r.removeCells(firstIndex, lastIndex);
i += r.getRepeated();
}
}
void updateWidth(final boolean keepTableWidth) {
// tables widths must have a lower precision than columns since they can't be the exact sum
// of them.
final Length currentWidth = getWidth().roundDecimalAmount();
Length newWidth = Length.ZERO;
Column<?> nullWidthCol = null;
// columns are flattened in ctor: no repeated
for (final Column<?> col : this.cols) {
final Length colWidth = col.getWidth();
if (colWidth.isDefined()) {
assert colWidth.signum() >= 0;
newWidth = newWidth.add(colWidth);
} else {
// we cannot compute the newWidth
newWidth = Length.getNone();
nullWidthCol = col;
break;
}
}
// tables widths must have a lower precision than columns since they can't be the exact sum
// of them.
newWidth = newWidth.roundDecimalAmount();
// remove all rel-column-width, simpler and Spreadsheet doesn't use them
// SpreadSheets have no table width
if (keepTableWidth && currentWidth.isDefined()) {
if (nullWidthCol != null)
throw new IllegalStateException("Cannot keep width since a column has no width : " + nullWidthCol);
// compute column-width from table width
final Number ratio = currentWidth.divide(newWidth);
// once per style not once per col, otherwise if multiple columns with same styles they
// all will be affected multiple times
final Set<ColumnStyle> colStyles = new HashSet<ColumnStyle>();
for (final Column<?> col : this.cols) {
colStyles.add(col.getStyle());
}
for (final ColumnStyle colStyle : colStyles) {
colStyle.setWidth(colStyle.getWidth().multiply(ratio));
}
} else {
// compute table width from column-width
final TableStyle style = this.getStyle();
if (style != null) {
if (nullWidthCol == null)
style.setWidth(newWidth);
else if (currentWidth.isDefined())
throw new IllegalStateException("Cannot update table width since a column has no width : " + nullWidthCol);
// else currentWidth undefined, no inconsistency, nothing to update
}
// either there's no table width or it's positive and equal to the sum of its columns
assert style == null || style.getWidth().isNone() || (!newWidth.isNone() && newWidth.compareTo(style.getWidth()) == 0);
for (final Column<?> col : this.cols) {
final ColumnStyle colStyle = col.getStyle();
// if no style, nothing to remove
if (colStyle != null)
colStyle.rmRelWidth();
}
}
}
/**
* Table width.
*
* @return the table width, can be <code>null</code> (table has no style or style has no width,
* eg in SpreadSheet).
*/
public final Length getWidth() {
return this.getStyle().getWidth();
}
/**
* Create and add an automatic style.
*
* @param amount the column width.
* @param unit the unit of <code>amount</code>.
* @return the newly added style.
*/
public final ColumnStyle createColumnStyle(final Number amount, final LengthUnit unit) {
return createColumnStyle(amount == null ? Length.getNone() : Length.create(amount, unit));
}
public final ColumnStyle createColumnStyle(final Length l) {
final ColumnStyle colStyle = this.getStyleDesc(ColumnStyle.class).createAutoStyle(this.getODDocument().getPackage());
colStyle.setWidth(l);
return colStyle;
}
private final void setCount(final Axis col, final int newSize) {
this.remove(col, newSize, -1);
}
private final void remove(final Axis col, final int fromIndex, final int toIndexIncl) {
this.remove(col, fromIndex, toIndexIncl, true);
}
// both inclusive
private final void remove(final Axis col, final int fromIndex, final int toIndexIncl, final boolean updateList) {
assert col == Axis.COLUMN || !updateList || toIndexIncl < 0 : "Row index will be wrong";
final List<? extends TableCalcNode<?, ?>> l = col == Axis.COLUMN ? this.cols : this.rows;
final int toIndexValid = CollectionUtils.getValidIndex(l, toIndexIncl);
final int toRemoveCount = toIndexValid - fromIndex + 1;
int removedCount = 0;
while (removedCount < toRemoveCount) {
// works backwards to keep y OK
final int i = toIndexValid - removedCount;
final TableCalcNode<?, ?> removed = l.get(i);
if (removed instanceof Row) {
final Row<?> r = (Row<?>) removed;
final int removeFromRepeated = i - Math.max(fromIndex, r.getY()) + 1;
// removedCount grows each iteration
assert removeFromRepeated > 0;
final int newRepeated = r.getRepeated() - removeFromRepeated;
if (newRepeated == 0)
removed.getElement().detach();
else
r.setRepeated(newRepeated);
removedCount += removeFromRepeated;
} else {
// Columns are always flattened
removed.getElement().detach();
removedCount++;
}
}
// one remove to be efficient
if (updateList)
l.subList(fromIndex, toIndexValid + 1).clear();
}
public final void ensureRowCount(int newSize) {
if (newSize > this.getRowCount())
this.setRowCount(newSize);
}
public final void setRowCount(int newSize) {
this.setRowCount(newSize, -1);
}
/**
* Changes the row count. If <code>newSize</code> is less than {@link #getRowCount()} extra rows
* will be chopped off. Otherwise empty cells will be created.
*
* @param newSize the new row count.
* @param rowIndex the index of the row to be copied, -1 for empty row (i.e. default style).
*/
public final void setRowCount(int newSize, int rowIndex) {
final int toGrow = newSize - this.getRowCount();
if (toGrow < 0) {
setCount(Axis.ROW, newSize);
} else if (toGrow > 0) {
final Element elemToClone;
if (rowIndex < 0) {
elemToClone = Row.createEmpty(this.getODDocument().getVersion());
// each row MUST have the same number of columns
elemToClone.addContent(Cell.createEmpty(this.getODDocument().getVersion(), this.getColumnCount()));
} else {
elemToClone = (Element) getRow(rowIndex).getElement().clone();
}
Axis.ROW.setRepeated(elemToClone, toGrow);
// as per section 8.1.1 rows are the last elements inside a table
this.getElement().addContent(elemToClone);
addRow(elemToClone, getRowStyleDesc(), getCellStyleDesc());
}
}
public final void removeRow(int index) {
this.removeRows(index, index + 1);
}
/**
* Remove rows between the two passed indexes. If the origin of a merged cell is contained in
* the rows to remove, then it will be unmerged, otherwise (the origin is before the rows) the
* merged cell will just be shrunk.
*
* @param firstIndex the start index, inclusive.
* @param lastIndex the stop index, exclusive.
*/
public final void removeRows(int firstIndex, int lastIndex) {
if (firstIndex < 0)
throw new IllegalArgumentException("Negative index " + firstIndex);
if (firstIndex == lastIndex)
return;
if (firstIndex > lastIndex)
throw new IllegalArgumentException("First index after last index " + firstIndex + " > " + lastIndex);
// remove row elements, minding merged cells
final int columnCount = getColumnCount();
final Set<Point> coverOrigins = new HashSet<Point>();
for (int y = firstIndex; y < lastIndex; y++) {
for (int x = 0; x < columnCount; x++) {
final Cell<D> cell = this.getImmutableCellAt(x, y);
if (cell.isCovered()) {
coverOrigins.add(this.getCoverOrigin(x, y));
// cells spanning inside the removed rows don't need to be unmerged
} else if (cell.coversOtherCells() && y + cell.getRowsSpanned() > lastIndex) {
this.getCellAt(x, y).unmerge();
}
}
}
// don't update this.rows as we need them below and indexes would be wrong anyway
this.remove(Axis.ROW, firstIndex, lastIndex - 1, false);
// adjust rows spanned
// we haven't yet reset this.rows, so we can use getImmutableCellAt()
for (final Point coverOrigin : coverOrigins) {
// if the origin is inside the removed rows, then the whole merged cell was removed
// (if the merged cell extended past lastIndex, it got unmerged above and wasn't added
// to the set)
if (coverOrigin.y >= firstIndex) {
assert this.getImmutableCellAt(coverOrigin.x, coverOrigin.y).getElement().getDocument() == null;
continue;
}
final Cell<D> coverOriginCell = this.getImmutableCellAt(coverOrigin.x, coverOrigin.y);
final int rowsSpanned = coverOriginCell.getRowsSpanned();
// otherwise the cover origin is between firstIndex and lastIndex and should have been
// unmerged
assert rowsSpanned > 1;
final int stopY = coverOrigin.y + rowsSpanned;
final int toRemove = Math.min(lastIndex, stopY) - firstIndex;
assert toRemove > 0 && toRemove < rowsSpanned;
coverOriginCell.getElement().setAttribute("number-rows-spanned", (rowsSpanned - toRemove) + "", getTABLE());
}
// remove empty table-row-group
// since indexes are stored in TableGroup, we can do it after having removed rows
final Set<TableGroup> groups = new HashSet<TableGroup>();
for (int y = firstIndex; y < lastIndex; y++) {
groups.add(this.getRowGroupAt(y));
}
for (final TableGroup group : groups) {
TableGroup g = group;
Element elem = g.getElement();
while (g != null && elem.getParent() != null && elem.getChildren().size() == 0) {
elem.detach();
g = g.getParent();
elem = g == null ? null : g.getElement();
}
}
// recreate list from XML
this.readRows();
}
// *** table models
public final SheetTableModel<D> getTableModel(final int column, final int row) {
return new SheetTableModel<D>(this, row, column);
}
public final SheetTableModel<D> getTableModel(final int column, final int row, final int lastCol, final int lastRow) {
return new SheetTableModel<D>(this, row, column, lastRow, lastCol);
}
public final MutableTableModel<D> getMutableTableModel(final int column, final int row) {
return new MutableTableModel<D>(this, row, column);
}
/**
* Return the table from <code>start</code> to <code>end</code> inclusive.
*
* @param start the first cell of the result.
* @param end the last cell of the result.
* @return the table.
*/
public final MutableTableModel<D> getMutableTableModel(final Point start, final Point end) {
// +1 since exclusive
return new MutableTableModel<D>(this, start.y, start.x, end.y + 1, end.x + 1);
}
public final void merge(TableModel t, final int column, final int row) {
this.merge(t, column, row, false);
}
/**
* Merges t into this sheet at the specified point.
*
* @param t the data to be merged.
* @param column the column t will be merged at.
* @param row the row t will be merged at.
* @param includeColNames if <code>true</code> the column names of t will also be merged.
*/
public final void merge(TableModel t, final int column, final int row, final boolean includeColNames) {
final int offset = (includeColNames ? 1 : 0);
// the columns must be first, see section 8.1.1 of v1.1
this.ensureColumnCount(column + t.getColumnCount());
this.ensureRowCount(row + t.getRowCount() + offset);
final TableModel thisModel = this.getMutableTableModel(column, row);
if (includeColNames) {
for (int x = 0; x < t.getColumnCount(); x++) {
thisModel.setValueAt(t.getColumnName(x), 0, x);
}
}
for (int y = 0; y < t.getRowCount(); y++) {
for (int x = 0; x < t.getColumnCount(); x++) {
final Object value = t.getValueAt(y, x);
thisModel.setValueAt(value, y + offset, x);
}
}
}
/**
* All ranges defined in this table.
*
* @return the names.
* @see SpreadSheet#getRangesNames()
*/
public final Set<String> getRangesNames() {
return SpreadSheet.getRangesNames(getElement(), getTABLE());
}
/**
* Get a named range in this table.
*
* @param name the name of the range.
* @return a named range, or <code>null</code> if the passed name doesn't exist.
* @see #getRangesNames()
* @see SpreadSheet#getRange(String)
*/
public final Range getRange(String name) {
return SpreadSheet.getRange(getElement(), getTABLE(), name);
}
public final MutableTableModel<D> getTableModel(String name) {
final Range points = getRange(name);
if (points == null)
return null;
if (points.spanSheets())
throw new IllegalStateException("different sheet names: " + points.getStartSheet() + " != " + points.getEndSheet());
return this.getMutableTableModel(points.getStartPoint(), points.getEndPoint());
}
// * UsedRange & CurrentRegion
/**
* The range that covers all used cells.
*
* @return the range that covers all used cells, <code>null</code> if the table is completely
* empty.
*/
public final Range getUsedRange() {
return this.getUsedRange(false);
}
/**
* The range that covers all used cells.
*
* @param checkStyle <code>true</code> to check the background and borders in addition to the
* content.
* @return the range that covers all used cells, <code>null</code> if the table is completely
* blank.
*/
public final Range getUsedRange(boolean checkStyle) {
int minX = -1, minY = -1, maxX = -1, maxY = -1;
final int colCount = this.getColumnCount();
final int rowCount = this.getRowCount();
for (int x = 0; x < colCount; x++) {
for (int y = 0; y < rowCount; y++) {
if (!this.isCellBlank(x, y, checkStyle)) {
if (minX < 0 || x < minX)
minX = x;
if (minY < 0 || y < minY)
minY = y;
if (maxX < 0 || x > maxX)
maxX = x;
if (maxY < 0 || y > maxY)
maxY = y;
}
}
}
return minX < 0 ? null : new Range(getName(), new Point(minX, minY), new Point(maxX, maxY));
}
protected final boolean isCellBlank(final int x, int y, boolean checkStyle) {
if (!getImmutableCellAt(x, y).isEmpty())
return false;
if (checkStyle) {
final StyleTableCellProperties tableCellProperties = this.getTableCellPropertiesAt(x, y);
return tableCellProperties == null || (tableCellProperties.getBackgroundColor() == null && tableCellProperties.getBorders().isEmpty());
} else {
return true;
}
}
private class RegionExplorer {
private final boolean checkStyle;
private final int rowCount, colCount;
protected int minX, minY, maxX, maxY;
public RegionExplorer(final int startX, final int startY, final boolean checkStyle) {
this.rowCount = getRowCount();
this.colCount = getColumnCount();
this.minX = this.maxX = startX;
this.minY = this.maxY = startY;
this.checkStyle = checkStyle;
}
public boolean canXDecrement() {
return this.minX > 0;
}
public boolean canYDecrement() {
return this.minY > 0;
}
public boolean canXIncrement() {
return this.maxX < this.colCount - 1;
}
public boolean canYIncrement() {
return this.maxY < this.rowCount - 1;
}
private boolean checkRow(final boolean upper) {
if (upper && this.canYDecrement() || !upper && this.canYIncrement()) {
final int y = upper ? this.minY - 1 : this.maxY + 1;
final int start = this.canXDecrement() ? this.minX - 1 : this.minX;
final int stop = this.canXIncrement() ? this.maxX + 1 : this.maxX;
for (int x = start; x <= stop; x++) {
if (!isCellBlank(x, y, this.checkStyle)) {
if (upper)
this.minY = y;
else
this.maxY = y;
if (x < this.minX)
this.minX = x;
if (x > this.maxX)
this.maxX = x;
return true;
}
}
}
return false;
}
// sans corners (checked by checkRow())
private boolean checkCol(final boolean left) {
if (left && this.canXDecrement() || !left && this.canXIncrement()) {
final int x = left ? this.minX - 1 : this.maxX + 1;
for (int y = this.minY; y <= this.maxY; y++) {
if (!isCellBlank(x, y, this.checkStyle)) {
if (left)
this.minX = x;
else
this.maxX = x;
return true;
}
}
}
return false;
}
private final boolean checkFrame() {
return this.checkRow(true) || this.checkRow(false) || this.checkCol(true) || this.checkCol(false);
}
public final Range getCurrentRegion() {
while (this.checkFrame())
;// bounded by table size
return new Range(getName(), new Point(this.minX, this.minY), new Point(this.maxX, this.maxY));
}
}
public final Range getCurrentRegion(String ref) {
return this.getCurrentRegion(ref, false);
}
public final Range getCurrentRegion(String ref, boolean checkStyle) {
final Point p = resolveHint(ref);
return this.getCurrentRegion(p.x, p.y, checkStyle);
}
/**
* The smallest range containing the passed cell completely surrounded by empty rows and
* columns.
*
* @param startX x coordinate.
* @param startY y coordinate.
* @return the smallest range containing the passed cell.
* @see <a href="http://msdn.microsoft.com/library/aa214248(v=office.11).aspx">CurrentRegion
* Property</a>
*/
public final Range getCurrentRegion(final int startX, final int startY) {
return this.getCurrentRegion(startX, startY, false);
}
public final Range getCurrentRegion(final int startX, final int startY, final boolean checkStyle) {
return new RegionExplorer(startX, startY, checkStyle).getCurrentRegion();
}
// *** static
/**
* Convert string coordinates into numeric ones.
*
* @param ref the string address, eg "$AA$34" or "AA34".
* @return the numeric coordinates or <code>null</code> if <code>ref</code> is not valid, eg
* {26, 33}.
*/
static final Point resolve(String ref) {
final Matcher matcher = SpreadSheet.minCellPattern.matcher(ref);
if (!matcher.matches())
return null;
return resolve(matcher.group(1), matcher.group(2));
}
/**
* Convert string coordinates into numeric ones. ATTN this method does no checks.
*
* @param letters the column, eg "AA".
* @param digits the row, eg "34".
* @return the numeric coordinates, eg {26, 33}.
*/
static final Point resolve(final String letters, final String digits) {
return new Point(toInt(letters), Integer.parseInt(digits) - 1);
}
// "AA" => 26
static final int toInt(String col) {
if (col.length() < 1)
throw new IllegalArgumentException("x cannot be empty");
col = col.toUpperCase();
int x = 0;
for (int i = 0; i < col.length(); i++) {
x = x * 26 + (col.charAt(i) - 'A' + 1);
}
// zero based
return x - 1;
}
public static final String toStr(int col) {
if (col < 0)
throw new IllegalArgumentException("negative column : " + col);
// one based (i.e. 0 is A)
col++;
final int radix = 26;
final StringBuilder chars = new StringBuilder(4);
while (col > 0) {
chars.append((char) ('A' + ((col - 1) % radix)));
col = (col - 1) / radix;
}
return chars.reverse().toString();
}
/**
* Convert numeric coordinates into string ones.
*
* @param p the numeric coordinates, e.g. {26, 33}.
* @return the string address, e.g. "AA34".
*/
static final String getAddress(Point p) {
return getAddress(p.x, p.y);
}
static final String getAddress(final int x, final int y) {
if (x < 0 || y < 0)
throw new IllegalArgumentException("negative coordinates : " + x + ":" + y);
return toStr(x) + (y + 1);
}
}