de.tsl2.nano.collection.TableList Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tsl2.nano.datastructure Show documentation
Show all versions of tsl2.nano.datastructure Show documentation
optimized implementations for trees, collections, arrays, historized input
/*
* File: $HeadURL$
* Id : $Id$
*
* created by: Thomas Schneider
* created on: Nov 19, 2012
*
* Copyright: (c) Thomas Schneider 2012, all rights reserved
*/
package de.tsl2.nano.collection;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import org.apache.commons.logging.Log;
import org.xml.sax.helpers.DefaultHandler;
import de.tsl2.nano.core.ManagedException;
import de.tsl2.nano.core.cls.BeanClass;
import de.tsl2.nano.core.log.LogFactory;
import de.tsl2.nano.core.util.CollectionUtil;
import de.tsl2.nano.core.util.FileUtil;
import de.tsl2.nano.core.util.StringUtil;
/**
* Defines a table of values. A header defines the columns. The header array shouldn't hold any null values! All rows
* hold an index, a row-identifier and fixed sized array of values. The values are not typed, as it is possible to have
* different types in one row. Overwrite this class, if you have a static value type per column.
*
* To work properly, your row-id type must implement the equals(Object) method correctly (see {@link #indexOf(Object)}.
*
* @param column-header type. to use the binarySearch of Arrays, H must implement Comparable
* @param row identifier type
* @author Thomas Schneider
* @version $Revision$
*/
@SuppressWarnings("unchecked")
public class TableList, ID> implements Serializable {
/** serialVersionUID */
private static final long serialVersionUID = -9192974285797951377L;
protected String name;
protected Class headerType;
protected H[] header;
protected List> rows;
private static final Log LOG = LogFactory.getLog(TableList.class);
/**
* used by dumps
*/
protected static final String DIV = "\t";
protected static final String LF = System.getProperty("line.separator");
public static final String FILE_EXT = ".csv";
public static final String CELL_ROOT = "--";
protected TableList() {}
public TableList(String name, int cols, int rows) {
this(name, cols);
fill((Class) DefaultHandler.class, rows);
}
/**
* column headers will be of type String. see {@link #TableList(Class, int)}
*/
public TableList(String name, int columnCount) {
//don't use the generic type H (Class) to be compilable on standard jdk javac.
this((Class) String.class, columnCount);
this.name = name;
}
/**
* constructor to create any columns
*
* @param headerType header type. if {@link String}, the column names will have their index as column-text. On
* default, the default constructor of the header type will be called to create an instance.
* @param columnCount count of columns to create
*/
public TableList(Class headerType, int columnCount) {
this((H[]) Array.newInstance(headerType, columnCount));
this.headerType = headerType;
if (headerType.equals(String.class)) {
for (int i = 0; i < header.length; i++) {
// using dynamic cast to be compilable on standard jdk16 compilers
header[i] = headerType.cast(String.valueOf(i));
}
} else {//--> default construction
BeanClass bc = BeanClass.getBeanClass(headerType);
for (int i = 0; i < header.length; i++) {
header[i] = bc.createInstance();
}
}
}
/**
* constructor
*
* @param header array columns. must not be null and must contain at least one element!
*/
public TableList(H... header) {
this((Class) (header.length > 0 && header[0] != null ? header[0].getClass() : Object.class), header);
}
/**
* base constructor
*
* @param header column definitions
*/
protected TableList(Class headerType, H... header) {
super();
assert header != null && header.length > 0;
this.header = header;
this.headerType = headerType;
rows = new ArrayList>();
}
public > T fill(int rowCount) {
return fill((Class) Row.class, rowCount);
}
/**
* pre fill (e.g. a fixed sized table) with empty values
*
* @param idType row identifier type
* @param rowCount rows to fill
*/
public > T fill(Class idType, int rowCount) {
return fill((ID[]) Array.newInstance(idType, rowCount));
}
/**
* pre fill the table - all values will be null
*
* @param ids row identifiers
*/
public > T fill(ID... ids) {
/*
* the row list is declarated to be a list - only at this point we try to optimize
* the creation of a min-fixed-sized array.
*/
if (rows instanceof ArrayList) {
((ArrayList>) rows).ensureCapacity(ids.length);
}
for (int i = 0; i < ids.length; i++) {
add(ids[i], null);
}
return (T) this;
}
/**
* @return Returns the header.
*/
public H[] getHeader() {
return header;
}
/**
* getColumns
*
* @return headers as string array
*/
public String[] getColumns() {
if (headerType.equals(String.class)) {
// using dynamic cast to be compilable on standard jdk16 compilers
return String[].class.cast(header);
} else {
String[] columns = new String[header.length];
for (int i = 0; i < header.length; i++) {
columns[i] = header[i].toString();
}
return columns;
}
}
public TableList addAll(boolean includedRowIds, List values) {
return addAll(includedRowIds, values.toArray());
}
/**
* addAll
* @param includedRowIds if true, row-ids are included in the values
* @param values to be splitted and packed into col/rows
* @return tablelist itself
*/
public TableList addAll(boolean includedRowIds, Object... values) {
int colSize = getColumnCount() + (includedRowIds ? 1 : 0);
Object[][] rows = CollectionUtil.split(values, colSize);
Object rowId;
for (int i = 0; i < rows.length; i++) {
if (includedRowIds && i % colSize == 0) {
rowId = rows[i][0];
} else {
rowId = createRowID(i);
}
add((ID) rowId, CollectionUtil.copyOfRange(rows[i], 1, rows[i].length));
}
return this;
}
protected ID createRowID(int i) {
return (ID)Integer.valueOf(i);
}
protected Row createRow(int index, Object...values) {
return new Row(index, createRowID(index), values);
}
/**
* adds a new row to the table. if no values are given, an empty row will be added. if some values were given, but
* not the size of the header, an exception will be thrown
*
* @param rowId (optional) row identifier
* @param values row values
* @return the table object itself
*/
public TableList add(ID rowId, Object... values) {
return add(rows.size(), rowId, values);
}
/**
* adds a new row to the table. if no values are given, an empty row will be added. if some values were given, but
* not the size of the header, an exception will be thrown
*
* @param index row index
* @param rowId (optional) row identifier
* @param values row values
* @return the table object itself
*/
public TableList add(int index, ID rowId, Object... values) {
if (values == null || values.length == 0) {
values = new Object[header.length];
}
rows.add(index, new Row(index, rowId, values));
return this;
}
/**
* resets an existing row of the table. if no values are given, an empty row will be added. if some values were
* given, but not the size of the header, an exception will be thrown
*
* @param index row index
* @param rowId (optional) new row identifier
* @param values row values
* @return the table object itself
*/
public TableList set(int index, ID rowId, Object... values) {
if (values == null || values.length == 0) {
values = new Object[header.length];
} else {
checkRowSize(values);
}
rows.set(index, new Row(index, rowId, values));
return this;
}
/**
* convenience to refresh the values of the given row (the table-value instances wont change!). the instances of
* values might be gotten through {@link #get(Object, Class)}.no check will be done for the compatibility of the
* given array type!
*
* @param rowId row identifier
* @param values new values to be copied to the table row
* @return table itself
*/
public TableList set(ID rowId, S... values) {
Object[] v = get(rowId);
int count = Math.min(values.length, v.length);
System.arraycopy(values, 0, v, 0, count);
return this;
}
/**
* resets an existing value of a given row and column of the table.
*
* @param row index index
* @param column column index
* @param values row values
* @return the table object itself
*/
public TableList set(int row, int column, Object value) {
checkSizes(row, column);
rows.get(row).values[column] = value;
return this;
}
/**
* resets an existing value of a given row and column of the table.
*
* @param row index index
* @param column column index
* @param values row values
* @return the table object itself
*/
public TableList set(int row, H column, Object value) {
int c = Arrays.binarySearch(header, column);
return set(row, c, value);
}
/**
* resets an existing value of a given row and column of the table.
*
* @param row index index
* @param column column index
* @param values row values
* @return the table object itself
*/
public TableList set(ID rowID, int column, Object value) {
int r = indexOf(rowID);
return set(r, column, value);
}
/**
* resets an existing value of a given row and column of the table.
*
* @param row index index
* @param column column index
* @param values row values
* @return the table object itself
*/
public TableList set(ID rowID, H column, Object value) {
int r = indexOf(rowID);
return set(r, column, value);
}
/**
* remove row
*
* @param rowId row identifier
*/
public Object remove(ID rowId) {
for (Row> r : rows) {
if (r.rowId.equals(rowId)) {
return rows.remove(r);
}
}
return null;
}
/**
* remove row at given index
*
* @param index row index
* @return removed row
*/
public Row remove(int index) {
return rows.remove(index);
}
/**
* base implementation of getting a cell value. overwrite this method to do extended formatting.
*
* @param rowId row identifer
* @param column column identifier
* @return cell value
* @throws NullPointerException if rowId couldn't be found
*/
public Object get(int row, int column) {
checkSizes(row, column);
return rows.get(row).values[column];
}
/**
* get
*
* @param rowId row identifer
* @param column column identifier
* @return cell value
* @throws NullPointerException if rowId couldn't be found
*/
public Object get(ID row, H column) {
int i = indexOf(row);
return get(i, column);
}
/**
* get
*
* @param row row index
* @param column column identifier
* @return cell value or null
*/
public Object get(int row, H column) {
int i = Arrays.binarySearch(header, column);
return get(row, i);
}
/**
* getRowID
*
* @param row row index
* @return row identifier
*/
public ID getRowID(int row) {
return rows.get(row).rowId;
}
/**
* gets row content directly. be careful using that method from outside. the base method {@link #get(int, int)} is
* not used.
*
* @param rowId row identifier
* @param arrType array type. this must be the type, you gave on constructing this class! the value-array will not
* be changed!
* @return row values
*/
public Object[] get(ID rowId) {
int i = indexOf(rowId);
return rows.get(i).values;
}
/**
* convenience to get a row wrapped into a known object type. delegates to {@link #get(Object)}. no check will be
* done for the compatibility of the given array type! the returned array is a copy of th origin table row. please
* see {@link #set(Object, Object...)} to refresh your changed values.
*
* @param array type to pack the given row into.
* @param rowId row values to be returned
* @param arrayType array type
* @return array filled with row values
*/
public S[] get(ID rowId, Class arrayType) {
Object[] objects = get(rowId);
S[] arr = (S[]) Array.newInstance(arrayType, objects.length);
System.arraycopy(objects, 0, arr, 0, objects.length);
return arr;
}
/**
* get cell value
*
* @param rowId row identifer
* @param column column identifier
* @return cell value
* @throws ArrayIndexOutOfBoundsException if rowId couldn't be found
*/
public Object get(ID rowId, int column) {
return get(indexOf(rowId), column);
}
/**
* like in {@link List}. works only, if row ID type implements its equals(Object) correctly.
*
* @param rowId
* @return
*/
public int indexOf(ID rowId) {
Row.temp.rowId = rowId;
if (rowId.equals(Integer.valueOf(0))) // header row
return -1;
int i = rows.indexOf(Row.temp);
if (i == -1) {
throw new IllegalArgumentException("The row-id " + rowId + " couldn't be found on table " + this);
}
return i;
// int i = 0;
// for (Row r : rows) {
// if (r.rowId.equals(rowId)) {
// return i;
// }
// i++;
// }
// return -1;
}
/**
* size
*
* @return count of rows
*/
public int size() {
return rows.size();
}
/**
* getColumnCount
*
* @return count of header columns
*/
public int getColumnCount() {
return header.length;
}
public int getRowCount() {
return size();
}
protected final void checkSizes(int row, int column) {
/*
* to be a little bit faster, we throw a readable exception only on debugging
*/
if (LOG.isDebugEnabled()) {
checkRowSize(row);
checkColumnSize(column);
}
}
protected final void checkRowSize(int row) {
if (row == -1 || row >= rows.size()) {
throw new IllegalArgumentException("The given row index " + row
+ " is unavailable. Only "
+ rows.size()
+ " rows are available!");
}
}
protected final void checkColumnSize(int column) {
if (column == -1 || column >= header.length) {
LOG.debug(dump());
throw new IllegalArgumentException("The given column index " + column
+ " is unavailable. Only "
+ header.length
+ " columns are available!");
}
}
protected final void checkRowSize(Object... values) {
assert values.length != header.length : "value array must have same size as header array!";
}
public void save(String relDir) {
String path = relDir + name + FILE_EXT;
new File(path).getParentFile().mkdirs();
FileUtil.writeBytes(dump().getBytes(), path, false);
}
/**
* @return Returns the name.
*/
public String getName() {
return name;
}
public static T load(String file) {
return (T) load(file, TableList.class);
}
public static T load(String file, Class type) {
return load(file, type, DIV);
}
public static T load(String file, Class type, String div) {
//TODO: load from dump...
T logic = null;
try {
Scanner sc = new Scanner(new File(file).getAbsoluteFile());
if (sc.hasNextLine()) {//header
String[] header = sc.nextLine().split(div);
Object[] args = new Object[1];
String[] header1 = CollectionUtil.copyOfRange(header, 1, header.length);
//convert the header(strings) into types defaultHeaderType
Comparable[] h = new Comparable[header1.length];
for (int i = 0; i < header1.length; i++) {
h[i] = (Comparable) BeanClass.call(type, "createDefaultHeader", new Class[]{Object.class}, header1[i]);
}
args[0] = h;
//create the table instance
logic = BeanClass.createInstance(type, args);
logic.name = StringUtil.substring(file, "/", ".", true);
String[] ss;
while (sc.hasNextLine()) {
ss = sc.nextLine().split(div);
Object[] row = CollectionUtil.copyOfRange(ss, 0, ss.length, Object[].class);
//TODO: row-id?
logic.addAll(true, row);
}
}
} catch (FileNotFoundException e) {
ManagedException.forward(e);
}
return logic;
}
/**
* wraps the given source string into a default header instance of this class. this implementatino simply returns
* the source string. each extension class shuold provide its own 'createDefaultHeader(string)'
*
* @param source header to be wrapped
* @return wrapped source
*/
protected static Object createDefaultHeader(Object source) {
return source;
}
public String dump() {
return dump(DIV, true);
}
/**
* dump
*
* @return
*/
public String dump(String div, boolean resolve) {
StringBuilder buf = new StringBuilder(20 + header.length
* 5
+ rows.size()
* 20
+ rows.size()
* header.length
* 4);
buf.append(CELL_ROOT + div);//row-id header
for (int i = 0; i < header.length; i++) {
buf.append(header[i] + div);
}
buf.append(LF);
dumpValues(buf, div, resolve);
return buf.toString();
}
protected void dumpValues(StringBuilder buf, String div, boolean resolve) {
for (Row r : rows) {
buf.append(r.dump() + LF);
}
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return this.getClass().getSimpleName() + "("
+ header.length
+ ", "
+ rows.size()
+ "), rowIDs: "
+ StringUtil.toString(rows, 80);
}
}
/**
* internal row/line holder. the value array cannot be typed - each row or column may hold different value types.
*
* @param optional row-id type
* @author Thomas Schneider
* @version $Revision$
*/
class Row {
static final Row