blob: b1710b1969087a6420dfa75780a56fd1f2621f8d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.freemarker.generator.base.table;
import org.apache.freemarker.generator.base.util.ListUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
/**
* Simple table model filled from maps or rows representing tabular data.
*/
public class Table {
/** List of column names */
private final List<String> columnNames;
/** Column types derived from tabular data */
private final List<Class<?>> columnTypes;
/** Table data as rows */
private final List<List<Object>> values;
/** Map column names to column index */
private final Map<String, Integer> columnMap;
private Table() {
this.columnNames = emptyList();
this.columnTypes = emptyList();
this.values = emptyList();
this.columnMap = new HashMap<>();
}
private Table(Collection<String> columnNames, Collection<Class<?>> columnTypes, List<List<Object>> columnValuesList) {
this.columnNames = new ArrayList<>(requireNonNull(columnNames));
this.columnTypes = new ArrayList<>(requireNonNull(columnTypes));
this.values = ListUtils.transpose(requireNonNull(columnValuesList));
this.columnMap = columnMap(this.columnNames);
}
public List<String> getColumnNames() {
return columnNames;
}
public List<Class<?>> getColumnTypes() {
return columnTypes;
}
public List<List<Object>> getValues() {
return values;
}
public int getNrOfColumns() {
return columnNames.isEmpty() ? values.isEmpty() ? 0 : values.get(0).size() : columnNames.size();
}
public int size() {
return values.size();
}
public boolean isEmpty() {
return values.isEmpty();
}
public List<Object> getRow(int row) {
return values.get(row);
}
public Object get(int row, int column) {
return values.get(row).get(column);
}
public Object get(int row, String column) {
return values.get(row).get(columnMap.get(column));
}
public boolean hasColumnHeaderRow() {
return !columnNames.isEmpty();
}
/**
* Create a table from a list of maps. Non-tabular data is supported,
* i.e. not all maps contains all possible keys.
*
* @param maps list of maps
* @return table
*/
public static Table fromMaps(Collection<Map<String, Object>> maps) {
if (maps == null || maps.isEmpty()) {
return new Table();
}
final List<String> columnNames = columnNames(maps);
final List<List<Object>> columnValuesList = columnValuesList(maps, columnNames);
final List<Class<?>> columnTypes = columnTypes(columnValuesList);
return new Table(
columnNames,
columnTypes,
columnValuesList);
}
/**
* Create a table from a list of rows representing tabular data.
*
* @param rows row values
* @return table
*/
public static Table fromRows(List<List<Object>> rows) {
requireNonNull(rows, "rows is null");
final List<List<Object>> columnValuesList = ListUtils.transpose(rows);
final List<Class<?>> columnTypes = columnTypes(columnValuesList);
return new Table(
new ArrayList<>(),
columnTypes,
columnValuesList);
}
/**
* Create a table from a list of rows representing tabular data
* where the first row may consist of column headers.
*
* @param rows row values
* @param withFirstRowAsColumnNames column names as first row?
* @return table
*/
public static Table fromRows(List<List<Object>> rows, boolean withFirstRowAsColumnNames) {
if (ListUtils.isNullOrEmpty(rows) && withFirstRowAsColumnNames) {
throw new IllegalArgumentException("Header columns expected but list is empty");
}
if (withFirstRowAsColumnNames) {
final List<String> columnNames = columnNames(rows.get(0));
final List<List<Object>> table = rows.subList(1, rows.size());
return fromRows(columnNames, table);
} else {
return fromRows(rows);
}
}
/**
* Create a table from column names and row values.
*
* @param columnNames list of column names
* @param rows row values as rows
* @return table
*/
public static Table fromRows(Collection<String> columnNames, List<List<Object>> rows) {
requireNonNull(columnNames, "columnNames is null");
requireNonNull(rows, "rows is null");
final List<List<Object>> columnValuesList = ListUtils.transpose(rows);
final List<Class<?>> columnTypes = columnTypes(columnValuesList);
return new Table(
new ArrayList<>(columnNames),
columnTypes,
columnValuesList);
}
/**
* Determine column names based on all available keys of the maps.
*
* @param maps list of maps
* @return column names
*/
private static List<String> columnNames(Collection<Map<String, Object>> maps) {
return maps.stream()
.map(Map::keySet)
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toList());
}
/**
* Determine column names based on a single list.
*
* @param list list of column names
* @return column names
*/
private static List<String> columnNames(List<Object> list) {
return list.stream()
.map(Object::toString)
.collect(Collectors.toList());
}
/**
* Determine all column values.
*
* @param maps list of maps
* @param columnNames column names
* @return list of column values
*/
private static List<List<Object>> columnValuesList(Collection<Map<String, Object>> maps, List<String> columnNames) {
return columnNames.stream()
.map(columnName -> columnValues(maps, columnName))
.collect(Collectors.toList());
}
/**
* Determine the values of a single column.
*
* @param maps list of maps
* @param columnName column name
* @return values of the given column
*/
private static List<Object> columnValues(Collection<Map<String, Object>> maps, String columnName) {
return maps.stream()
.map(map -> map.getOrDefault(columnName, null))
.collect(Collectors.toList());
}
/**
* Determine the column types based on the first non-null values.
*
* @param columnValuesList list of column values
* @return classes of the first non-null value
*/
private static List<Class<?>> columnTypes(List<List<Object>> columnValuesList) {
return columnValuesList.stream()
.map(Table::columnType)
.collect(Collectors.toList());
}
/**
* Determine the column type based on the first non-null value.
*
* @param columnValues column values
* @return class of the first non-null value
*/
private static Class<?> columnType(List<Object> columnValues) {
return ListUtils.coalesce(columnValues).getClass();
}
private static Map<String, Integer> columnMap(List<String> columnNames) {
final Map<String, Integer> result = new HashMap<>();
for (int i = 0; i < columnNames.size(); i++) {
result.put(columnNames.get(i), i);
}
return result;
}
}