act.util.DataTable Maven / Gradle / Ivy
package act.util;
/*-
* #%L
* ACT Framework
* %%
* Copyright (C) 2014 - 2019 ActFramework
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import act.annotations.Label;
import org.osgl.$;
import org.osgl.util.*;
import java.lang.reflect.*;
import java.util.*;
/**
* A `DataTable` contains a list of heading (column labels) plus
* a list of data rows.
*
* The sequence of data fields in each data row matches the corresponding
* sequence of field labels in heading list.
*
* **Note**
* 1. `DataTable` does data processing in memory, it is not suitable
* for huge amount of data.
* 2. `DataTable` does not suit big sparse matrix unless heading is
* provided with {@link PropertySpec} or the row data type is a concrete
* POJO. In case sparse matrix row is supplied with {@link Map} or
* {@link org.osgl.util.AdaptiveMap}, `DataTable` will try to probe at
* most 10 first rows to find out the heading fields(keys). Thus if
* the first 10 rows has missing columns, they will not be able to output
*/
public class DataTable implements Iterable {
public static final Keyword HTML_TABLE = Keyword.of("html-table");
public static final String KEY_FIELD = "_field";
/**
* A pseudo column key for the entire data object
* - used with data table of {@link org.osgl.Lang#isSimpleType(Class) simple types}.
*/
public static final String KEY_THIS = "_this_";
private List colKeys;
private Iterable rows;
private int rowCount;
private boolean isMap;
private boolean isAdaptiveMap;
private boolean isTranspose;
private Object firstRow;
private Set rightAlignCols;
// key: col key; val: label
private C.Map labelLookup;
// key: label, val: col key
private C.Map reverseLabelLookup;
// the transpose table of this table
private transient volatile DataTable transpose;
// used to build transpose table
private DataTable() {
}
/**
* Construct a `DataTable` with data.
*
* This constructor will call `{@link ThreadLocal#get()}` on
* {@link PropertySpec#currentSpec} to get the current
* PropertySpec meta info.
*
* @param data the data used to populate the data table
*/
public DataTable(Object data) {
init(data, PropertySpec.currentSpec.get());
}
/**
* Construct a `DataTable` with data and {@link PropertySpec.MetaInfo column spec}.
*
* @param data the data used to populate the data table
* @param colSpec the {@link PropertySpec.MetaInfo} specifies column headings
*/
public DataTable(Object data, PropertySpec.MetaInfo colSpec) {
init(data, colSpec);
}
/**
* Return number of rows in the data table.
*
* If {@link #rows} is an iterable and cannot be cast to a collection, then
* it returns `-1`, meaning row number cannot be count.
*
* @return the number of rows in the table
*/
public int rowCount() {
return rowCount;
}
/**
* Return number of columns in the data table.
*
* @return the number of columns.
*/
public int colCount() {
return colKeys.size();
}
/**
* Return {@link Iterator} of {@link #rows}.
*
* @return the row iterator.
*/
@Override
public Iterator iterator() {
return rows.iterator();
}
/**
* Get the heading row.
*
* @return the heading row
*/
public List heading() {
if (isTranspose) {
return colKeys;
}
List heading = new ArrayList<>();
List colKeys = new ArrayList(this.colKeys);
if (colKeys.remove("id")) {
String idLabel = labelLookup.get("id");
if (null != idLabel) {
heading.add(idLabel);
} else {
heading.add("id");
}
}
for (String colKey : colKeys) {
String label = labelLookup.get(colKey);
heading.add(null != label ? label : colKey);
}
return heading;
}
/**
* Get column value on a row by col index.
*
* @param row the row object
* @param colIndex the column index
* @return the value of the column on the row
*/
public Object val(Object row, int colIndex) {
String colKey = colKeys.get(colIndex);
return $.getProperty(row, colKey);
}
/**
* Get column value on a row by label.
*
* @param row the row object
* @param label the column label
* @return the value of the column on the row
*/
public Object val(Object row, String label) {
if (KEY_THIS == label) {
return row;
}
String colKey = label;
if (!isTranspose) {
colKey = reverseLabelLookup.get(label);
if (null == colKey) {
colKey = label;
}
}
return $.getProperty(row, colKey);
}
/**
* Get a transpose table of this table.
*
* @return a transpose table of this table.
*/
public DataTable transpose() {
if (null == transpose) {
synchronized (this) {
if (null == transpose) {
transpose = buildTranspose();
}
}
}
return transpose;
}
/**
* Check if a label is right aligned column.
* @param label the label to check
* @return `true` if the column of the label should be right aligned.
*/
public boolean isRightAligned(String label) {
if (isTranspose) {
return false;
}
String key = reverseLabelLookup.get(label);
if (null == key) {
key = label;
}
return rightAlignCols.contains(key);
}
/**
* Reports if this data table is a transposed table.
*
* @return `true` if this table is transposed, or `false` otherwise.
*/
public boolean isTransposed() {
return isTranspose;
}
private DataTable buildTranspose() {
E.illegalArgumentIf(rowCount < 0, "transpose table not applied to table does not have row count.");
DataTable transpose = new DataTable();
transpose = isTranspose() ? buildReverseTranspose(transpose) : buildTranspose(transpose);
transpose.labelLookup = this.labelLookup;
transpose.reverseLabelLookup = this.reverseLabelLookup;
transpose.isTranspose = !this.isTranspose;
return transpose;
}
private DataTable buildReverseTranspose(DataTable target) {
target.colKeys = buildReverseTransposeColKeys();
target.rowCount = this.colKeys.size() - 1;
target.rows = buildReverseTransposeRows(target.rowCount);
return target;
}
private DataTable buildTranspose(DataTable target) {
target.colKeys = buildTransposeColKeys();
target.rowCount = this.colKeys.size();
target.rows = buildTransposeRows();
return target;
}
private List buildReverseTransposeColKeys() {
List colKeys = new ArrayList<>(this.rowCount);
for (Object row : rows) {
colKeys.add(S.string($.getProperty(row, KEY_FIELD)));
}
return colKeys;
}
private List buildTransposeColKeys() {
List colKeys = new ArrayList<>(this.rowCount + 1);
colKeys.add(KEY_FIELD);
for (int i = 0; i < this.rowCount; ++i) {
colKeys.add("_r" + i);
}
return colKeys;
}
private Map buildTransposeRow(String colKey) {
Map row = new HashMap<>(this.rowCount + 1);
String label = labelLookup.get(colKey);
if (null == label) {
label = colKey;
}
row.put(KEY_FIELD, label);
int rowId = 0;
for (Object data : this.rows) {
row.put("_r" + rowId++, $.getProperty(data, colKey));
}
return row;
}
private Iterable buildTransposeRows() {
List