View Javadoc
1   package guru.mikelue.jdut.datagrain;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.HashMap;
6   import java.util.List;
7   import java.util.Map;
8   import java.util.Optional;
9   import java.util.function.Consumer;
10  import java.util.function.Supplier;
11  import java.util.stream.Stream;
12  
13  import org.apache.commons.lang3.StringUtils;
14  import org.apache.commons.lang3.Validate;
15  import org.apache.commons.lang3.builder.EqualsBuilder;
16  import org.apache.commons.lang3.builder.HashCodeBuilder;
17  
18  /**
19   * Represents the data of a row.<br>
20   *
21   * @see DataRow.Builder for detail information to set-up a row of data
22   */
23  public class DataRow {
24  	private SchemaTable tableSchema;
25  	private Map<String, DataField<?>> data;
26  	private Map<String, Object> attributes = new HashMap<>(8);
27  	private boolean validated = false;
28  
29  	/**
30  	 * Used with {@link DataRow#build(Consumer) DataRow.build(Consumer&lt;DataRow.Builder&gt;)}.<br>
31   	 *
32   	 * <h3><em style="color:red">Infinite recursion</em> while using {@link Supplier} for value of field</h3>
33   	 * In following code snippet, the builder will set-up <em style="color:red">a field which cause infinite recursion while getting data</em>:
34   	 *
35   	 * <pre><code class="java">
36   	 * builder -&gt; builder.fieldOfValueSupplier(
37   	 *     "ct_1", () -&gt; 30 + builder.getDataSupplier("ct_1").get().get()
38   	 * )
39   	 * </code></pre>
40   	 *
41   	 * Since the lazy evaluation of lambda expression, the new lambda expression of <em>"ct_1"</em> would be
42   	 * a self-reference to builder.<br>
43   	 * Instead, you should keep "<b>the old instance of lambda expression</b>":
44   	 * <pre><code class="java">
45   	 * builder -&gt; {
46   	 *     final Supplier&lt;Integer&gt; oldSupplier = builder.getDataSupplier("ct_1").get();
47   	 *     builder.fieldOfValueSupplier("ct_1", () -&gt; 30 + oldSupplier.get());
48   	 * }
49   	 * </code></pre>
50  	 */
51  	public class Builder {
52  		private DataField.Factory fieldFactory = null;
53  
54  		private Builder() {}
55  
56  		/**
57  		 * Sets the table schema.
58  		 *
59  		 * @param newTableSchema The table schema
60  		 *
61  		 * @return cascading self
62  		 */
63  		public Builder tableSchema(SchemaTable newTableSchema)
64  		{
65  			Validate.notNull(newTableSchema, "Need viable table schema");
66  
67  			fieldFactory = new DataField.Factory(newTableSchema);
68  
69  			/**
70  			 * Change the table schema of column
71  			 */
72  			Map<String, DataField<?>> dataWithNewTableSchema = new HashMap<>(data.size());
73  
74  			data.entrySet().forEach(
75  				fieldEntry -> dataWithNewTableSchema.put(
76  					newTableSchema.treatIdentifier(
77  						fieldEntry.getKey()
78  					),
79  					fieldFactory.clone(fieldEntry.getValue())
80  				)
81  			);
82  
83  			tableSchema = newTableSchema;
84  			return data(dataWithNewTableSchema);
85  			// :~)
86  		}
87  		/**
88  		 * Sets data of row.
89  		 *
90  		 * @param newData the data of row
91  		 *
92  		 * @return cascading self
93  		 */
94  		public Builder data(Map<String, DataField<?>> newData)
95  		{
96  			Validate.notNull(newData, "Need viable data");
97  
98  			data = newData;
99  			return this;
100 		}
101 
102 		/**
103 		 * Sets value of a field.
104 		 *
105 		 * @param <T> The type of value
106 		 * @param columnName The name of column
107 		 * @param value The value instance
108 		 *
109 		 * @return cascading self
110 		 */
111 		public <T> Builder fieldOfValue(String columnName, T value)
112 		{
113 			return field(
114 				fieldFactory.composeData(columnName, value)
115 			);
116 		}
117 		/**
118 		 * Sets value supplier of a field.
119 		 *
120 		 * @param <T> The type of value
121 		 * @param columnName The name of column
122 		 * @param valueSupplier The value instance
123 		 *
124 		 * @return cascading self
125 		 */
126 		public <T> Builder fieldOfValueSupplier(String columnName, Supplier<T> valueSupplier)
127 		{
128 			return field(
129 				fieldFactory.composeDataSupplier(columnName, valueSupplier)
130 			);
131 		}
132 
133 		/**
134 		 * Whether the data row is validated or not.
135 		 *
136 		 * @return true if validated
137 		 */
138 		public boolean getValidated()
139 		{
140 			return validated;
141 		}
142 
143 		/**
144 		 * Validates this row.
145 		 *
146 		 * @throws DataRowException if there is error in data or schema
147 		 */
148 		public void validate() throws DataRowException
149 		{
150 			DataRow.this.validate();
151 		}
152 
153 		/**
154 		 * Gets name of table.
155 		 *
156 		 * @return the name of table
157 		 *
158 		 * @see #getTable
159 		 */
160 		public String getTableName()
161 		{
162 			return tableSchema.getName();
163 		}
164 
165 		/**
166 		 * Gets definition of table.
167 		 *
168 		 * @return definition of table
169 		 *
170 		 * @see #getTableName
171 		 */
172 		public SchemaTable getTable()
173 		{
174 			return tableSchema;
175 		}
176 
177 		/**
178 		 * Gets value of data.
179 		 *
180 		 * @param <T> The type of data
181 		 * @param columnName The name of column
182 		 *
183 		 * @return empty option if the column is not existing or the value is empty
184 		 */
185 		public <T> Optional<T> getData(String columnName)
186 		{
187 			return this.<T>getDataField(columnName).map(
188 				dataField -> dataField.getData()
189 			);
190 		}
191 
192 		/**
193 		 * Gets supplier of data.<br>
194 		 *
195 		 * <p style="color:red">Note: be careful of lazy building, which may cause infinite recursion</p>
196 		 *
197 		 * <b style="color:red">Following code will cause infinite recursion</b>
198 		 * <pre><code class="java">
199 		 * Supplier&lt;Integer&gt; wrappedSupplier = () -&gt; 20 + builder.&lt;Integer&gt;getDataSupplier("ct_1").get().get();
200 		 * builder.fieldOfValueSupplier("col_1", wrappedSupplier);
201 		 * </code></pre>
202 		 *
203 		 * <b style="color:green">Instead, you should:</b>
204 		 * <pre><code class="java">
205 		 * // Keep the reference of supplier in current lambda of builder
206 		 * Supplier&lt;Integer&gt; sourceSupplier = builder.&lt;Integer&gt;getDataSupplier("ct_1").get();
207 		 * Supplier&lt;Integer&gt; wrappedSupplier = () -&gt; 20 + sourceSupplier.get();
208 		 *
209 		 * builder.fieldOfValueSupplier("col_1", wrappedSupplier);
210 		 * </code></pre>
211 		 *
212 		 * @param <T> The type of data for the supplier
213 		 * @param columnName The name of column
214 		 *
215 		 * @return empty option if the column is not existing or the value supplier is empty
216 		 */
217 		@SuppressWarnings("unchecked")
218 		public <T> Optional<Supplier<T>> getDataSupplier(String columnName)
219 		{
220 			return this.<T>getDataField(columnName).map(
221 				dataField -> (Supplier<T>)dataField.getDataSupplier().orElse(null)
222 			);
223 		}
224 
225 		/**
226 		 * Gets field of data.
227 		 *
228 		 * @param <T> The data type of field
229 		 * @param columnName The name of column
230 		 *
231 		 * @return empty option if the column is not existing
232 		 */
233 		@SuppressWarnings("unchecked")
234 		public <T> Optional<DataField<T>> getDataField(String columnName)
235 		{
236 			columnName = StringUtils.trimToNull(columnName);
237 			Validate.notNull(columnName, "Need viable name of column");
238 
239 			return Optional.ofNullable((DataField<T>)data.get(columnName));
240 		}
241 
242 		/**
243 		 * Gets the fields as {@link Stream}.
244 		 *
245 		 * @return The data fields as stream
246 		 */
247 		public Stream<DataField<?>> getStreamOfFields()
248 		{
249 			return data.values().stream();
250 		}
251 
252 		private <T> Builder field(DataField<T> field)
253 		{
254 			data.put(
255 				tableSchema.treatIdentifier(
256 					field.getColumnName()
257 				),
258 				field
259 			);
260 			return this;
261 		}
262 	}
263 
264 	private DataRow() {}
265 
266 	/**
267 	 * Builds row by {@link Consumer} of {@link Builder}.
268 	 *
269 	 * @param builderConsumer The consumer to build row
270 	 *
271 	 * @return the built data of row
272 	 */
273 	public static DataRow build(Consumer<Builder> builderConsumer)
274 	{
275 		DataRow newRow = new DataRow();
276 		newRow.data = new HashMap<>(CollectionUsage.HASH_SPACE_OF_COLUMNS);
277 		Builder newBuilder = newRow.new Builder();
278 
279 		builderConsumer.accept(newBuilder);
280 
281 		return newRow.clone();
282 	}
283 
284 	/**
285 	 * Builds row by {@link Consumer} of {@link Builder} and existing row.
286 	 *
287 	 * @param builderConsumer The consumer to build row
288 	 * @param existingRow The row to be modified
289 	 *
290 	 * @return the built data of row
291 	 */
292 	public static DataRown/DataRow.html#DataRow">DataRow build(Consumer<Builder> builderConsumer, DataRow existingRow)
293 	{
294 		DataRow newRow = existingRow.modifiableClone();
295 		Builder newBuilder = newRow.new Builder();
296 		newBuilder.tableSchema(existingRow.getTable());
297 
298 		builderConsumer.accept(newBuilder);
299 
300 		return newRow.clone();
301 	}
302 
303 	/**
304 	 * Gets the table schema.
305 	 *
306 	 * @return table schema
307 	 */
308 	public SchemaTable getTable()
309 	{
310 		return tableSchema;
311 	}
312 
313 	/**
314 	 * Gets name of columns.
315 	 *
316 	 * @return The name of columns
317 	 */
318 	public List<String> getColumns()
319 	{
320 		return new ArrayList<>(data.keySet());
321 	}
322 
323 	/**
324 	 * Gets the field by column name.
325 	 *
326 	 * @param <T> The type of field
327 	 * @param columnName The name of column
328 	 *
329 	 * @return The field with the column name
330 	 *
331 	 * @throws IllegalArgumentException If the is no such field found with the column name
332 	 *
333 	 * @see #getData
334 	 */
335 	@SuppressWarnings("unchecked")
336 	public <T> DataField<T> getDataField(String columnName)
337 	{
338 		DataField<T> dataField = (DataField<T>)data.get(
339 			tableSchema.treatIdentifier(columnName)
340 		);
341 		if (dataField == null) {
342 			throw new IllegalArgumentException(String.format("Cannot find column: \"%s\"", columnName));
343 		}
344 
345 		return dataField;
346 	}
347 
348 	/**
349 	 * Gets the data by column name.
350 	 *
351 	 * @param <T> The type of data
352 	 * @param columnName The name of column
353 	 *
354 	 * @return The field with the column name
355 	 *
356 	 * @throws IllegalArgumentException If the is no such field found with the column name
357 	 *
358 	 * @see #getDataField
359 	 */
360 	public <T> T getData(String columnName)
361 	{
362 		return this.<T>getDataField(columnName).getData();
363 	}
364 
365 	/**
366 	 * Validates the data of row with schema information from {@link #getTable}.
367 	 *
368 	 * @throws DataRowException The exception if the validation is failed
369 	 *
370 	 * @see #isValidated
371 	 */
372 	public void validate() throws DataRowException
373 	{
374 		if (validated) {
375 			return;
376 		}
377 
378 		/**
379 		 * Checks defined data has corresponding definition in database
380 		 */
381 		for (DataField<?> dataField: data.values()) {
382 			if (!tableSchema.hasColumn(dataField.getColumnName())) {
383 				throw new MissedColumnException(tableSchema, dataField.getColumn());
384 			}
385 		}
386 		// :~)
387 
388 		validated = true;
389 	}
390 
391 	/**
392 	 * Whether or not this row is validated with schema of table.
393 	 *
394 	 * @return true if validated
395 	 *
396 	 * @see #validate
397 	 */
398 	public boolean isValidated()
399 	{
400 		return validated;
401 	}
402 
403 	/**
404 	 * Gets attribute of this row.
405 	 *
406 	 * @param <T> The expected type of result
407 	 * @param name The name of attribute
408 	 *
409 	 * @return null if attribute is not existing
410 	 *
411 	 * @see #hasAttribute
412 	 * @see #putAttribute
413 	 */
414 	@SuppressWarnings("unchecked")
415 	public <T> T getAttribute(String name)
416 	{
417 		return (T)attributes.get(name);
418 	}
419 	/**
420 	 * Checks whether or not a attribute is existing.
421 	 *
422 	 * @param name The name of attribute
423 	 *
424 	 * @return true if the attribute is existing
425 	 *
426 	 * @see #getAttribute
427 	 * @see #putAttribute
428 	 */
429 	public boolean hasAttribute(String name)
430 	{
431 		return attributes.containsKey(name);
432 	}
433 	/**
434 	 * Puts a attribute.
435 	 *
436 	 * @param name The name of attribute
437 	 * @param value the value of attribute
438 	 *
439 	 * @see #getAttribute
440 	 * @see #hasAttribute
441 	 */
442 	public void putAttribute(String name, Object value)
443 	{
444 		attributes.put(name, value);
445 	}
446 
447 	@Override
448 	protected DataRow clone()
449 	{
450 		DataRow newRow = new DataRow();
451 		newRow.tableSchema = this.tableSchema;
452 		newRow.data = Collections.unmodifiableMap(this.data);
453 		newRow.validated = this.validated;
454 		newRow.attributes = new HashMap<>(this.attributes);
455 
456 		return newRow;
457 	}
458 
459 	private DataRow modifiableClone()
460 	{
461 		DataRow newRow = new DataRow();
462 		newRow.tableSchema = this.tableSchema;
463 		newRow.data = new HashMap<>(this.data);
464 		newRow.validated = this.validated;
465 		newRow.attributes = new HashMap<>(this.attributes);
466 
467 		return newRow;
468 	}
469 
470 	/**
471 	 * Hashes the table schema and data.
472 	 */
473 	@Override
474 	public int hashCode()
475 	{
476 		return new HashCodeBuilder(82782651, 925373735)
477 			.append(tableSchema)
478 			.append(data)
479 		.toHashCode();
480 	}
481 
482 	/**
483 	 * Compares the table schema and data.
484 	 */
485 	@Override
486 	public boolean equals(Object obj)
487 	{
488 		if (obj == null) { return false; }
489 		if (obj == this) { return true; }
490 		if (obj.getClass() != getClass()) {
491 			return false;
492 		}
493 
494 		DataRow rhs = (DataRow)obj;
495 		return new EqualsBuilder()
496 			.append(this.tableSchema, rhs.tableSchema)
497 			.append(this.data, rhs.data)
498 			.isEquals();
499 	}
500 }