package prefuse.data;
import java.util.HashMap;
import prefuse.util.PrefuseLib;
/**
* <p>The Schema class represents a description of a Table's columns, including
* column names, data types, and default values. New Table
* instances can be created directly from Schema objects through the use of
* the {@link #instantiate()} method. If a schema is subsequently changed,
* instantiated table instances are not affected, keeping their original
* schema.</p>
*
* <p>Schema instances can be locked to prevent further changes. Any attempt
* to alter a locked schema will result in a runtime exception being thrown.
* If a schema is not locked, clients are free to add new columns and
* edit default values.</p>
*
* @author <a href="http://jheer.org">jeffrey heer</a>
*/
public class Schema implements Cloneable {
private String[] m_names;
private Class[] m_types;
private Object[] m_dflts;
private HashMap m_lookup;
private int m_size;
private boolean m_locked;
// ------------------------------------------------------------------------
// Constructors
/**
* Creates a new empty schema.
*/
public Schema() {
this(10);
}
/**
* Creates a new empty schema with a starting capacity for a given number
* of columns.
* @param ncols the number of columns in this schema
*/
public Schema(int ncols) {
m_names = new String[ncols];
m_types = new Class[ncols];
m_dflts = new Object[ncols];
m_size = 0;
m_locked = false;
}
/**
* Create a new schema consisting of the given column names and types.
* @param names the column names
* @param types the column types (as Class instances)
*/
public Schema(String[] names, Class[] types) {
this(names.length);
// check the schema validity
if ( names.length != types.length ) {
throw new IllegalArgumentException(
"Input arrays should be the same length");
}
for ( int i=0; i<names.length; ++i ) {
addColumn(names[i], types[i], null);
}
}
/**
* Create a new schema consisting of the given column names, types, and
* default column values.
* @param names the column names
* @param types the column types (as Class instances)
* @param defaults the default values for each column
*/
public Schema(String[] names, Class[] types, Object[] defaults) {
this(names.length);
// check the schema validity
if ( names.length != types.length ||
types.length != defaults.length )
{
throw new IllegalArgumentException(
"Input arrays should be the same length");
}
for ( int i=0; i<names.length; ++i ) {
addColumn(names[i], types[i], defaults[i]);
}
}
/**
* Creates a copy of this Schema. This might be useful for creating
* extended schemas from a shared base schema. Cloned copies
* of a locked Schema will not inherit the locked status.
* @see java.lang.Object#clone()
*/
public Object clone() {
Schema s = new Schema(m_size);
for ( int i=0; i<m_size; ++i ) {
s.addColumn(m_names[i], m_types[i], m_dflts[i]);
}
return s;
}
/**
* Lazily construct the lookup table for this schema. Used to
* accelerate name-based lookups of schema information.
*/
protected void initLookup() {
m_lookup = new HashMap();
for ( int i=0; i<m_names.length; ++i ) {
m_lookup.put(m_names[i], new Integer(i));
}
}
// ------------------------------------------------------------------------
// Accessors / Mutators
/**
* Locks the schema, preventing any additional changes. Locked schemas
* can not be unlocked! Cloned copies of a locked schema will not inherit
* this locked status.
* @return a pointer to this schema
*/
public Schema lockSchema() {
m_locked = true;
return this;
}
/**
* Indicates if this schema is locked. Locked schemas can not be edited.
* @return true if this schema is locked, false otherwise
*/
public boolean isLocked() {
return m_locked;
}
/**
* Add a column to this schema.
* @param name the column name
* @param type the column type (as a Class instance)
* @throws IllegalArgumentException is either name or type are null or
* the name already exists in this schema.
*/
public void addColumn(String name, Class type) {
addColumn(name, type, null);
}
/**
* Add a column to this schema.
* @param name the column name
* @param type the column type (as a Class instance)
* @throws IllegalArgumentException is either name or type are null or
* the name already exists in this schema.
*/
public void addColumn(String name, Class type, Object defaultValue) {
// check lock status
if ( m_locked ) {
throw new IllegalStateException(
"Can not add column to a locked Schema.");
}
// check for validity
if ( name == null ) {
throw new IllegalArgumentException(
"Null column names are not allowed.");
}
if ( type == null ) {
throw new IllegalArgumentException(
"Null column types are not allowed.");
}
for ( int i=0; i<m_size; ++i ) {
if ( m_names[i].equals(name) ) {
throw new IllegalArgumentException(
"Duplicate column names are not allowed: "+m_names[i]);
}
}
// resize if necessary
// TODO put resizing functionality into library routines?
if ( m_names.length == m_size ) {
int capacity = (3*m_names.length)/2 + 1;
String[] names = new String[capacity];
Class[] types = new Class[capacity];
Object[] dflts = new Object[capacity];
System.arraycopy(m_names, 0, names, 0, m_size);
System.arraycopy(m_types, 0, types, 0, m_size);
System.arraycopy(m_dflts, 0, dflts, 0, m_size);
m_names = names;
m_types = types;
m_dflts = dflts;
}
m_names[m_size] = name;
m_types[m_size] = type;
m_dflts[m_size] = defaultValue;
if ( m_lookup != null )
m_lookup.put(name, new Integer(m_size));
++m_size;
}
/**
* <p>Add a new interpolated column to this data schema. This actually adds
* three columns to the schema: a column for the current value of the
* field, and columns for starting and ending values. During animation or
* operations spread over a time span, the current value can be
* interpolated between the start and end values.</p>
*
* <p>The name for the current value column is the name parameter provided
* to the method. The name for the start and end columns will be determined
* by the return value of {@link PrefuseLib#getStartField(String)} and
* {@link PrefuseLib#getEndField(String)}. The default behavior for these
* methods is to append ":start" to the name of a stating value column
* and append ":end" to the the name of an ending value column.</p>
*
* @param name the name of the interpolated column to add
* @param type the data type the columns will contain
* @param dflt the default value for each of the columns
*/
public void addInterpolatedColumn(String name, Class type, Object dflt) {
addColumn(name, type, dflt);
addColumn(PrefuseLib.getStartField(name), type, dflt);
addColumn(PrefuseLib.getEndField(name), type, dflt);
}
/**
* Add an interpolated column with a null default value.
* @see #addInterpolatedColumn(String, Class, Object)
* @param name the name of the interpolated column to add
* @param type the data type the columns will contain
*/
public void addInterpolatedColumn(String name, Class type) {
addInterpolatedColumn(name, type, null);
}
/**
* Get the number of columns in this schema.
* @return the number of columns
*/
public int getColumnCount() {
return m_size;
}
/**
* The name of the column at the given position.
* @param col the column index
* @return the column name
*/
public String getColumnName(int col) {
return m_names[col];
}
/**
* The column index for the column with the given name.
* @param field the column name
* @return the column index
*/
public int getColumnIndex(String field) {
if ( m_lookup == null )
initLookup();
Integer idx = (Integer)m_lookup.get(field);
return ( idx==null ? -1 : idx.intValue() );
}
/**
* The type of the column at the given position.
* @param col the column index
* @return the column type
*/
public Class getColumnType(int col) {
return m_types[col];
}
/**
* The type of the column with the given name.
* @param field the column name
* @return the column type
*/
public Class getColumnType(String field) {
int idx = getColumnIndex(field);
return ( idx<0 ? null : m_types[idx] );
}
/**
* The default value of the column at the given position.
* @param col the column index
* @return the column's default value
*/
public Object getDefault(int col) {
return m_dflts[col];
}
/**
* The default value of the column with the given name.
* @param field the column name
* @return the column's default value
*/
public Object getDefault(String field) {
int idx = getColumnIndex(field);
return ( idx<0 ? null : m_dflts[idx] );
}
/**
* Set the default value for the given field.
* @param col the column index of the field to set the default for
* @param val the new default value
*/
public void setDefault(int col, Object val) {
// check lock status
if ( m_locked ) {
throw new IllegalStateException(
"Can not update default values of a locked Schema.");
}
m_dflts[col] = val;
}
/**
* Set the default value for the given field.
* @param field the name of column to set the default for
* @param val the new default value
*/
public void setDefault(String field, Object val) {
// check lock status
if ( m_locked ) {
throw new IllegalStateException(
"Can not update default values of a locked Schema.");
}
int idx = getColumnIndex(field);
m_dflts[idx] = val;
}
/**
* Set the default value for the given field as an int.
* @param field the name of column to set the default for
* @param val the new default value
*/
public void setDefault(String field, int val) {
setDefault(field, new Integer(val));
}
/**
* Set the default value for the given field as a long.
* @param field the name of column to set the default for
* @param val the new default value
*/
public void setDefault(String field, long val) {
setDefault(field, new Long(val));
}
/**
* Set the default value for the given field as a float.
* @param field the name of column to set the default for
* @param val the new default value
*/
public void setDefault(String field, float val) {
setDefault(field, new Float(val));
}
/**
* Set the default value for the given field as a double.
* @param field the name of column to set the default for
* @param val the new default value
*/
public void setDefault(String field, double val) {
setDefault(field, new Double(val));
}
/**
* Set the default value for the given field as a boolean.
* @param field the name of column to set the default for
* @param val the new default value
*/
public void setDefault(String field, boolean val) {
setDefault(field, val ? Boolean.TRUE : Boolean.FALSE);
}
/**
* Set default values for the current, start, and end columns of an
* interpolated column.
* @param field the field name of the interpolated column
* @param val the new default value for all three implicated columns
*/
public void setInterpolatedDefault(String field, Object val) {
setDefault(field, val);
setDefault(PrefuseLib.getStartField(field), val);
setDefault(PrefuseLib.getEndField(field), val);
}
/**
* Set default values for the current, start, and end columns of an
* interpolated column as an int.
* @param field the field name of the interpolated column
* @param val the new default value for all three implicated columns
*/
public void setInterpolatedDefault(String field, int val) {
setInterpolatedDefault(field, new Integer(val));
}
/**
* Set default values for the current, start, and end columns of an
* interpolated column as a long.
* @param field the field name of the interpolated column
* @param val the new default value for all three implicated columns
*/
public void setInterpolatedDefault(String field, long val) {
setInterpolatedDefault(field, new Long(val));
}
/**
* Set default values for the current, start, and end columns of an
* interpolated column as a float.
* @param field the field name of the interpolated column
* @param val the new default value for all three implicated columns
*/
public void setInterpolatedDefault(String field, float val) {
setInterpolatedDefault(field, new Float(val));
}
/**
* Set default values for the current, start, and end columns of an
* interpolated column as a double.
* @param field the field name of the interpolated column
* @param val the new default value for all three implicated columns
*/
public void setInterpolatedDefault(String field, double val) {
setInterpolatedDefault(field, new Double(val));
}
/**
* Set default values for the current, start, and end columns of an
* interpolated column as a boolean.
* @param field the field name of the interpolated column
* @param val the new default value for all three implicated columns
*/
public void setInterpolatedDefault(String field, boolean val) {
setInterpolatedDefault(field, val ? Boolean.TRUE : Boolean.FALSE);
}
// ------------------------------------------------------------------------
// Comparison and Hashing
/**
* Compares this schema with another one for equality.
*/
public boolean equals(Object o) {
if ( !(o instanceof Schema) )
return false;
Schema s = (Schema)o;
if ( m_size != s.getColumnCount() )
return false;
for ( int i=0; i<m_size; ++i ) {
if ( !(m_names[i].equals(s.getColumnName(i)) &&
m_types[i].equals(s.getColumnType(i)) &&
m_dflts[i].equals(s.getDefault(i))) )
{
return false;
}
}
return true;
}
/**
* Indicates if values from a given Schema can be safely assigned to
* data using this Schema. The input Schema must be less than or
* equal in length to this Schema, and all contained columns in the
* given Schema must have matching names and types to this Schema.
* This method does not consider default values settings or the
* locked status of the Schemas. For example, if the given Schema
* has different default values than this one, this will have no
* impact on the assignability of the two.
* @param s the input Schema
* @return true if data models using this Schema could be assigned values
* directly from data using the input Schema, false otherwise.
*/
public boolean isAssignableFrom(Schema s) {
int ssize = s.getColumnCount();
if ( ssize > m_size )
return false;
for ( int i=0; i<ssize; ++i ) {
int idx = getColumnIndex(s.getColumnName(i));
if ( idx < 0 )
return false;
if ( !m_types[idx].equals(s.getColumnType(i)) )
return false;
}
return true;
}
/**
* Computes a hashcode for this schema.
*/
public int hashCode() {
int hashcode = 0;
for ( int i=0; i<m_size; ++i ) {
int idx = i+1;
int code = idx*m_names[i].hashCode();
code ^= idx*m_types[i].hashCode();
if ( m_dflts[i] != null )
code ^= m_dflts[i].hashCode();
hashcode ^= code;
}
return hashcode;
}
/**
* Returns a descriptive String for this schema.
*/
public String toString() {
StringBuffer sbuf = new StringBuffer();
sbuf.append("Schema[");
for ( int i=0; i<m_size; ++i ) {
if ( i > 0 ) sbuf.append(' ');
sbuf.append('(').append(m_names[i]).append(", ");
sbuf.append(m_types[i].getName()).append(", ");
sbuf.append(m_dflts[i]).append(')');
}
sbuf.append(']');
return sbuf.toString();
}
// ------------------------------------------------------------------------
// Table Operations
/**
* Instantiate this schema as a new Table instance.
* @return a new Table with this schema
*/
public Table instantiate() {
return instantiate(0);
}
/**
* Instantiate this schema as a new Table instance.
* @param nrows the number of starting rows in the table
* @return a new Table with this schema
*/
public Table instantiate(int nrows) {
Table t = new Table(nrows, m_size);
for ( int i=0; i<m_size; ++i ) {
t.addColumn(m_names[i], m_types[i], m_dflts[i]);
}
return t;
}
} // end of class Schema