/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008 jOpenDocument, 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.jopendocument.dom.spreadsheet;
import org.jopendocument.dom.ODDocument;
import org.jopendocument.dom.XMLVersion;
import org.jopendocument.dom.spreadsheet.SheetTableModel.MutableTableModel;
import org.jopendocument.util.CollectionUtils;
import org.jopendocument.util.Tuple2;
import org.jopendocument.util.JDOMUtils;
import java.awt.Point;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
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) {
// from the relaxNG : a table must have at least one cell
final Element col = Column.createEmpty(ns, null);
final Element row = Row.createEmpty(ns).addContent(Cell.createEmpty(ns));
return new Element("table", ns.getTABLE()).addContent(col).addContent(row);
}
static final String getName(final Element elem) {
return elem.getAttributeValue("name", elem.getNamespace("table"));
}
// ATTN Row have their index as attribute
private final List<Row<D>> rows;
private int headerRowCount;
private final List<Column<D>> cols;
private int headerColumnCount;
public Table(D parent, Element local) {
super(parent, local, TableStyle.class);
this.rows = new ArrayList<Row<D>>();
this.cols = new ArrayList<Column<D>>();
this.readColumns();
this.readRows();
}
private void readColumns() {
this.read(true);
}
private final void readRows() {
this.read(false);
}
private final void read(final boolean col) {
final Tuple2<List<Element>, Integer> r = flatten(col);
(col ? this.cols : this.rows).clear();
for (final Element clone : r.get0()) {
if (col)
this.addCol(clone);
else
this.addRow(clone);
}
if (col)
this.headerColumnCount = r.get1();
else
this.headerRowCount = r.get1();
}
private final void addCol(Element clone) {
this.cols.add(new Column<D>(this, clone));
}
private Tuple2<List<Element>, Integer> flatten(boolean col) {
final List<Element> res = new ArrayList<Element>();
final Element header = this.getElement().getChild("table-header-" + getName(col) + "s", getTABLE());
if (header != null)
res.addAll(flatten(header, col));
final int headerCount = res.size();
res.addAll(flatten(getElement(), col));
return Tuple2.create(res, headerCount);
}
@SuppressWarnings("unchecked")
private List<Element> flatten(final Element elem, boolean col) {
final String childName = getName(col);
final List<Element> children = elem.getChildren("table-" + childName, getTABLE());
// not final, since iter.add() does not work consistently, and
// thus we must recreate an iterator each time
ListIterator<Element> iter = children.listIterator();
while (iter.hasNext()) {
final Element row = iter.next();
final Attribute repeatedAttr = row.getAttribute("number-" + childName + "s-repeated", getTABLE());
if (repeatedAttr != null) {
row.removeAttribute(repeatedAttr);
final int index = iter.previousIndex();
int repeated = Integer.parseInt(repeatedAttr.getValue());
if (repeated > 60000) {
repeated = 10;
}
// -1 : we keep the original row
for (int i = 0; i < repeated - 1; i++) {
final Element clone = (Element) row.clone();
// cannot use iter.add() since on JDOM 1.1 if row is the last table-column
// before table-row the clone is added at the very end
children.add(index, clone);
}
// restart after the added rows
iter = children.listIterator(index + repeated);
}
}
return children;
}
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();
}
private final String getName(boolean col) {
return col ? "column" : "row";
}
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.
* @param copies the number of copies of the range to make.
*/
public final synchronized void duplicateRows(int start, int count, int copies) {
final int stop = start + count;
// clone xml elements and add them to our tree
final List<Element> clones = new ArrayList<Element>(count * copies);
for (int i = 0; i < copies; i++) {
for (int l = start; l < stop; l++) {
final Element r = this.rows.get(l).getElement();
clones.add((Element) r.clone());
}
}
// works anywhere its XML element is
JDOMUtils.insertAfter(this.rows.get(stop - 1).getElement(), clones);
// synchronize our rows with our new tree
this.readRows();
}
private synchronized void addRow(Element child) {
this.rows.add(new Row<D>(this, child, this.rows.size()));
}
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();
}
public final MutableCell<D> getCellAt(int x, int y) {
return this.getRow(y).getMutableCellAt(x);
}
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
protected final Cell<D> getImmutableCellAt(int x, int y) {
return this.getRow(y).getCellAt(x);
}
protected final Cell<D> getImmutableCellAt(String ref) {
final Point p = resolveHint(ref);
return this.getImmutableCellAt(p.x, p.y);
}
/**
* @param row la ligne (0 a lineCount-1)
* @param column la colonnee (0 a colonneCount-1)
* @return la valeur de la cellule spécifiée.
*/
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)
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 CellStyle.DESC.findStyle(this.getODDocument().getPackage(), this.getElement().getDocument(), this.getStyleNameAt(column, row));
}
/**
* 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;
}
/**
* Retourne la valeur de la cellule spécifiée.
*
* @param ref une référence de la forme "A3".
* @return la valeur de la cellule spécifiée.
*/
public final Object getValueAt(String ref) {
return this.getImmutableCellAt(ref).getValue();
}
// *** get count
private Row<D> getRow(int index) {
return this.rows.get(index);
}
public final Column<D> getColumn(int i) {
return this.cols.get(i);
}
public final int getRowCount() {
return this.rows.size();
}
public final int getHeaderRowCount() {
return this.headerRowCount;
}
public final int getColumnCount() {
return this.cols.size();
}
public final int getHeaderColumnCount() {
return this.headerColumnCount;
}
// *** 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 (i.e. default
* 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) {
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(), this.createDefaultColStyle());
} else {
elemToClone = getColumn(colIndex).getElement();
}
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));
}
// now update widths
updateWidth(keepTableWidth);
// add needed cells
for (final Row r : this.rows) {
r.columnCountChanged();
}
}
}
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
for (final Row r : this.rows) {
r.checkRemove(firstIndex, lastIndex);
}
// rm column element
remove(true, firstIndex, lastIndex - 1);
// update widths
updateWidth(keepTableWidth);
// rm cells
for (final Row r : this.rows) {
r.removeCells(firstIndex, lastIndex);
}
}
private void updateWidth(final boolean keepTableWidth) {
final Float currentWidth = getWidth();
float newWidth = 0;
Column<?> nullWidthCol = null;
// columns are flattened in ctor: no repeated
for (final Column<?> col : this.cols) {
final Float colWidth = col.getWidth();
if (colWidth != null) {
assert colWidth >= 0;
newWidth += colWidth;
} else {
// we cannot compute the newWidth
newWidth = -1;
nullWidthCol = col;
break;
}
}
// remove all rel-column-width, simpler and Spreadsheet doesn't use them
// SpreadSheets have no table width
if (keepTableWidth && currentWidth != null) {
if (nullWidthCol != null)
throw new IllegalStateException("Cannot keep width since a column has no width : " + nullWidthCol);
// compute column-width from table width
final float ratio = currentWidth / 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() * ratio);
}
} else {
// compute table width from column-width
final TableStyle style = this.getStyle();
if (style != null) {
if (nullWidthCol != null)
throw new IllegalStateException("Cannot update table width since a column has no width : " + nullWidthCol);
style.setWidth(newWidth);
}
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 Float getWidth() {
final TableStyle style = this.getStyle();
return style == null ? null : style.getWidth();
}
private final ColumnStyle createDefaultColStyle() {
final ColumnStyle colStyle = ColumnStyle.DESC.createAutoStyle(this.getODDocument().getPackage(), "defaultCol");
colStyle.setWidth(20.0f);
return colStyle;
}
private final void setCount(final boolean col, final int newSize) {
this.remove(col, newSize, -1);
}
// both inclusive
private final void remove(final boolean col, final int fromIndex, final int toIndexIncl) {
// ok since rows and cols are flattened in ctor
final List<? extends TableCalcNode> l = col ? this.cols : this.rows;
final int toIndexValid = CollectionUtils.getValidIndex(l, toIndexIncl);
for (int i = toIndexValid; i >= fromIndex; i--) {
// works anywhere its XML element is
l.remove(i).getElement().detach();
}
}
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 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 = getRow(rowIndex).getElement();
final int toGrow = newSize - this.getRowCount();
if (toGrow < 0) {
setCount(false, newSize);
} else {
for (int i = 0; i < toGrow; i++) {
final Element newElem = (Element) elemToClone.clone();
// as per section 8.1.1 rows are the last elements inside a table
this.getElement().addContent(newElem);
addRow(newElem);
}
}
}
// *** 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 columnn 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);
}
}
}
// *** 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;
}
}