View Javadoc
1   package org.djutils.data;
2   
3   import java.util.ArrayList;
4   import java.util.Collection;
5   import java.util.Collections;
6   import java.util.Iterator;
7   import java.util.LinkedHashMap;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Spliterator;
11  import java.util.stream.Stream;
12  
13  import org.djunits.Throw;
14  import org.djutils.immutablecollections.Immutable;
15  import org.djutils.immutablecollections.ImmutableArrayList;
16  import org.djutils.immutablecollections.ImmutableLinkedHashMap;
17  import org.djutils.immutablecollections.ImmutableList;
18  import org.djutils.immutablecollections.ImmutableMap;
19  import org.djutils.primitives.Primitive;
20  
21  /**
22   * List implementation of {@code Table}.
23   * <p>
24   * Copyright (c) 2020-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
25   * BSD-style license. See <a href="https://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
26   * </p>
27   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
28   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
29   * @author <a href="https://www.transport.citg.tudelft.nl">Wouter Schakel</a>
30   */
31  public class ListDataTable extends AbstractDataTable
32  {
33  
34      /** Records. */
35      private List<DataRecord> records = Collections.synchronizedList(new ArrayList<>());
36  
37      /** Column numbers. */
38      private Map<DataColumn<?>, Integer> columnNumbers = new LinkedHashMap<>();
39  
40      /** Id numbers. */
41      private Map<String, Integer> idNumbers = new LinkedHashMap<>();
42  
43      /**
44       * Constructor with a regular collection.
45       * @param id String; id
46       * @param description String; description
47       * @param columns Collection&lt;Column&lt;?&gt;&gt;; columns
48       */
49      public ListDataTable(final String id, final String description, final Collection<DataColumn<?>> columns)
50      {
51          this(id, description, new ImmutableArrayList<DataColumn<?>>(columns));
52      }
53  
54      /**
55       * Constructor with an immutable list.
56       * @param id String; id
57       * @param description String; description
58       * @param columns ImmutableList&lt;Column&lt;?&gt;&gt;; columns
59       */
60      public ListDataTable(final String id, final String description, final ImmutableList<DataColumn<?>> columns)
61      {
62          super(id, description, columns);
63          for (int index = 0; index < getColumns().size(); index++)
64          {
65              DataColumn<?> column = getColumns().get(index);
66              this.columnNumbers.put(column, index);
67              this.idNumbers.put(column.getId(), index);
68          }
69          Throw.when(getNumberOfColumns() != this.idNumbers.size(), IllegalArgumentException.class,
70                  "Duplicate column ids are not allowed.");
71      }
72  
73      /**
74       * {@inheritDoc} <br>
75       * <br>
76       * It is imperative that the user manually synchronize on the returned list when traversing it via {@link Iterator},
77       * {@link Spliterator} or {@link Stream} when there is a risk of adding records while traversing the iterator:
78       * 
79       * <pre>
80       *  List list = Collections.synchronizedList(new ArrayList());
81       *      ...
82       *  synchronized (list) 
83       *  {
84       *      Iterator i = list.iterator(); // Must be in synchronized block
85       *      while (i.hasNext())
86       *          foo(i.next());
87       *  }
88       * </pre>
89       * 
90       * Failure to follow this advice may result in non-deterministic behavior.<br>
91       * <br>
92       */
93      @Override
94      public Iterator<DataRecord> iterator()
95      {
96          return this.records.iterator();
97      }
98  
99      /** {@inheritDoc} */
100     @Override
101     public boolean isEmpty()
102     {
103         return this.records.isEmpty();
104     }
105 
106     /**
107      * Adds a record to the table, based on a map with columns and values.
108      * @param data Map&lt;String, Object&gt;; data with values given per column
109      * @throws IllegalArgumentException when the size or data types in the data map do not comply to the columns
110      * @throws NullPointerException when data is null
111      */
112     public void addRecordByColumns(final Map<DataColumn<?>, Object> data)
113     {
114         Throw.whenNull(data, "Data may not be null.");
115         addRecordByColumns(new ImmutableLinkedHashMap<>(data, Immutable.WRAP));
116     }
117 
118     /**
119      * Adds a record to the table, based on an immutable map with columns and values.
120      * @param data Map&lt;String, Object&gt;; data with values given per column
121      * @throws IllegalArgumentException when the size or data types in the data map do not comply to the columns
122      * @throws NullPointerException when data is null
123      */
124     public void addRecordByColumns(final ImmutableMap<DataColumn<?>, Object> data)
125     {
126         Throw.whenNull(data, "Data may not be null.");
127         Throw.when(data.size() != getNumberOfColumns(), IllegalArgumentException.class,
128                 "Number of data columns doesn't match number of table columns.");
129         Object[] dataObjects = new Object[getNumberOfColumns()];
130         for (int index = 0; index < getColumns().size(); index++)
131         {
132             DataColumn<?> column = getColumns().get(index);
133             Throw.when(!data.containsKey(column), IllegalArgumentException.class, "Missing data for column %s", column.getId());
134             Object value = data.get(column);
135             Throw.when(!Primitive.isPrimitiveAssignableFrom(column.getValueType(), value.getClass()),
136                     IllegalArgumentException.class, "Data value for column %s is not of type %s, but of type %s.",
137                     column.getId(), column.getValueType(), value.getClass());
138             dataObjects[index] = value;
139         }
140         this.records.add(new ListRecord(dataObjects));
141     }
142 
143     /**
144      * Adds a record to the table, based on a map with column ids and values.
145      * @param data Map&lt;String, Object&gt;; immutable data with values given per column id
146      * @throws IllegalArgumentException when the size or data types in the data map do not comply to the columns
147      * @throws NullPointerException when data is null
148      */
149     public void addRecordByColumnIds(final Map<String, Object> data)
150     {
151         Throw.whenNull(data, "Data may not be null.");
152         addRecordByColumnIds(new ImmutableLinkedHashMap<>(data, Immutable.WRAP));
153     }
154 
155     /**
156      * Adds a record to the table, based on an immutable map with column ids and values.
157      * @param data Map&lt;String, Object&gt;; data with values given per column id
158      * @throws IllegalArgumentException when the size or data types in the data map do not comply to the columns
159      * @throws NullPointerException when data is null
160      */
161     public void addRecordByColumnIds(final ImmutableMap<String, Object> data)
162     {
163         Throw.whenNull(data, "Data may not be null.");
164         Throw.when(data.size() != getNumberOfColumns(), IllegalArgumentException.class,
165                 "Number of data columns doesn't match number of table columns.");
166         Object[] dataObjects = new Object[getNumberOfColumns()];
167         for (int index = 0; index < getColumns().size(); index++)
168         {
169             DataColumn<?> column = getColumns().get(index);
170             Throw.when(!data.containsKey(column.getId()), IllegalArgumentException.class, "Missing data for column %s",
171                     column.getId());
172             Object value = data.get(column.getId());
173             Class<?> dataClass = value.getClass();
174             Throw.when(!Primitive.isPrimitiveAssignableFrom(column.getValueType(), dataClass), IllegalArgumentException.class,
175                     "Data value for column %s is not of type %s, but of type %s.", column.getId(), column.getValueType(),
176                     dataClass);
177             dataObjects[index] = value;
178         }
179         this.records.add(new ListRecord(dataObjects));
180     }
181 
182     /**
183      * 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
184      * the columns.
185      * @param data Object[]; record data
186      * @throws IllegalArgumentException when the size, order or data types in the {@code Object[]} do not comply to the columns
187      * @throws NullPointerException when data is null
188      */
189     public void addRecord(final Object[] data)
190     {
191         Throw.whenNull(data, "Data may not be null.");
192         Throw.when(data.length != getNumberOfColumns(), IllegalArgumentException.class,
193                 "Number of data columns doesn't match number of table columns.");
194         Object[] dataObjects = new Object[getNumberOfColumns()];
195         for (int index = 0; index < getColumns().size(); index++)
196         {
197             DataColumn<?> column = getColumns().get(index);
198             Class<?> dataClass = data[index].getClass();
199             Throw.when(!Primitive.isPrimitiveAssignableFrom(column.getValueType(), dataClass), IllegalArgumentException.class,
200                     "Data value for column %s is not of type %s, but of type %s.", column.getId(), column.getValueType(),
201                     dataClass);
202             dataObjects[index] = data[index];
203         }
204         this.records.add(new ListRecord(dataObjects));
205     }
206 
207     /** {@inheritDoc} */
208     @Override
209     public String toString()
210     {
211         StringBuilder result = new StringBuilder();
212         result.append("ListDataTable [getId()=");
213         result.append(this.getId());
214         result.append(", getDescription()=");
215         result.append(this.getDescription());
216         result.append("]\nColumns:\n");
217         for (DataColumn<?> column : getColumns())
218         {
219             result.append("  ");
220             result.append(column.toString());
221             result.append("\n");
222         }
223         return result.toString();
224     }
225 
226     /** Record in a {@code ListTable}. */
227     public class ListRecord implements DataRecord
228     {
229 
230         /** Values. */
231         private final Object[] values;
232 
233         /**
234          * Constructor.
235          * @param values Object[]; values
236          */
237         public ListRecord(final Object[] values)
238         {
239             this.values = values;
240         }
241 
242         /** {@inheritDoc} */
243         @SuppressWarnings({"unchecked", "synthetic-access"})
244         @Override
245         public <T> T getValue(final DataColumn<T> column)
246         {
247             return (T) this.values[ListDataTable.this.columnNumbers.get(column)];
248         }
249 
250         /** {@inheritDoc} */
251         @SuppressWarnings("synthetic-access")
252         @Override
253         public Object getValue(final String id)
254         {
255             return this.values[ListDataTable.this.idNumbers.get(id)];
256         }
257 
258         /** {@inheritDoc} */
259         @Override
260         public Object[] getValues()
261         {
262             return this.values;
263         }
264 
265         /** {@inheritDoc} */
266         @Override
267         public String toString()
268         {
269             StringBuilder result = new StringBuilder();
270             result.append("ListDataTable.ListRecord\n");
271             for (DataColumn<?> column : ListDataTable.this.getColumns())
272             {
273                 result.append("  ");
274                 result.append(column.getId());
275                 result.append(" = ");
276                 result.append(getValue(column.getId()));
277                 result.append("\n");
278             }
279             return result.toString();
280         }
281 
282     }
283 
284 }