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