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