/*!
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.reporting.libraries.base.util;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;
/**
* A lookup table for objects. This implementation is not synchronized, it is up to the caller to synchronize it
* properly.
*
* @author Thomas Morgner
*/
public class ObjectTable<T> implements Serializable {
/**
* For serialization.
*/
private static final long serialVersionUID = -3968322452944912066L;
/**
* The number of rows.
*/
private int rows;
/**
* The number of columns.
*/
private int columns;
/**
* An array of objects. The array may contain <code>null</code> values.
*/
private transient Object[][] data;
/**
* Defines how many object-slots get reserved each time we run out of space.
*/
private int rowIncrement;
/**
* Defines how many object-slots get reserved each time we run out of space.
*/
private int columnIncrement;
/**
* Creates a new table.
*/
public ObjectTable() {
this( 5, 5 );
}
/**
* Creates a new table.
*
* @param increment the row and column size increment.
*/
public ObjectTable( final int increment ) {
this( increment, increment );
}
/**
* Creates a new table.
*
* @param rowIncrement the row size increment.
* @param colIncrement the column size increment.
*/
public ObjectTable( final int rowIncrement, final int colIncrement ) {
if ( rowIncrement < 1 ) {
throw new IllegalArgumentException( "Increment must be positive." );
}
if ( colIncrement < 1 ) {
throw new IllegalArgumentException( "Increment must be positive." );
}
this.rows = 0;
this.columns = 0;
this.rowIncrement = rowIncrement;
this.columnIncrement = colIncrement;
this.data = new Object[ rowIncrement ][];
}
/**
* Returns the column size increment.
*
* @return the increment.
*/
public int getColumnIncrement() {
return this.columnIncrement;
}
/**
* Returns the row size increment.
*
* @return the increment.
*/
public int getRowIncrement() {
return this.rowIncrement;
}
/**
* Checks that there is storage capacity for the specified row and resizes if necessary.
*
* @param row the row index.
*/
protected void ensureRowCapacity( final int row ) {
// does this increase the number of rows? if yes, create new storage
if ( row >= this.data.length ) {
final Object[][] enlarged = new Object[ row + this.rowIncrement ][];
System.arraycopy( this.data, 0, enlarged, 0, this.data.length );
// do not create empty arrays - this is more expensive than checking
// for null-values.
this.data = enlarged;
}
}
/**
* Ensures that there is storage capacity for the specified item.
*
* @param row the row index.
* @param column the column index.
*/
public void ensureCapacity( final int row, final int column ) {
if ( row < 0 ) {
throw new IndexOutOfBoundsException( "Row is invalid. " + row );
}
if ( column < 0 ) {
throw new IndexOutOfBoundsException( "Column is invalid. " + column );
}
ensureRowCapacity( row );
final Object[] current = this.data[ row ];
if ( current == null ) {
final Object[] enlarged
= new Object[ Math.max( column + 1, this.columnIncrement ) ];
this.data[ row ] = enlarged;
} else if ( column >= current.length ) {
final Object[] enlarged = new Object[ column + this.columnIncrement ];
System.arraycopy( current, 0, enlarged, 0, current.length );
this.data[ row ] = enlarged;
}
}
/**
* Returns the number of rows in the table.
*
* @return The row count.
*/
public int getRowCount() {
return this.rows;
}
/**
* Returns the number of columns in the table.
*
* @return The column count.
*/
public int getColumnCount() {
return this.columns;
}
/**
* Returns the object from a particular cell in the table. Returns null, if there is no object at the given position.
* <p/>
* Note: throws IndexOutOfBoundsException if row or column is negative.
*
* @param row the row index (zero-based).
* @param column the column index (zero-based).
* @return The object.
*/
protected T getObject( final int row, final int column ) {
if ( row < this.data.length ) {
final Object[] current = this.data[ row ];
if ( current == null ) {
return null;
}
if ( column < current.length ) {
return (T) current[ column ];
}
}
return null;
}
/**
* Sets the object for a cell in the table. The table is expanded if necessary.
*
* @param row the row index (zero-based).
* @param column the column index (zero-based).
* @param object the object.
*/
protected void setObject( final int row, final int column,
final T object ) {
ensureCapacity( row, column );
this.data[ row ][ column ] = object;
this.rows = Math.max( this.rows, row + 1 );
this.columns = Math.max( this.columns, column + 1 );
}
/**
* Tests this paint table for equality with another object (typically also an <code>ObjectTable</code>).
*
* @param o the other object.
* @return A boolean.
*/
public boolean equals( final Object o ) {
if ( o == null ) {
return false;
}
if ( this == o ) {
return true;
}
if ( ( o instanceof ObjectTable ) == false ) {
return false;
}
final ObjectTable ot = (ObjectTable) o;
if ( getRowCount() != ot.getRowCount() ) {
return false;
}
if ( getColumnCount() != ot.getColumnCount() ) {
return false;
}
for ( int r = 0; r < getRowCount(); r++ ) {
for ( int c = 0; c < getColumnCount(); c++ ) {
if ( ObjectUtilities.equal( getObject( r, c ),
ot.getObject( r, c ) ) == false ) {
return false;
}
}
}
return true;
}
/**
* Returns a hash code value for the object.
*
* @return the hashcode
*/
public int hashCode() {
int result;
result = this.rows;
result = 29 * result + this.columns;
return result;
}
/**
* Handles serialization.
*
* @param stream the output stream.
* @throws java.io.IOException if there is an I/O problem.
*/
private void writeObject( final ObjectOutputStream stream )
throws IOException {
stream.defaultWriteObject();
final int rowCount = this.data.length;
stream.writeInt( rowCount );
for ( int r = 0; r < rowCount; r++ ) {
final Object[] column = this.data[ r ];
stream.writeBoolean( column != null );
if ( column != null ) {
final int columnCount = column.length;
stream.writeInt( columnCount );
for ( int c = 0; c < columnCount; c++ ) {
writeSerializedData( stream, column[ c ] );
}
}
}
}
/**
* Handles the serialization of an single element of this table.
*
* @param stream the stream which should write the object
* @param o the object that should be serialized
* @throws java.io.IOException if an IO error occured
*/
protected void writeSerializedData( final ObjectOutputStream stream,
final Object o )
throws IOException {
stream.writeObject( o );
}
/**
* Restores a serialized object.
*
* @param stream the input stream.
* @throws java.io.IOException if there is an I/O problem.
* @throws ClassNotFoundException if a class cannot be found.
*/
private void readObject( final ObjectInputStream stream )
throws IOException, ClassNotFoundException {
stream.defaultReadObject();
final int rowCount = stream.readInt();
this.data = new Object[ rowCount ][];
for ( int r = 0; r < rowCount; r++ ) {
final boolean isNotNull = stream.readBoolean();
if ( isNotNull ) {
final int columnCount = stream.readInt();
final Object[] column = new Object[ columnCount ];
this.data[ r ] = column;
for ( int c = 0; c < columnCount; c++ ) {
column[ c ] = readSerializedData( stream );
}
}
}
}
/**
* Handles the deserialization of a single element of the table.
*
* @param stream the object input stream from which to read the object.
* @return the deserialized object
* @throws ClassNotFoundException if a class cannot be found.
* @throws java.io.IOException Any of the usual Input/Output related exceptions.
*/
protected Object readSerializedData( final ObjectInputStream stream )
throws ClassNotFoundException, IOException {
return stream.readObject();
}
/**
* Clears the table.
*/
public void clear() {
this.rows = 0;
this.columns = 0;
for ( int i = 0; i < this.data.length; i++ ) {
if ( this.data[ i ] != null ) {
Arrays.fill( this.data[ i ], null );
}
}
}
/**
* Copys the contents of the old column to the new column.
*
* @param oldColumn the index of the old (source) column
* @param newColumn the index of the new column
*/
protected void copyColumn( final int oldColumn, final int newColumn ) {
for ( int i = 0; i < getRowCount(); i++ ) {
setObject( i, newColumn, getObject( i, oldColumn ) );
}
}
/**
* Copys the contents of the old row to the new row. This uses raw access to the data and is remarkably faster than
* manual copying.
*
* @param oldRow the index of the old row
* @param newRow the index of the new row
*/
protected void copyRow( final int oldRow, final int newRow ) {
this.ensureCapacity( newRow, getColumnCount() );
final Object[] oldRowStorage = this.data[ oldRow ];
if ( oldRowStorage == null ) {
final Object[] newRowStorage = this.data[ newRow ];
if ( newRowStorage != null ) {
Arrays.fill( newRowStorage, null );
}
} else {
this.data[ newRow ] = oldRowStorage.clone();
}
}
/**
* Replaces the data in the table with the given two-dimensional array. For performance reasons, the array is added as
* is without cloning it, so make sure that you either clone it up-front or risk instable objects.
*
* @param data the array to be used as new data array
* @param colCount the column count in the array.
* @noinspection AssignmentToCollectionOrArrayFieldFromParameter for performance reasons as this is an internal
* method
*/
protected void setData( final Object[][] data, final int colCount ) {
if ( data == null ) {
throw new NullPointerException();
}
if ( colCount < 0 ) {
throw new IndexOutOfBoundsException();
}
this.data = data;
this.rows = data.length;
this.columns = colCount;
}
/**
* Clears the row by removing the array that stores the row-data. This reduces the in-memory size of the table at the
* cost of possibly having to recreate the row-data-array later.
*
* @param row the row to be deleted.
*/
public void clearRow( final int row ) {
if ( data.length <= row ) {
return;
}
this.data[ row ] = null;
}
/**
* Returns the data-storage as raw-object. You better do not modify the data-storage unless you are absolutely sure
* about what you are doing.
*
* @return the data as raw-object.
*/
protected Object[][] getData() {
return data;
}
}