at.spardat.xma.mdl.table.TableWM Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2003, 2007 s IT Solutions AT Spardat GmbH .
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* s IT Solutions AT Spardat GmbH - initial API and implementation
*******************************************************************************/
// @(#) $Id: TableWM.java 7141 2011-01-27 14:25:43Z gub $
package at.spardat.xma.mdl.table;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import at.spardat.enterprise.util.Types;
import at.spardat.xma.mdl.Atom;
import at.spardat.xma.mdl.ISelectable;
import at.spardat.xma.mdl.ModelChangeEvent;
import at.spardat.xma.mdl.NewModelEvent;
import at.spardat.xma.mdl.NewModelEventFactory;
import at.spardat.xma.mdl.Notification;
import at.spardat.xma.mdl.WModel;
import at.spardat.xma.mdl.util.*;
import at.spardat.xma.page.Page;
import at.spardat.xma.serializer.XmaInput;
import at.spardat.xma.serializer.XmaOutput;
import at.spardat.xma.test.TestUtil;
import at.spardat.xma.util.Assert;
/**
* A table widget model where the programmer is in full control on what rows the
* table has. Rows may be added, replaced or removed. Every row may have
* an additional image displayed with the row. Rows are stored in an ordered
* collection and may be accessed either by a unique String key or by
* a zero based row index.
*
* Besides managing the rows, a single or multiple selection state is
* controlled (interface ISelectable). The selected rows are
* identified by their String keys.
*
* @author YSD, 26.04.2003 09:27:39
*/
public class TableWM extends TableBaseWM implements ISelectable, ITableWM {
/**
* Holds selection information.
*/
TransStringSet selection_;
/**
* Two dimensional array of Atom objects managing the data of the table. This table
* has one more columns that there are TableColumns. The last columns holds
* a T_BCD atom which specifies the image id of a row icon.
*/
TransAtomTable table_;
/**
* Specifies if this table model is one way.
* @see S_ONE_WAY
*/
private boolean isOneWay_;
/**
* A counter which is incremented after every update. The purpose is to detect
* illegal deferred usage of TableRow objects (which cannot be updated anymore).
*/
protected int updateCount_;
/**
* column types which will be set by generated code
*/
protected byte[] columnTypes;
/**
* Constructor
*
* @param id uniquely identifies the model within its page
* @param pm the enclosing page model this widget model belongs to.
* @param numColumns number of columns this table has
* @param style bit or combination of the style constants S_*.
*/
public TableWM (short id, Page pm, int numColumns, int style) {
super (id, pm, numColumns, true);
if ((style & S_MULTI_SELECT) != 0) selection_ = new TransStringSetN();
else selection_ = new TransStringSet1();
if ((style & S_ONE_WAY) != 0) isOneWay_ = true;
// note that the TransAtomTable has one more column. The last
// (additional) column is used to store the image id
table_ = new TransAtomTable(numColumns+1);
columnTypes = new byte[numColumns];
}
/**
* @see at.spardat.xma.mdl.Transactional#changed()
*/
public boolean changed () {
return selection_.changed() || table_.changed();
}
/**
* @see at.spardat.xma.mdl.Transactional#rollback()
*/
public void rollback () {
selection_.rollback();
table_.rollback();
handle (new TableRowsChangedEvent());
handle (new SelectionChangedEvent(false));
}
/**
* @see at.spardat.xma.mdl.Transactional#commit()
*/
public void commit () {
selection_.commit();
table_.commit();
}
/**
* @see at.spardat.xma.mdl.WModel#handle(at.spardat.xma.mdl.ModelChangeEvent)
*/
public boolean handle (ModelChangeEvent event) {
boolean success = event.execute();
if (success) updateCount_++;
return success;
}
/**
* @see at.spardat.xma.mdl.ISelectable#select(java.lang.String)
*/
public void select (String key) {
if (isOneWay_ || table_.containsKey(key)) {
boolean success = selection_.add (key);
if (success)
handle (new SelectionChangedEvent (false));
}
}
/**
* @see at.spardat.xma.mdl.ISelectable#deselect(java.lang.String)
*/
public void deselect (String key) {
selection_.remove (key);
handle (new SelectionChangedEvent (false));
}
/**
* @see at.spardat.xma.mdl.ISelectable#deselectAll()
*/
public void deselectAll() {
selection_.clear();
handle (new SelectionChangedEvent (false));
}
/**
* @see at.spardat.xma.mdl.ISelectable#isMultiSelect()
*/
public boolean isMultiSelect() {
return selection_ instanceof TransStringSetN;
}
/**
* @see at.spardat.xma.mdl.ISelectable#getSelected()
*/
public String getSelected() {
return selection_.getSome();
}
/**
* @see at.spardat.xma.mdl.ISelectable#getSelection()
*/
public String[] getSelection() {
return selection_.getAll();
}
/**
* @see at.spardat.xma.mdl.ISelectable#getSelectionCount()
*/
public int getSelectionCount() {
return selection_.size();
}
/**
* @see at.spardat.xma.mdl.ISelectable#isSelected(java.lang.String)
*/
public boolean isSelected(String key) {
return selection_.contains(key);
}
/**
* @see at.spardat.xma.mdl.ISelectable#isStrict()
*/
public boolean isStrict() {
return !isOneWay_;
}
/**
* @see at.spardat.xma.mdl.table.ITableWM#selectByModelIndex(int)
*/
public void selectByModelIndex (int index) {
if (index < 0 || index >= size()) return;
select (getRow(index).getKey());
}
/**
* @see at.spardat.xma.mdl.Synchronization#externalize(at.spardat.xma.serializer.XmaOutput, boolean)
*/
public void externalize (XmaOutput xo, boolean forceFull) throws IOException {
// decide on what to write
byte what = 0;
boolean writeTable = forceFull || table_.changed();
boolean writeSel = forceFull || selection_.changed();
boolean forceFullTable = forceFull;
/**
* Special treatment of one way tables. A one way table is never synchronized
* from client to server. For the other direction applies: If it has been changed
* at the server, its row are fully transmitted (no deltas).
*/
if (isOneWay_) {
if (xo.isAtServer()) {
if (table_.changed()) forceFullTable = true;
} else {
// from client to server
writeTable = false;
}
}
// externalize
if (writeTable) what |= 1;
if (writeSel) what |= 2;
xo.writeByte("tableOrSel", what);
if (writeTable) {
// externalize table
table_.externalize (xo, forceFullTable);
}
if (writeSel) {
// externalize selection
selection_.externalize (xo, forceFull);
}
}
/**
* @see at.spardat.xma.mdl.Synchronization#internalize(at.spardat.xma.serializer.XmaInput)
*/
public void internalize (XmaInput in) throws IOException, ClassNotFoundException {
byte what = in.readByte();
boolean tableChanged = ((what & 1) != 0);
boolean selChanged = ((what & 2) != 0);
if (tableChanged) {
// table
table_.internalize(in);
}
if (selChanged) {
selection_.internalize(in);
}
if (tableChanged) handle (new TableRowsChangedEvent ());
if (selChanged || tableChanged) handle (new SelectionChangedEvent (false));
}
/**
* Returns if this table is one way.
* @see #S_ONE_WAY
*/
public boolean isOneWay() {
return isOneWay_;
}
/**
* Returns the number of rows in this table.
*/
public int size () {
return table_.size();
}
/**
* Adds a row to the table at a provided zero based row index.
*
* If the row is not added at the end of the table, runtime order is O(n)
* since an array is internally used to store the table.
*
* @param rowIndex the zero based index of the new row. Must be greater or equal
* to zero and not greater than size().
* @param key the key of the new row
* @param atoms Atom array
* @return true if row has been added or false if this table already contained
* a row with the specified key.
* @exception IllegalArgumentException if rowIndex is invalid.
*/
protected boolean add (int rowIndex, String key, Atom[] atoms) {
if (key == null) throw new IllegalArgumentException();
boolean success = table_.add(rowIndex, key, atoms);
if (success) handle (new RowAddedEvent(rowIndex));
return success;
}
/**
* Returns true if this table contains a row with the provided key.
*/
public boolean containsKey (String key) {
return table_.containsKey(key);
}
/**
* Returns the index at which the row with the provided key is located
* or -1 if no row with key is here. This is a time consuming operation
* of O(n).
*
* @param key the key of the row that is looked up.
* @return zero based index of the key or -1 if this table does not contain
* a row with the provided key.
*/
public int indexOf (String key) {
return table_.indexOf (key);
}
/**
* Returns the table row for a provided key.
*
* The returned TableRow may be used to query and modify the row. It
* must not be cached outside for later reuse because the returned row
* becomes invalid if the table is modified in other ways.
*
* @param key the key whose row is wanted
* @return the TableRow or null, if there is no row with the provided key.
*/
public TableRow getRow (String key) {
Atom [] atoms = table_.getAtoms(key);
if (atoms == null) return null;
TableRow tr = new TableRow();
tr.atoms_ = atoms;
tr.key_ = key;
tr.table_ = this;
tr.updateCount_ = updateCount_;
return tr;
}
/**
* Returns the table row at a provided zero based row index.
*
* The returned TableRow may be used to query and modify the row. It
* must not be cached outside for later reuse because the returned row
* becomes invalid if the table is modified in other ways.
*
* @param rowIndex the index of the row
* @return a TableRow, never null
* @exception ArrayIndexOutOfBoundsException if rowIndex invalid
*/
public TableRow getRow (int rowIndex) {
TableRow row = getRow (table_.getKey(rowIndex));
row.index_ = rowIndex;
return row;
}
/**
* Updates a row which has been previously read via getRow(). Internal
* method to be used from TableRow.
*
* @param newRow the row that has been read via getRow.
*/
void updateRow (TableRow newRow) {
Atom [] oldAtoms = table_.getAtoms (newRow.key_);
if (oldAtoms == null) throw new IllegalArgumentException();
Atom [] newAtoms = newRow.atoms_;
// determine how many atoms did change
int numChanged = 0;
for (int i=0; i<=columnCount_; i++) {
if (oldAtoms[i] != newAtoms[i]) numChanged++;
}
int rowIndex = newRow.index_;
if (rowIndex == -1) rowIndex = table_.indexOf(newRow.key_);
if (numChanged < (columnCount_+1)/2) {
// less than half the atoms changed; we therefore just update the Atoms
for (int i=0; i<=columnCount_; i++) {
if (oldAtoms[i] != newAtoms[i]) table_.replace(rowIndex, i, newAtoms[i]);
}
} else {
// change the complete row
table_.replace(rowIndex, newAtoms);
}
// fire changed event
handle (new RowChangedEvent (rowIndex));
}
/**
* Removes a row from this table at a provided index.
*
* @param rowIndex the zero based row index.
* @exception IndexOutOfBoundsException if rowIndex invalid.
*/
public void removeRow (int rowIndex) {
String key = table_.getKey(rowIndex);
table_.remove(rowIndex);
handle (new RowRemovedEvent(rowIndex));
// update the selection state
if (selection_.contains(key)) {
deselect (key);
}
}
/**
* Removes a row with a particular key.
*
* Note that this method is of runtime order O(n). If performance matters,
* please use removeRow(int) instead.
*
* @param key the key whose row is to be removed.
* @return true if removed, false if this table does not contain a row for the
* provided key.
*/
public boolean removeRow (String key) {
int rowIndex = table_.indexOf(key);
if (rowIndex == -1) return false;
removeRow (rowIndex);
return true;
}
/**
* Removes all rows from the table and deselects all rows.
*/
public void clear () {
table_.clear();
selection_.clear();
handle (new TableRowsChangedEvent ());
handle (new SelectionChangedEvent (false));
}
/**
* Removes only the rows but leave selection unchanged. Internal method for
* test purpose. This method is not intended to be called from outside
* and result in undefined behaviour!!!
*/
public void internalRemoveRows () {
if (isAtServer_) {
table_.clear();
handle (new TableRowsChangedEvent ());
}
}
/**
* @see at.spardat.xma.mdl.util.Descriptive#describe(at.spardat.xma.mdl.util.DNode)
*/
public void describe (DNode n) {
super.describe(n);
n.app("oneWay", isOneWay_).comma();
n.app("isMultiSel", isMultiSelect()).comma();
DNode sel = new DNode(n, "selection: ");
sel.app(selection_);
DNode table = new DNode(n, "tableData: ");
table.app(table_);
}
/**
* Make random changes to this
*/
public void randomlyChange () {
if (Assert.ON) {
int change = TestUtil.randomInt(0, 7);
switch (change) {
case 0:
// select a random entry
if (size() > 0) select(randomKey());
break;
case 1:
// deselect a random entry
if (size() > 0) deselect (randomKey());
break;
case 2:
// deselect all
deselectAll();
break;
case 3:
// select a key randomly choosen
select (TestUtil.randomString(TestUtil.randomInt(0, 3)));
break;
case 4:
// randomly add a row
int index = TestUtil.randomInt(0, size());
String key = TestUtil.randomString(3);
if (!containsKey(key)) {
new TableRow(this, index, key, newRandomObjectArray(), TestUtil.randomInt(0, 1));
}
break;
case 5:
// update a row
if (size() > 0) {
index = TestUtil.randomInt(0, size()-1);
TableRow tr = getRow(index);
tr.setCells(newRandomObjectArray());
}
break;
case 6:
// update a cell
if (size() > 0) {
index = TestUtil.randomInt(0, size()-1);
TableRow tr = getRow(index);
tr.setCell (TestUtil.randomInt(0, columnCount_-1), TestUtil.randomNullString(0, 5));
}
break;
case 7:
// delete a row
if (size() > 0) {
index = TestUtil.randomInt(0, size()-1);
removeRow(index);
}
break;
}
}
}
// returns a key randomly choosen from the table
private String randomKey () {
return getRow(TestUtil.randomInt(0, size()-1)).getKey();
}
// creates an object array of length columnCount_ and fills it with random data
private Object [] newRandomObjectArray () {
Object [] oa = new Object[columnCount_];
for (int i=0; iTypes.LAST) throw new IllegalArgumentException("unsupported type: "+type);
if(size()>0) throw new IllegalStateException("column types must not be changed after adding data to the table");
columnTypes[column]=type;
}
// see at.spardat.xma.mdl.table.ITableWM
public byte getColumnType(int column) {
return columnTypes[column];
}
/**
* Notification event that the selection state has changed.
*
* @author YSD, 26.04.2003 10:36:09
*/
class SelectionChangedEvent extends Notification {
/**
* Constructor
*/
public SelectionChangedEvent (boolean fromUI) {
super (TableWM.this, fromUI);
}
}
/**
* Notification event sent when a row has been added to the table.
*
* @author YSD, 27.04.2003 10:31:30
*/
class RowAddedEvent extends Notification {
/**
* Constructor
*
* @param index the row index where the row has been added
*/
public RowAddedEvent (int index) {
super (TableWM.this, false);
index_ = index;
}
/**
* Returns the index where the row has been added.
*/
public int getIndex () {
return index_;
}
private int index_;
}
/**
* Notification event sent when a row has been removed from the table.
*
* @author YSD, 27.04.2003 10:31:30
*/
class RowRemovedEvent extends Notification {
/**
* Constructor
*
* @param index the old row index of the removed row
*/
public RowRemovedEvent (int index) {
super (TableWM.this, false);
index_ = index;
}
/**
* Returns the index of the old (removed) row.
*/
public int getIndex () {
return index_;
}
private int index_;
}
/**
* Notification event sent when the data of a row has been changed.
*
* @author YSD, 27.04.2003 10:31:30
*/
class RowChangedEvent extends Notification {
/**
* Constructor
*
* @param index the row
*/
public RowChangedEvent (int index) {
super (TableWM.this, false);
index_ = index;
}
/**
* Returns the index of the changed row
*/
public int getIndex () {
return index_;
}
private int index_;
}
/**
* Notification event sent when one or more rows of the table might
* have changed, removed or added without knowning the precise
* reason of the change.
*
* @author YSD, 27.04.2003 15:53:54
*/
class TableRowsChangedEvent extends Notification {
/**
* Constructor
*
* @param wModel the widget model
*/
public TableRowsChangedEvent () {
super (TableWM.this, false);
}
}
/**
* Modificative event that sets an entirely new selection
*
* @author YSD, 26.04.2003 10:44:29
*/
class NewSelectionEvent extends ModelChangeEvent {
/**
* Constructor
*
* @param selectedKeys the set of keys that are to be selected
* @param fromUI indicates if the events origin is the UI or not
*/
public NewSelectionEvent (Collection selectedKeys, boolean fromUI) {
super (TableWM.this, fromUI);
selectedKeys_ = selectedKeys;
}
/**
* Selects the provided keys. Invalid keys (that are not in the table) are ignored.
*
* @see at.spardat.xma.mdl.ModelChangeEvent#execute()
*/
public boolean execute() {
TableWM tableDirect = (TableWM) wModel_;
TransStringSet sel = ((TableWM)wModel_).selection_;
TransAtomTable table = ((TableWM)wModel_).table_;
sel.clear();
Iterator iter = selectedKeys_.iterator();
while (iter.hasNext()) {
String key = (String) iter.next();
if (tableDirect.isOneWay() || table.containsKey(key)) {
sel.add(key);
}
}
return true;
}
private Collection selectedKeys_;
}
/**
* Event class used to notify the dynamic registration of a new TableWM.
* @author gub
* @since 2.1.0
* @see Page#addWModel(WModel)
*/
public static class NewTableWMEvent extends NewModelEvent {
int columns;
int style;
byte[] columnTypes;
/** empty constructor for deserialization */
public NewTableWMEvent() {}
/**
* constructor which initializes the modelType
* @param columns number of columns this table has
* @param style bit or combination of the style constants S_*.
*/
public NewTableWMEvent(int columns,int style,byte[] columnTypes) {
this.columns=columns;
this.style=style;
this.columnTypes=columnTypes;
}
// see at.spardat.xma.mdl.NewModelEvent.getType()
public byte getType() {
return NewModelEventFactory.TableWM;
}
// see at.spardat.xma.mdl.NewModelEvent.createModel()
public WModel createModel(short id,Page page) {
TableWM newModel = new TableWM(id,page,columns,style);
newModel.columnTypes=columnTypes;
return newModel;
}
// see at.spardat.xma.mdl.NewModelEvent.serialize()
public void serialize(XmaOutput out) throws IOException {
super.serialize(out);
out.writeInt("columns",columns);
out.writeInt("style",style);
for(int i=0;i