nom.tam.util.ColumnTable Maven / Gradle / Ivy
package nom.tam.util;
/*
* #%L
* nom.tam FITS library
* %%
* Copyright (C) 2004 - 2024 nom-tam-fits
* %%
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
* #L%
*/
import java.io.EOFException;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import nom.tam.util.type.ElementType;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
*
* Table data that is stored (internally) in column major format. This class has been completely re-written by A. Kovacs
* for 1.18. We keep the old API for compatibility, but make some practical additions to it.
*
* Note that while column tables are fine to use for accessing data from FITS ASCII tables, they are generally not
* suitable for user-access of binary table data, because the column tables contain data in the format that is used for
* storing values in the regular FITS table in the file. That is:
*
* - logical values are represented by
byte
entries of 'T', 'F' or '0'.
* - String values are represented as ASCII arrays bytes.
* - Complex values are represented by
float[2]
or double[2]
.
* - Variable length columns of all types are represented by heap pointers of
int[2]
or
* long[2]
.
*
*
* @param dummy generic type parameter that is no longer used. We'll stick to it a a memento of the bad design
* decisions of the past...
*/
public class ColumnTable implements DataTable, Cloneable {
/** A list of columns contained in this table */
private ArrayList> columns = new ArrayList<>();
/** The number of rows */
private int nrow = 0;
/** The smallest dynamic allocation when addig / deleting rows */
private static final int MIN_CAPACITY = 16;
/**
* Allow the client to provide opaque data.
*/
private T extraState;
/**
* Creates an empty column table.
*
* @since 1.18
*/
public ColumnTable() {
}
/**
* Create the object after checking consistency.
*
* @param arrays An array of one-d primitive arrays.
* @param sizes The number of elements in each row for the corresponding column
*
* @throws TableException if the structure of the columns is not consistent
*/
public ColumnTable(Object[] arrays, int[] sizes) throws TableException {
for (int i = 0; i < arrays.length; i++) {
addColumn(arrays[i], sizes[i]);
}
}
/**
* Checks if the table is empty (contains no data and no column definitions)
*
* @return true
if the table has no columns defined, otherwise false
*
* @since 1.18
*
* @see #clear()
* @see #deleteAllRows()
*/
public final boolean isEmpty() {
return columns.isEmpty();
}
/**
* Clears the table, discarding all columns.
*
* @see #deleteAllRows()
*
* @since 1.18
*/
public void clear() {
columns.clear();
nrow = 0;
}
/**
* Makes sure that the table is expanded to hold up to the specified number of rows without having to grow
* dynamically. Typically it not necessary when adding new rows to the table, as the automatic dynamic allocation is
* quite efficient, but if you know in advance how many rows you want to add to the table, it does not hurt to just
* grow the table once, raher than a few times potentially. Note, that if deleting rows may annul the effect of this
* call, and shrink the table to a reasonable size after the deletions.
*
* @param rows the number of rows we will want the table to hold at some point in the future...
*
* @see #addRow(Object[])
*/
public void ensureSize(int rows) {
for (Column> c : columns) {
c.ensureSize(rows);
}
}
/**
* Checks a column data, in which elements have been already wrapped to ensure that the column is self consistent,
* containing
*
* @param newColumn An array holding the column data with each entry corresponding to hte data for a row.
*
* @return the element count
*
* @throws TableException if the data is inconsistent, or contains null, or non-arrays
*/
private int checkWrappedColumn(Object newColumn) throws TableException, NullPointerException {
// For array elements, check consistency...
if (!(newColumn instanceof Object[])) {
// Check as scalar column...
checkFlatColumn(newColumn, 1);
return 1;
}
try {
int[] dims = ArrayFuncs.checkRegularArray(newColumn, false);
if (dims.length != 2) {
throw new TableException("Not a 2D array: " + newColumn.getClass());
}
} catch (Exception e) {
throw new TableException(e);
}
Object[] entries = (Object[]) newColumn;
if (entries.length == 0) {
return 0;
}
Object first = entries[0];
if (!first.getClass().getComponentType().isPrimitive()) {
throw new TableException("Entries are not a primitive arrays: " + first.getClass());
}
return Array.getLength(first);
}
/**
* Adds a column as an array of scalars or regular 1D primitve array elements.
*
* @param newColumn the column to add, either as a 1D array of scalar primitives, or a regular 2D array of
* primitives, in which each row contains the same type of 1D primitive array of the same
* sizes.
*
* @throws TableException if the new column is not a 1D or 2D array of primitives, or it it does not match the
* number of existing table rows or if the column contains an irregular 2D array.
*
* @since 1.18
*
* @see #getWrappedColumn(int)
* @see #setWrappedColumn(int, Object)
*/
@SuppressWarnings("unchecked")
public void addWrappedColumn(Object newColumn) throws TableException {
if (newColumn == null) {
throw new TableException("Cannot add a null column.");
}
int eCount = checkWrappedColumn(newColumn);
Class> eType = newColumn.getClass().getComponentType();
if (eType.isArray()) {
eType = eType.getComponentType();
}
@SuppressWarnings("rawtypes")
Column c = createColumn(eType, eCount);
c.data = newColumn;
nrow = Array.getLength(newColumn);
columns.add(c);
}
/**
* Adds a new column with the specified primitive base class and element count.
*
* @param type the primitive class of the elements in this colymn, such as boolean.class
* @param size the number of primitive elements (use 1 to create scalar columns)
*
* @throws TableException if the type is not a primitive type or the size is invalid.
*
* @see #addColumn(Object, int)
*
* @since 1.18
*/
public void addColumn(Class> type, int size) throws TableException {
columns.add(createColumn(type, size));
}
/**
* Converts a one-dimensional flat array of elements to a wrapped column, in which each top-level entry contains the
* specified number of row elements
*
* @param data a one-domensional primitive array
* @param size the number of column data elements per row
*
* @return the wrapped column data, in which each tp-level entry contains data for a specific row.
*
* @throws TableException If the data could not be converted
*/
private Object wrapColumn(Object data, int size) throws TableException {
checkFlatColumn(data, size);
// Fold the 1D array into 2D array of subarrays for storing
Class> type = data.getClass().getComponentType();
int len = size == 0 ? nrow : Array.getLength(data) / size;
// The parent array
Object[] array = (Object[]) Array.newInstance(data.getClass(), len);
int offset = 0;
for (int i = 0; i < len; i++, offset += size) {
// subarrays...
array[i] = Array.newInstance(type, size);
System.arraycopy(data, offset, array[i], 0, size);
}
return array;
}
/**
* Adds a column in flattened 1D format, specifying the size of array 'elements'.
*
* @param newColumn the column to add.
* @param size size for the column
*
* @throws TableException if the new column is not a 1D array of primitives, or it it does not conform to the number
* of existing table rows for the given element size.
*
* @see #addColumn(Class, int)
* @see #setColumn(int, Object)
*/
@SuppressWarnings("unchecked")
public void addColumn(Object newColumn, int size) throws TableException {
if (newColumn == null) {
throw new TableException("Cannot add a null column: we don't know its type.");
}
if (size < 0) {
throw new TableException("Invalid element size: " + size);
}
if (size == 1 && newColumn.getClass().getComponentType().isPrimitive()) {
// Add scalar columns as is...
addWrappedColumn(newColumn);
return;
}
checkFlatColumn(newColumn, size);
@SuppressWarnings("rawtypes")
Column c = createColumn(newColumn.getClass().getComponentType(), size);
c.data = wrapColumn(newColumn, size);
columns.add(c);
nrow = Array.getLength(c.data);
}
/**
* Returns the next standard table capacity step that will allow adding at least one more row to the table.
*
* @return the standard capacity (number of rows) to which we'd want to grow to contain another table row.
*/
private int nextLargerCapacity() {
return nextLargerCapacity(nrow);
}
/**
* Returns the next standard table capacity step that will allow adding at least one more row to the table.
*
* @param rows The number of rows we should be able to store.
*
* @return the standard capacity (number of rows) to which we'd want to grow to contain another table row.
*/
private int nextLargerCapacity(int rows) {
if (rows < MIN_CAPACITY) {
return MIN_CAPACITY;
}
// Predictable doubling beyond the minimum size...
return (int) Math.min(Long.highestOneBit(rows) << 1, Integer.MAX_VALUE);
}
/**
* Checks the integrity of an array containing element for a new row, with each element corresponding to an entry to
* the respective table column.
*
* @param row An array containing data for a new table row.
*
* @throws TableException If the row structure is inconsistent with that of the table.
*/
private void checkRow(Object[] row) throws TableException {
// Check that the row matches existing columns
if (row.length != columns.size()) {
throw new TableException("Mismatched row size: " + row.length + ", expected " + columns.size());
}
for (int col = 0; col < row.length; col++) {
Column> c = columns.get(col);
c.checkEntry(row[col]);
}
}
/**
* Add a row to the table. Each element of the row must be a 1D primitive array. If this is not the first row in the
* table, each element must match the types and sizes of existing rows.
*
* @param row the row to add
*
* @throws TableException if the row contains other than 1D primitive array elements or if the elements do not match
* the types and sizes of existing table columns.
*
* @see #deleteRow(int)
* @see #ensureSize(int)
*/
public void addRow(Object[] row) throws TableException {
if (row == null) {
throw new TableException("Cannot add null row");
}
if (nrow == Integer.MAX_VALUE) {
throw new TableException("Table has reached its capacity limit");
}
if (isEmpty()) {
// This is the first row in the table, create columns with their elements if possible
try {
for (int col = 0; col < row.length; col++) {
addColumn(row[col], Array.getLength(row[col]));
}
return;
} catch (TableException e) {
// The row was invalid, clear the table and re-throw the exception
columns = new ArrayList<>();
throw e;
}
}
checkRow(row);
// Get the size we'll grow to if we must...
int capacity = nextLargerCapacity();
for (int i = 0; i < row.length; i++) {
Column> c = columns.get(i);
c.ensureSize(capacity);
c.checkEntry(row[i]);
c.setArrayElement(nrow, row[i]);
}
nrow++;
}
/**
* Checks if a data in column format is consistent with this table, for example before adding it.
*
* @param data A one-dimensional representation of the column data
* @param size the number of primitive elements per table row
*
* @throws TableException if the column is not consistent with the current table structure.
*/
private void checkFlatColumn(Object data, int size) throws TableException {
if (!data.getClass().isArray()) {
throw new TableException("Argument is not an array: " + data.getClass());
}
int len = Array.getLength(data);
// Data cannot be null here (we check upstream)
// if (data == null) {
// throw new TableException("Unexpected null column");
// }
if (size > 0 && len % size != 0) {
throw new TableException("The column size " + len + " is not a multiple of the element size " + size);
}
if (nrow == 0 || size == 0) {
return;
}
if (len != nrow * size) {
throw new TableException("Mismatched element count: " + len + ", expected " + (nrow * size) + " for " + nrow
+ " rows of size " + size);
}
}
@Override
@SuppressWarnings("unchecked")
protected ColumnTable clone() {
try {
return (ColumnTable) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
/**
* Returns a deep copy of this column table, such that modification to either the original or the copy will not
* affect the other.
*
* @return A deep (independent) copy of this column table
*
* @throws TableException (for back compatibility) never thrown.
*/
@SuppressWarnings("cast")
public ColumnTable copy() throws TableException {
ColumnTable copy = (ColumnTable) clone();
copy.columns = new ArrayList<>(columns.size());
for (Column> c : columns) {
copy.columns.add(c.copy(nrow));
}
return copy;
}
/**
* Deletes a column from this table.
*
* @param col the 0-based column index.
*
* @throws TableException if the column index is out of bounds
*
* @see #deleteColumns(int, int)
*
* @since 1.18
*/
public void deleteColumn(int col) throws TableException {
if (col < 0 || col >= columns.size()) {
throw new TableException("Column out of bounds: col=" + col + ", size=" + columns.size());
}
columns.remove(col);
if (isEmpty()) {
nrow = 0;
}
}
/**
* Delete a contiguous set of columns from the table.
*
* @param start The first column (0-indexed) to be deleted.
* @param len The number of columns to be deleted.
*
* @throws TableException if the request goes outside the boundaries of the table or if the length is negative.
*
* @see #deleteColumn(int)
*/
public void deleteColumns(int start, int len) throws TableException {
if (len == 0) {
return;
}
if (start < 0 || len < 0 || start + len > columns.size()) {
throw new TableException(
"Column eange out of bounds: start=" + start + ", len=" + len + ", size=" + columns.size());
}
ArrayList> c = new ArrayList<>();
int i;
for (i = 0; i < start; i++) {
c.add(columns.get(i));
}
i += len;
for (; i < columns.size(); i++) {
c.add(columns.get(i));
}
columns = c;
if (isEmpty()) {
nrow = 0;
}
}
/**
* Delete all rows from the table, but leaving the column structure intact.
*
* @throws TableException if the request goes outside the boundaries of the table or if the length is negative.
*
* @see #deleteRows(int, int)
* @see #clear()
*/
public final void deleteAllRows() throws TableException {
nrow = 0;
for (Column> c : columns) {
c.trim(MIN_CAPACITY);
}
}
/**
* Delete a row from the table.
*
* @param row The row (0-indexed) to be deleted.
*
* @throws TableException if the request goes outside the boundaries of the table or if the length is negative.
*
* @see #deleteRows(int, int)
*/
public final void deleteRow(int row) throws TableException {
deleteRows(row, 1);
}
/**
* Delete a contiguous set of rows from the table.
*
* @param from the 0-based index of the first tow to be deleted.
* @param length The number of rows to be deleted.
*
* @throws TableException if the request goes outside the boundaries of the table or if the length is negative.
*
* @see #deleteRow(int)
*/
public void deleteRows(int from, int length) throws TableException {
if (length == 0) {
return;
}
if (from < 0 || length < 0 || from + length > nrow) {
throw new TableException("Row range out of bounds: start=" + from + ", len=" + length + ", size=" + nrow);
}
int maxSize = nextLargerCapacity(nrow - length);
for (Column> c : columns) {
c.deleteRows(from, length, nrow, maxSize);
}
nrow -= length;
}
/**
* Returns the primitive based class of the elements in a given colum.
*
* @param col the 0-based column index
*
* @return the primitive base class of the elements in the column, e.g. short.class
.
*
* @since 1.18
*/
public final Class> getElementClass(int col) {
return columns.get(col).baseType();
}
/**
* Get the base classes of the columns. As of 1.18, this method returns a copy ot the array used internally, which
* is safe to modify.
*
* @return An array of Class objects, one for each column.
*
* @deprecated Use {@link #getElementClass(int)} instead. This method may be removed in the future.
*
* @see #getElementClass(int)
*/
public Class>[] getBases() {
Class>[] bases = new Class>[columns.size()];
for (int i = 0; i < bases.length; i++) {
bases[i] = getElementClass(i);
}
return bases;
}
/**
* Returns the wrapped column data, in which each entry correspond to data for a given row. If the column contains
* non-scalar elements, then each entry in the returned array will be a primitive array of the column's element
* size.
*
* @param col the 0-based column index
*
* @return the array in which each entry corresponds to table row. For scalar columns this will be a primitive
* array of {@link #getNRows()} entries. Otherwise, it will be an array, whch contains the primitive
* array elements for each row.
*
* @see #getColumn(int)
* @see #addWrappedColumn(Object)
* @see #setWrappedColumn(int, Object)
*
* @since 1.18
*/
public Object getWrappedColumn(int col) {
Column> c = columns.get(col);
c.trim(nrow);
return c.getData();
}
/**
*
* Returns the data for a particular column in as a single 1D array of primitives. See
* {@link nom.tam.fits.TableData#addColumn(Object)} for more information about the format of data elements in
* general.
*
*
* @param col The 0-based column index.
*
* @return an array of primitives (for scalar columns), or else an Object[]
array, or possibly
* null
*
* @see #getWrappedColumn(int)
* @see #setColumn(int, Object)
* @see #getElement(int, int)
* @see #getNCols()
*/
@Override
public Object getColumn(int col) {
Column> c = columns.get(col);
c.trim(nrow);
return c.getFlatData();
}
/**
* Returns the data for all columns as an array of flattened 1D primitive arrays.
*
* @return An array containing the flattened data for each column. Each columns's data is represented by a single 1D
* array holding all elements for that column. Because this is not an easily digestible format, you are
* probably better off using {@link #getElement(int, int)} or {@link #getRow(int)} instead.
*
* @see #getColumn(int)
*/
public Object[] getColumns() {
Object[] table = new Object[columns.size()];
for (int i = 0; i < table.length; i++) {
table[i] = getColumn(i);
}
return table;
}
@Override
public Object getElement(int row, int col) {
if (row < 0 || row >= nrow) {
throw new ArrayIndexOutOfBoundsException(row);
}
return columns.get(col).getArrayElement(row);
}
/**
* Returns the extra state information of the table. The type and nature of this information is implementation
* dependent.
*
* @return the object capturing the implementation-specific table state
*
* @deprecated (for internal use) No longer used, will be removed in the future.
*/
public T getExtraState() {
return extraState;
}
@Override
public final int getNCols() {
return columns.size();
}
@Override
public final int getNRows() {
return nrow;
}
@Override
public Object getRow(int row) {
if (row < 0 || row >= nrow) {
throw new ArrayIndexOutOfBoundsException(row);
}
Object[] x = new Object[columns.size()];
for (int col = 0; col < x.length; col++) {
x[col] = getElement(row, col);
}
return x;
}
/**
* Returns the size of 1D array elements stored in a given column
*
* @param col the 0-based column index
*
* @return the array size in the column (scalars return 1)
*
* @since 1.18
*/
public final int getElementSize(int col) {
return columns.get(col).elementCount();
}
/**
* Returns the flattened (1D) size of elements in each column of this table. As of 1.18, this method returns a copy
* ot the array used internally, which is safe to modify.
*
* @return an array with the byte sizes of each column
*
* @see #getElementSize(int)
*
* @deprecated Use {@link #getElementSize(int)} instead. This method may be removed in the future.
*
* @since 1.18
*/
public int[] getSizes() {
int[] sizes = new int[columns.size()];
for (int i = 0; i < sizes.length; i++) {
sizes[i] = getElementSize(i);
}
return sizes;
}
/**
* Returns the Java array type character for the elements stored in a given column.
*
* @param col the 0-based column index
*
* @return the Java array type of the elements in the column, such as 'I' if the array is class is
* I[
(that is for int[]
).
*/
public final char getTypeChar(int col) {
return columns.get(col).getElementType().type();
}
/**
* Get the characters describing the base classes of the columns. As of 1.18, this method returns a copy ot the
* array used internally, which is safe to modify.
*
* @return An array of type characters (Java array types), one for each column.
*
* @deprecated Use {@link #getTypeChar(int)} instead. This method may be removed in the future.
*
* @see #getTypeChar(int)
*/
public char[] getTypes() {
char[] types = new char[columns.size()];
for (int i = 0; i < types.length; i++) {
types[i] = getTypeChar(i);
}
return types;
}
/**
* Reads the table's data from the input, in row-major format
*
* @param in The input to read from.
*
* @throws EOFException is already at the end of file.
* @throws IOException if the reading failed
*
* @see #write(ArrayDataOutput)
*/
public void read(ArrayDataInput in) throws EOFException, IOException {
for (int row = 0; row < nrow; row++) {
for (Column> c : columns) {
c.read(row, in);
}
}
}
@SuppressWarnings("unchecked")
@Override
public void setColumn(int col, Object newColumn) throws TableException {
if (newColumn == null) {
throw new TableException("Cannot set column data to null");
}
if (!newColumn.getClass().isArray()) {
throw new TableException("Not an array: " + newColumn.getClass().getName());
}
@SuppressWarnings("rawtypes")
Column c = columns.get(col);
if (!c.baseType().equals(newColumn.getClass().getComponentType())) {
throw new TableException(
"Mismatched type " + newColumn.getClass().getName() + ", expected " + c.baseType().getName());
}
if (Array.getLength(newColumn) != nrow * c.elementCount()) {
throw new TableException(
"Mismatched size " + Array.getLength(newColumn) + ", expected " + (nrow * c.elementCount()));
}
c.data = c.elementCount() > 1 ? wrapColumn(newColumn, c.elementCount()) : newColumn;
}
/**
* Sets new data for a column, in wrapped format. The argument is an 1D or 2D array of primitives, in which the
* eading dimension must match the number of rows already in the table (if any).
*
* @param col the zero-based column index
* @param newColumn the new column data, either as a 1D array of scalar primitives, or a regular 2D array of
* primitives, in which each row contains the same type of 1D primitive array of the same
* sizes.
*
* @throws TableException if the new column data is not a 1D or 2D array of primitives, or it it does not match the
* number of existing table rows or if the column contains an irregular 2D array.
*
* @since 1.18
*
* @see #getWrappedColumn(int)
* @see #addWrappedColumn(Object)
*/
@SuppressWarnings("unchecked")
public void setWrappedColumn(int col, Object newColumn) throws TableException {
if (!newColumn.getClass().isArray()) {
throw new TableException("Not an array: " + newColumn.getClass().getName());
}
if (Array.getLength(newColumn) != nrow) {
throw new TableException("Mismatched row count " + Array.getLength(newColumn) + ", expected " + nrow);
}
@SuppressWarnings("rawtypes")
Column c = columns.get(col);
int eSize = 0;
try {
eSize = checkWrappedColumn(newColumn);
} catch (Exception e) {
throw new TableException(e);
}
if (eSize != c.elementCount()) {
throw new TableException("Mismatched element size " + eSize + ", expected " + c.elementCount());
}
Class> eType = newColumn.getClass().getComponentType();
if (newColumn instanceof Object[]) {
eType = eType.getComponentType();
}
if (!c.baseType().equals(eType)) {
throw new TableException(
"Mismatched type " + newColumn.getClass().getName() + ", expected " + c.baseType().getName());
}
c.data = newColumn;
}
@Override
public void setElement(int row, int col, Object x) throws TableException {
if (row < 0 || row >= nrow) {
throw new ArrayIndexOutOfBoundsException(row);
}
Column> c = columns.get(col);
c.checkEntry(x);
c.setArrayElement(row, x);
}
/**
* Store additional information that may be needed by the client to regenerate initial arrays.
*
* @param opaque the extra state to set.
*
* @deprecated (for internal use) No longer used, will be removed in the future. We used the extra
* state to carry properties of an enclosing class in this enclosed object, so we could
* inherit those to new enclosing class instances. This is bad practie. If one needs data
* from the enclosing object, it should be handled by passing the enclsing object, and not
* this enclosed table.
*/
public void setExtraState(T opaque) {
extraState = opaque;
}
@Override
public void setRow(int row, Object data) throws TableException {
if (row < 0 || row >= nrow) {
throw new ArrayIndexOutOfBoundsException(row);
}
if (data == null) {
throw new TableException("Unexpected null data for row " + row);
}
if (!(data instanceof Object[])) {
throw new TableException("Not an Object[] array: " + data.getClass().getName());
}
Object[] r = (Object[]) data;
checkRow(r);
for (int i = 0; i < columns.size(); i++) {
Column> c = columns.get(i);
c.checkEntry(r[i]);
c.setArrayElement(row, r[i]);
}
}
/**
* Writes the table's data to an output, in row-major format.
*
* @param out the output stream to write to.
*
* @throws IOException if the write operation failed
*
* @see #read(ArrayDataInput)
*/
public void write(ArrayDataOutput out) throws IOException {
for (int row = 0; row < nrow; row++) {
for (Column> c : columns) {
c.write(row, out);
}
}
}
/**
* Write a column of a table.
*
* @param out the output stream to write to.
* @param rowStart first row to write
* @param rowEnd the exclusive ending row index (not witten)
* @param col the zero-based column index to write.
*
* @throws IOException if the write operation failed
*/
public void write(ArrayDataOutput out, int rowStart, int rowEnd, int col) throws IOException {
columns.get(col).write(rowStart, rowEnd - rowStart, out);
}
/**
* Reads a column of a table from a
*
* @param in The input stream to read from.
* @param rowStart first row to read
* @param rowEnd the exclusive ending row index (not read)
* @param col the zero-based column index to read.
*
* @throws EOFException is already at the end of file.
* @throws IOException if the reading failed
*/
public void read(ArrayDataInput in, int rowStart, int rowEnd, int col) throws EOFException, IOException {
columns.get(col).read(rowStart, rowEnd - rowStart, in);
}
private Column> createColumn(Class> type, int size) throws TableException {
if (type == null) {
throw new TableException("Column type cannot be null.");
}
if (!type.isPrimitive()) {
throw new TableException("Not a primitive base type: " + type.getName());
}
if (size == 1) {
if (type.equals(byte.class)) {
return new Bytes();
}
if (type.equals(boolean.class)) {
return new Booleans();
}
if (type.equals(char.class)) {
return new Chars();
}
if (type.equals(short.class)) {
return new Shorts();
}
if (type.equals(int.class)) {
return new Integers();
}
if (type.equals(long.class)) {
return new Longs();
}
if (type.equals(float.class)) {
return new Floats();
}
if (type.equals(double.class)) {
return new Doubles();
}
}
return new Generic(type, size);
}
/**
* Container for one column in the table.
*
* @author Attila Kovacs
*
* @param The generic type of the data elements held in the table
*/
private abstract static class Column implements Cloneable {
protected Data data;
private ElementType> fitsType;
/**
* Instantiates a new column data container
*
* @param fitsType The primitive element type that is used to store data for this column in the FITS
*/
Column(ElementType> fitsType) {
this.fitsType = fitsType;
init(fitsType.primitiveClass());
}
@SuppressWarnings("unchecked")
void init(Class> storeType) {
data = (Data) Array.newInstance(storeType, 0);
}
/**
* Returns the data array that holds all elements (one entry per row) for this column. It may be a primitive
* array or an object array.
*
* @return the array that holds data for this column.
*/
Data getData() {
return data;
}
/**
* Returns the data a a 1D array containing all elements for this column. It may be a primitive array or an
* object array.
*
* @return the array that holds data for this column.
*/
Object getFlatData() {
return data;
}
/**
* Returns the FITS element type contained in this column.
*
* @return the FITS type of data elements as they are stored in FITS
*/
ElementType> getElementType() {
return fitsType;
}
/**
* Returns the primitive element type which reprensents data for this column in FITS.
*
* @return the primitive type that is used when writing this column's data into FITS
*/
Class> baseType() {
return fitsType.primitiveClass();
}
/**
* Returns the array data class that stores elements in this column.
*
* @return the class of array that holds elements in this column
*/
Class> arrayType() {
return data.getClass();
}
/**
* Returns the number of basic elements stored per row in this column.
*
* @return the number of basic elements per row
*/
int elementCount() {
return 1;
}
/**
* Returns the number of rows currently allocated in this column, which may exceed the number of entries
* currently populated.
*
* @return the number of rows allocated at present
*/
int capacity() {
return Array.getLength(data);
}
void deleteRows(int from, int len, int size, int maxCapacity) {
int end = from + len;
System.arraycopy(data, end, data, from, size - end);
trim(maxCapacity);
}
/**
* Reads a single table entry from an input
*
* @param index the zero-based row index of the column entry
* @param in the input to read from
*
* @return the number of bytes read from the input.
*
* @throws EOFException if already at the end of file.
* @throws IOException if the entry could not be read
*/
abstract int read(int index, ArrayDataInput in) throws EOFException, IOException;
/**
* Writes a since table entry to an output
*
* @param index the zero-based row index of the column entry
* @param out the output to write to
*
* @throws IOException if the entry could not be written
*/
abstract void write(int index, ArrayDataOutput out) throws IOException;
/**
* Reads a sequence of consecutive table entries from an input
*
* @param from the zero-based row index of the first column entry to read
* @param n the number of consecutive rows to read
* @param in the input to read from
*
* @return the number of bytes read from the input.
*
* @throws EOFException if already at the end of file.
* @throws IOException if the entry could not be read
*/
abstract int read(int from, int n, ArrayDataInput in) throws EOFException, IOException;
/**
* Writes a sequence of consecutive table entries to an output
*
* @param from the zero-based row index of the first column entry to write
* @param n the number of consecutive rows to write
* @param in the output to write to
*
* @throws IOException if the entry could not be written
*/
abstract void write(int from, int n, ArrayDataOutput out) throws IOException;
/**
* Checks if an object is consistent with becoming an entry in this column.
*
* @param x an object that is expected to be a compatible column entry
*
* @throws TableException if the object is not consistent with the data expected for this column.
*/
void checkEntry(Object x) throws TableException {
if (x == null) {
throw new TableException("Unexpected null element");
}
if (!baseType().equals(x.getClass().getComponentType())) {
throw new TableException(
"Incompatible element type: " + x.getClass().getName() + ", expected " + arrayType());
}
if (Array.getLength(x) != elementCount()) {
throw new TableException(
"Incompatible element size: " + Array.getLength(x) + ", expected " + elementCount());
}
}
/**
* Returns the entry at the specified index as an array. Primitive elements will be wrapped into an array of one
*
* @param i the zero-based row index of the entry.
*
* @return the entry as an array. Primitive values will be returned as a new array of one.
*/
abstract Object getArrayElement(int i);
/**
* Sets a new array entry at the specified index. Primitive values must be be wrapped into an array of one.
*
* @param i the zero-based row index of the entry.
* @param o the new entry as an array. Primitive values must be wrapped into an array of one.
*/
abstract void setArrayElement(int i, Object o);
/**
* Resized this columns storage to an allocation of the specified size. The size should be greater or equals to
* the number of elements already contained to avoid loss of data.
*
* @param size the new allocation (number of rows that can be contained)
*/
abstract void resize(int size);
/**
* Ensures that the column has storage allocated to contain the speciifed number of entries, and grows the
* column's allocation as necessaty to contain the specified number of rows in the future.
*
* @param size the number of rows that the table should be able to contain
*/
void ensureSize(int size) {
if (size > capacity()) {
resize(size);
}
}
/**
* Trims the table to the specified size, as needed. The size should be greater or equals to the number of
* elements already contained to avoid loss of data.
*
* @param size the new allocation (number of rows that can be contained)
*/
void trim(int size) {
if (capacity() > size) {
resize(size);
}
}
@SuppressWarnings("unchecked")
@Override
protected Column clone() {
try {
return (Column) super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
@SuppressWarnings("unchecked")
Data copyData(int length) {
Data copy = (Data) Array.newInstance(baseType(), length);
System.arraycopy(data, 0, copy, 0, Math.min(length, Array.getLength(data)));
return copy;
}
/**
* Makes a copy of this column of the specified allocation for the number of rows in the copy. If the size is
* less than the number of entries this column contains, the excess entries will be discared from the copy.
*
* @param size the number of rows the copy might contain.
*
* @return A copy of this column with storage for the specified number of rows.
*/
Column copy(int size) {
Column c = clone();
c.data = copyData(size);
return c;
}
}
/**
* A column for data consisting of 8-bit bytes.
*
* @author Attila Kovacs
*/
private static class Bytes extends Column {
/** Construct as new container for a byte-based data column */
Bytes() {
super(ElementType.BYTE);
}
@Override
void resize(int size) {
data = Arrays.copyOf(data, size);
}
@Override
int read(int index, ArrayDataInput in) throws IOException {
int i = in.read();
if (i < 0) {
throw new EOFException();
}
data[index] = (byte) i;
return 1;
}
@Override
void write(int index, ArrayDataOutput out) throws IOException {
out.write(data[index]);
}
@Override
int read(int from, int n, ArrayDataInput in) throws IOException {
int got = in.read(data, from, n);
if (got < 0) {
throw new EOFException();
}
return -1;
}
@Override
void write(int from, int n, ArrayDataOutput out) throws IOException {
out.write(data, from, n);
}
@Override
byte[] getArrayElement(int i) {
return new byte[] {data[i]};
}
@Override
void setArrayElement(int i, Object o) {
data[i] = ((byte[]) o)[0];
}
}
/**
* A column for data consisting of boolean values.
*
* @author Attila Kovacs
*/
private static class Booleans extends Column {
/** Construct as new container for a boolean-based data column */
Booleans() {
super(ElementType.BOOLEAN);
}
@Override
void resize(int size) {
data = Arrays.copyOf(data, size);
}
@Override
int read(int index, ArrayDataInput in) throws IOException {
data[index] = in.readBoolean();
return 1;
}
@Override
void write(int index, ArrayDataOutput out) throws IOException {
out.writeBoolean(data[index]);
}
@Override
int read(int from, int n, ArrayDataInput in) throws IOException {
return in.read(data, from, n);
}
@Override
void write(int from, int n, ArrayDataOutput out) throws IOException {
out.write(data, from, n);
}
@Override
boolean[] getArrayElement(int i) {
return new boolean[] {data[i]};
}
@Override
void setArrayElement(int i, Object o) {
data[i] = ((boolean[]) o)[0];
}
}
/**
* A column for data consisting of 16-bit unicode character values.
*
* @author Attila Kovacs
*/
private static class Chars extends Column {
/** Construct as new container for a unicode-based data column */
Chars() {
super(ElementType.CHAR);
}
@Override
void resize(int size) {
data = Arrays.copyOf(data, size);
}
@Override
int read(int index, ArrayDataInput in) throws IOException {
data[index] = in.readChar();
return ElementType.CHAR.size();
}
@Override
void write(int index, ArrayDataOutput out) throws IOException {
out.writeChar(data[index]);
}
@SuppressFBWarnings(value = "RR_NOT_CHECKED", justification = "not exposed and never needed locally")
@Override
int read(int from, int n, ArrayDataInput in) throws IOException {
return in.read(data, from, n);
}
@Override
void write(int from, int n, ArrayDataOutput out) throws IOException {
out.write(data, from, n);
}
@Override
char[] getArrayElement(int i) {
return new char[] {data[i]};
}
@Override
void setArrayElement(int i, Object o) {
data[i] = ((char[]) o)[0];
}
}
/**
* A column for data consisting of 16-bit integer values.
*
* @author Attila Kovacs
*/
private static class Shorts extends Column {
/** Construct as new container for a 16-bit integer based data column */
Shorts() {
super(ElementType.SHORT);
}
@Override
void resize(int size) {
data = Arrays.copyOf(data, size);
}
@Override
int read(int index, ArrayDataInput in) throws IOException {
int i = in.readUnsignedShort();
if (i < 0) {
throw new EOFException();
}
data[index] = (short) i;
return Short.BYTES;
}
@Override
void write(int index, ArrayDataOutput out) throws IOException {
out.writeShort(data[index]);
}
@SuppressFBWarnings(value = "RR_NOT_CHECKED", justification = "not exposed and never needed locally")
@Override
int read(int from, int n, ArrayDataInput in) throws IOException {
return in.read(data, from, n);
}
@Override
void write(int from, int n, ArrayDataOutput out) throws IOException {
out.write(data, from, n);
}
@Override
short[] getArrayElement(int i) {
return new short[] {data[i]};
}
@Override
void setArrayElement(int i, Object o) {
data[i] = ((short[]) o)[0];
}
}
/**
* A column for data consisting of 32-bit integer values.
*
* @author Attila Kovacs
*/
private static class Integers extends Column {
/** Construct as new container for a 32-bit integer based data column */
Integers() {
super(ElementType.INT);
}
@Override
void resize(int size) {
data = Arrays.copyOf(data, size);
}
@Override
int read(int index, ArrayDataInput in) throws IOException {
data[index] = in.readInt();
return Integer.BYTES;
}
@Override
void write(int index, ArrayDataOutput out) throws IOException {
out.writeInt(data[index]);
}
@Override
int read(int from, int n, ArrayDataInput in) throws IOException {
return in.read(data, from, n);
}
@Override
void write(int from, int n, ArrayDataOutput out) throws IOException {
out.write(data, from, n);
}
@Override
int[] getArrayElement(int i) {
return new int[] {data[i]};
}
@Override
void setArrayElement(int i, Object o) {
data[i] = ((int[]) o)[0];
}
}
/**
* A column for data consisting of 64-bit integer values.
*
* @author Attila Kovacs
*/
private static class Longs extends Column {
/** Construct as new container for a 64-bit integer based data column */
Longs() {
super(ElementType.LONG);
}
@Override
void resize(int size) {
data = Arrays.copyOf(data, size);
}
@Override
int read(int index, ArrayDataInput in) throws IOException {
data[index] = in.readLong();
return Long.BYTES;
}
@Override
void write(int index, ArrayDataOutput out) throws IOException {
out.writeLong(data[index]);
}
@Override
int read(int from, int n, ArrayDataInput in) throws IOException {
return in.read(data, from, n);
}
@Override
void write(int from, int n, ArrayDataOutput out) throws IOException {
out.write(data, from, n);
}
@Override
long[] getArrayElement(int i) {
return new long[] {data[i]};
}
@Override
void setArrayElement(int i, Object o) {
data[i] = ((long[]) o)[0];
}
}
/**
* A column for data consisting of 32-bit floating point values.
*
* @author Attila Kovacs
*/
private static class Floats extends Column {
/** Construct as new container for a 32-bit floating-point based data column */
Floats() {
super(ElementType.FLOAT);
}
@Override
void resize(int size) {
data = Arrays.copyOf(data, size);
}
@Override
int read(int index, ArrayDataInput in) throws IOException {
data[index] = in.readFloat();
return Float.BYTES;
}
@Override
void write(int index, ArrayDataOutput out) throws IOException {
out.writeFloat(data[index]);
}
@Override
int read(int from, int n, ArrayDataInput in) throws IOException {
return in.read(data, from, n);
}
@Override
void write(int from, int n, ArrayDataOutput out) throws IOException {
out.write(data, from, n);
}
@Override
float[] getArrayElement(int i) {
return new float[] {data[i]};
}
@Override
void setArrayElement(int i, Object o) {
data[i] = ((float[]) o)[0];
}
}
/**
* A column for data consisting of 64-bit floating point values.
*
* @author Attila Kovacs
*/
private static class Doubles extends Column {
/** Construct as new container for a 32-bit floating-point based data column */
Doubles() {
super(ElementType.DOUBLE);
}
@Override
void resize(int size) {
data = Arrays.copyOf(data, size);
}
@Override
int read(int index, ArrayDataInput in) throws IOException {
data[index] = in.readDouble();
return Double.BYTES;
}
@Override
void write(int index, ArrayDataOutput out) throws IOException {
out.writeDouble(data[index]);
}
@Override
int read(int from, int n, ArrayDataInput in) throws IOException {
return in.read(data, from, n);
}
@Override
void write(int from, int n, ArrayDataOutput out) throws IOException {
out.write(data, from, n);
}
@Override
double[] getArrayElement(int i) {
return new double[] {data[i]};
}
@Override
void setArrayElement(int i, Object o) {
data[i] = ((double[]) o)[0];
}
}
/**
* A column for data consisting of Objects (primitive arrays).
*
* @author Attila Kovacs
*/
private static class Generic extends Column