package org.djutils.data; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Spliterator; import java.util.stream.Stream; import org.djunits.Throw; import org.djutils.immutablecollections.Immutable; import org.djutils.immutablecollections.ImmutableArrayList; import org.djutils.immutablecollections.ImmutableLinkedHashMap; import org.djutils.immutablecollections.ImmutableList; import org.djutils.immutablecollections.ImmutableMap; import org.djutils.primitives.Primitive; /** * List implementation of {@code Table}. *

* Copyright (c) 2020-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See DJUTILS License. *

* @author Alexander Verbraeck * @author Peter Knoppers */ public class ListDataTable extends AbstractDataTable { /** Records. */ private List records = Collections.synchronizedList(new ArrayList<>()); /** Column numbers. */ private Map, Integer> columnNumbers = new LinkedHashMap<>(); /** Id numbers. */ private Map idNumbers = new LinkedHashMap<>(); /** * Constructor with a regular collection. * @param id String; id * @param description String; description * @param columns Collection<DataColumn<?>>; columns */ public ListDataTable(final String id, final String description, final Collection> columns) { this(id, description, new ImmutableArrayList>(columns)); } /** * Constructor with an immutable list. * @param id String; id * @param description String; description * @param columns ImmutableList<DataColumn<?>>; columns */ public ListDataTable(final String id, final String description, final ImmutableList> columns) { super(id, description, columns); for (int index = 0; index < getColumns().size(); index++) { DataColumn column = getColumns().get(index); this.columnNumbers.put(column, index); this.idNumbers.put(column.getId(), index); } Throw.when(getNumberOfColumns() != this.idNumbers.size(), IllegalArgumentException.class, "Duplicate column ids are not allowed."); } /** * {@inheritDoc}
*
* It is imperative that the user manually synchronize on the returned list when traversing it via {@link Iterator}, * {@link Spliterator} or {@link Stream} when there is a risk of adding records while traversing the iterator: * *
     *  List list = Collections.synchronizedList(new ArrayList());
     *      ...
     *  synchronized (list) 
     *  {
     *      Iterator i = list.iterator(); // Must be in synchronized block
     *      while (i.hasNext())
     *          foo(i.next());
     *  }
     * 
* * Failure to follow this advice may result in non-deterministic behavior.
*
*/ @Override public Iterator iterator() { return this.records.iterator(); } /** {@inheritDoc} */ @Override public boolean isEmpty() { return this.records.isEmpty(); } /** * Adds a record to the table, based on a map with columns and values. * @param data Map<DataColumn<?>, Object>; data with values given per column * @throws IllegalArgumentException when the size or data types in the data map do not comply to the columns * @throws NullPointerException when data is null */ public void addRecordByColumns(final Map, Object> data) { Throw.whenNull(data, "Data may not be null."); addRecordByColumns(new ImmutableLinkedHashMap<>(data, Immutable.WRAP)); } /** * Adds a record to the table, based on an immutable map with columns and values. * @param data ImmutableMap<DataColumn<?>, Object>; data with values given per column * @throws IllegalArgumentException when the size or data types in the data map do not comply to the columns * @throws NullPointerException when data is null */ public void addRecordByColumns(final ImmutableMap, Object> data) { Throw.whenNull(data, "Data may not be null."); Throw.when(data.size() != getNumberOfColumns(), IllegalArgumentException.class, "Number of data columns doesn't match number of table columns."); Object[] dataObjects = new Object[getNumberOfColumns()]; for (int index = 0; index < getColumns().size(); index++) { DataColumn column = getColumns().get(index); Throw.when(!data.containsKey(column), IllegalArgumentException.class, "Missing data for column %s", column.getId()); Object value = data.get(column); Throw.when(!Primitive.isPrimitiveAssignableFrom(column.getValueType(), value.getClass()), IllegalArgumentException.class, "Data value for column %s is not of type %s, but of type %s.", column.getId(), column.getValueType(), value.getClass()); dataObjects[index] = value; } this.records.add(new ListRecord(dataObjects)); } /** * Adds a record to the table, based on a map with column ids and values. * @param data Map<String, Object>; immutable data with values given per column id * @throws IllegalArgumentException when the size or data types in the data map do not comply to the columns * @throws NullPointerException when data is null */ public void addRecordByColumnIds(final Map data) { Throw.whenNull(data, "Data may not be null."); addRecordByColumnIds(new ImmutableLinkedHashMap<>(data, Immutable.WRAP)); } /** * Adds a record to the table, based on an immutable map with column ids and values. * @param data ImmutableMap<String, Object>; data with values given per column id * @throws IllegalArgumentException when the size or data types in the data map do not comply to the columns * @throws NullPointerException when data is null */ public void addRecordByColumnIds(final ImmutableMap data) { Throw.whenNull(data, "Data may not be null."); Throw.when(data.size() != getNumberOfColumns(), IllegalArgumentException.class, "Number of data columns doesn't match number of table columns."); Object[] dataObjects = new Object[getNumberOfColumns()]; for (int index = 0; index < getColumns().size(); index++) { DataColumn column = getColumns().get(index); Throw.when(!data.containsKey(column.getId()), IllegalArgumentException.class, "Missing data for column %s", column.getId()); Object value = data.get(column.getId()); Class dataClass = value.getClass(); Throw.when(!Primitive.isPrimitiveAssignableFrom(column.getValueType(), dataClass), IllegalArgumentException.class, "Data value for column %s is not of type %s, but of type %s.", column.getId(), column.getValueType(), dataClass); dataObjects[index] = value; } this.records.add(new ListRecord(dataObjects)); } /** * Adds a record to the table. The order in which the elements in the array are offered should be the same as the order of * the columns. * @param data Object[]; record data * @throws IllegalArgumentException when the size, order or data types in the {@code Object[]} do not comply to the columns * @throws NullPointerException when data is null */ public void addRecord(final Object[] data) { Throw.whenNull(data, "Data may not be null."); Throw.when(data.length != getNumberOfColumns(), IllegalArgumentException.class, "Number of data columns doesn't match number of table columns."); Object[] dataObjects = new Object[getNumberOfColumns()]; for (int index = 0; index < getColumns().size(); index++) { DataColumn column = getColumns().get(index); Class dataClass = data[index].getClass(); Throw.when(!Primitive.isPrimitiveAssignableFrom(column.getValueType(), dataClass), IllegalArgumentException.class, "Data value for column %s is not of type %s, but of type %s.", column.getId(), column.getValueType(), dataClass); dataObjects[index] = data[index]; } this.records.add(new ListRecord(dataObjects)); } /** {@inheritDoc} */ @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("ListDataTable [getId()="); result.append(this.getId()); result.append(", getDescription()="); result.append(this.getDescription()); result.append("]\nColumns:\n"); for (DataColumn column : getColumns()) { result.append(" "); result.append(column.toString()); result.append("\n"); } return result.toString(); } /** Record in a {@code ListTable}. */ public class ListRecord implements DataRecord { /** Values. */ private final Object[] values; /** * Constructor. * @param values Object[]; values */ public ListRecord(final Object[] values) { this.values = values; } /** {@inheritDoc} */ @SuppressWarnings({"unchecked", "synthetic-access"}) @Override public T getValue(final DataColumn column) { return (T) this.values[ListDataTable.this.columnNumbers.get(column)]; } /** {@inheritDoc} */ @SuppressWarnings("synthetic-access") @Override public Object getValue(final String id) { return this.values[ListDataTable.this.idNumbers.get(id)]; } /** {@inheritDoc} */ @Override public Object[] getValues() { return this.values; } /** {@inheritDoc} */ @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("ListDataTable.ListRecord\n"); for (DataColumn column : ListDataTable.this.getColumns()) { result.append(" "); result.append(column.getId()); result.append(" = "); result.append(getValue(column.getId())); result.append("\n"); } return result.toString(); } } }