View Javadoc
1   package guru.mikelue.jdut.datagrain;
2   
3   import java.sql.DatabaseMetaData;
4   import java.util.ArrayList;
5   import java.util.Collections;
6   import java.util.HashMap;
7   import java.util.List;
8   import java.util.Map;
9   import java.util.Optional;
10  import java.util.function.Consumer;
11  import java.util.stream.Collectors;
12  import java.util.stream.IntStream;
13  import java.util.stream.Stream;
14  
15  import org.apache.commons.lang3.StringUtils;
16  import org.apache.commons.lang3.Validate;
17  import org.apache.commons.lang3.builder.EqualsBuilder;
18  import org.apache.commons.lang3.builder.HashCodeBuilder;
19  
20  import guru.mikelue.jdut.jdbc.util.MetaDataWorker;
21  
22  /**
23   * The schema of table.<br>
24   *
25   * <h3>Building of data</h3>
26   * While you are building data for testing, this object defines necessary to declare the data of a table.<br>
27   * The name of table and keys(for deletion) is used by operators of data grain.
28   *
29   * <h3>Validation of data</h3>
30   * After the schema information is fetched from real database, this object contains the real schema defined in database.
31   * And, the auto-inspected keys if there is no one while building data.
32   */
33  public class SchemaTable {
34  	private final static String NULL_NAMING = "<null>";
35  
36  	private Optional<String> catalog = Optional.empty();
37  	private Optional<String> schema = Optional.empty();
38  
39      private String name;
40  	private String quotedTableName;
41  
42  	private MetaDataWorker metaDataWorker;
43  
44      private List<String> keys = Collections.emptyList();
45      private Map<String, SchemaColumn> columns;
46      private Map<String, Integer> nameToIndex;
47      private Map<Integer, String> indexToName;
48  
49      /**
50       * This object is used with {@link Consumer} by {@link SchemaTable#build}.
51       */
52      public class Builder {
53          private Builder() {}
54  
55  		/**
56  		 * Sets meta-data work.
57  		 *
58  		 * @param newWorker The worker of meta data
59  		 *
60  		 * @return cascading self
61  		 */
62  		public Builder metaDataWorker(MetaDataWorker newWorker)
63  		{
64  			metaDataWorker = newWorker;
65  			return this;
66  		}
67  
68  		/**
69  		 * Sets the catalog.
70  		 *
71  		 * @param newCatalog The value of catalog
72  		 *
73  		 * @return cascading self
74  		 *
75  		 * @see DatabaseMetaData#getColumns
76  		 */
77  		public Builder catalog(String newCatalog)
78  		{
79  			catalog = Optional.ofNullable(StringUtils.trimToNull(newCatalog))
80  				.map(SchemaTable.this::treatIdentifier);
81  			return this;
82  		}
83  
84  		/**
85  		 * Sets the schema.
86  		 *
87  		 * @param newSchema The value of schema
88  		 *
89  		 * @return cascading self
90  		 *
91  		 * @see DatabaseMetaData#getColumns
92  		 */
93  		public Builder schema(String newSchema)
94  		{
95  			schema = Optional.ofNullable(StringUtils.trimToNull(newSchema))
96  				.map(SchemaTable.this::treatIdentifier);
97  
98  			return this;
99  		}
100 
101         /**
102          * Sets the name of table.
103          *
104          * @param newName The name of table
105          *
106          * @return cascading self
107          */
108         public Builder name(String newName)
109         {
110 			name = StringUtils.trimToNull(newName);
111 			Validate.notNull(name, "Need table name");
112 
113 			name = SchemaTable.this.treatIdentifier(name);
114 
115             return this;
116         }
117 
118         /**
119          * Sets the keys of table.
120          *
121          * @param newKeys The keys of table
122          *
123          * @return cascading self
124          */
125         public Builder keys(String... newKeys)
126         {
127 			keys = Stream.of(newKeys)
128 				.map(key -> {
129 					String processedKey = StringUtils.trimToNull(key);
130 					if (processedKey == null) {
131 						return null;
132 					}
133 
134 					return SchemaTable.this.treatIdentifier(processedKey);
135 				})
136 				.filter(key -> key != null)
137 				.collect(Collectors.toList());
138 
139             return this;
140         }
141 
142 		/**
143 		 * Adds builder for a column.
144 		 *
145 		 * @param column The added column
146 		 *
147 		 * @return cascading self
148 		 */
149 		public Builder column(SchemaColumn column)
150 		{
151 			String columnName = SchemaTable.this.treatIdentifier(column.getName());
152 
153 			if (!nameToIndex.containsKey(columnName)) {
154 				nameToIndex.put(columnName, nameToIndex.size());
155 				indexToName.put(nameToIndex.get(columnName), columnName);
156 			}
157 
158 			columns.put(columnName, column);
159 
160 			return this;
161 		}
162 
163 		private void initQuotedName()
164 		{
165 			if (metaDataWorker == null) {
166 				quotedTableName = name;
167 				return;
168 			}
169 
170 			if (metaDataWorker.supportsSchemasInDataManipulation()) {
171 				quotedTableName = schema
172 					.map(s -> metaDataWorker.quoteIdentifier(s) + ".")
173 					.orElse("") +
174 					metaDataWorker.quoteIdentifier(name);
175 			} else {
176 				quotedTableName = metaDataWorker.quoteIdentifier(name);
177 			}
178 		}
179     }
180 
181     /**
182      * Builds a table schema by {@link Consumer}.
183      *
184      * @param builderConsumer The building code for table schema
185      *
186      * @return The result schema of table
187      */
188     public static SchemaTable build(Consumer<Builder> builderConsumer)
189     {
190         SchemaTablee.html#SchemaTable">SchemaTable tableSchema = new SchemaTable();
191 		SchemaTable.Builder tableBuilder = tableSchema.new Builder();
192 		tableSchema.columns = new HashMap<>(CollectionUsage.HASH_SPACE_OF_COLUMNS);
193 		tableSchema.indexToName = new HashMap<>(CollectionUsage.HASH_SPACE_OF_COLUMNS);
194 		tableSchema.nameToIndex = new HashMap<>(CollectionUsage.HASH_SPACE_OF_COLUMNS);
195 
196         builderConsumer.accept(tableBuilder);
197 		tableBuilder.initQuotedName();
198 
199 		return tableSchema.clone();
200     }
201 
202     private SchemaTable() {}
203 
204 	/**
205 	 * Gets the worker of meta-data.
206 	 */
207 	public MetaDataWorker getMetaDataWorker()
208 	{
209 		return metaDataWorker;
210 	}
211 
212     /**
213      * Gets name of table.
214      *
215      * @return The name of table
216      */
217     public String getName()
218     {
219         return name;
220     }
221 
222 	/**
223 	 * Gets quoted string of full name(may be include schema) of table.
224 	 *
225 	 * If the {@link Builder#metaDataWorker(MetaDataWorker)} is not set,
226 	 * the value returned by this method would be as same as {@link #getName}.
227 	 *
228 	 * @return The quoted name of table
229 	 */
230 	public String getQuotedFullName()
231 	{
232 		return quotedTableName;
233 	}
234 
235 	/**
236 	 * Gets optional value of schema.
237 	 *
238 	 * @return the schema
239 	 */
240 	public Optional<String> getSchema()
241 	{
242 		return schema;
243 	}
244 
245 	/**
246 	 * Gets optional value of schema.
247 	 *
248 	 * @return the schema
249 	 */
250 	public Optional<String> getCatalog()
251 	{
252 		return catalog;
253 	}
254 
255 	/**
256 	 * Gets keys of table.
257 	 *
258 	 * @return The keys of table
259 	 */
260 	public List<String> getKeys()
261 	{
262 		return keys;
263 	}
264 
265 	/**
266 	 * Gets number of columns.
267 	 *
268 	 * @return The number of columns
269 	 */
270 	public int getNumberOfColumns()
271 	{
272 		return columns.size();
273 	}
274 
275 	/**
276 	 * Gets the columns(sorted by added sequence).
277 	 *
278 	 * @return The columns
279 	 */
280 	public List<SchemaColumn> getColumns()
281 	{
282 		List<SchemaColumn> result = new ArrayList<>(getNumberOfColumns());
283 		IntStream.range(0, getNumberOfColumns())
284 			.forEach(
285 				i -> result.add(getColumn(i))
286 			);
287 
288 		return result;
289 	}
290 
291 	/**
292 	 * Whether or not has a column by name.
293 	 *
294 	 * @param columnName The name of column
295 	 *
296 	 * @return true if the column is existing
297 	 */
298 	public boolean hasColumn(String columnName)
299 	{
300 		return columns.containsKey(treatIdentifier(columnName));
301 	}
302 
303     /**
304      * Gets the column definition by name.
305      *
306      * @param columnName The name of column
307      *
308      * @return The column definition
309      *
310      * @throws IllegalArgumentException If the name of column cannot be found for definition
311      */
312     public SchemaColumn getColumn(String columnName)
313     {
314         SchemaColumn column = columns.get(treatIdentifier(columnName));
315         if (column == null) {
316             throw new IllegalArgumentException(String.format("Cannot find column: \"%s\"", columnName));
317         }
318 
319         return column;
320     }
321 
322     /**
323      * Gets the column definition by column index.
324      *
325      * @param columnIndex The index of column
326      *
327      * @return The column definition
328      *
329      * @throws IllegalArgumentException If the name of column cannot be found for definition
330      */
331     public SchemaColumn getColumn(int columnIndex)
332     {
333         String columnName = indexToName.get(columnIndex);
334         if (columnName == null) {
335             throw new IllegalArgumentException(String.format("Cannot find column by index: \"%d\"", columnIndex));
336         }
337 
338         return getColumn(columnName);
339     }
340 
341 	/**
342 	 * Converts the case of <em>identifier</em> by meta-data of database.
343 	 *
344 	 * @param identifier The identifier to be processed
345 	 *
346 	 * @return The processed identifier
347 	 */
348 	public String treatIdentifier(String identifier)
349 	{
350 		identifier = StringUtils.trimToEmpty(identifier);
351 		if (StringUtils.isEmpty(identifier)) {
352 			return identifier;
353 		}
354 
355 		if (metaDataWorker == null) {
356 			return identifier.toLowerCase();
357 		}
358 
359 		return metaDataWorker.processIdentifier(identifier);
360 	}
361 
362 	/**
363 	 * Use {@link DatabaseMetaData#getIdentifierQuoteString} to quote <em>identifier</em>.
364 	 *
365 	 * @param identifier The identifier to be quoted
366 	 *
367 	 * @return The quoted identifier
368 	 */
369 	public String quoteIdentifier(String identifier)
370 	{
371 		if (metaDataWorker == null) {
372 			return identifier;
373 		}
374 
375 		return metaDataWorker.quoteIdentifier(identifier);
376 	}
377 
378 	/**
379 	 * Gets name of table with catalog and schema and keys.<br>
380 	 *
381 	 * Use <em>{@code "<null>"}</em> if the value is not set
382 	 *
383 	 * @return The string can be used in collection
384 	 */
385 	public String getFullTableName()
386 	{
387 		return String.format(
388 			"%s.%s.%s|%s",
389 			catalog.orElse(NULL_NAMING), schema.orElse(NULL_NAMING),
390 			name,
391 			keys.stream().collect(
392 				Collectors.joining(",")
393 			)
394 		);
395 	}
396 
397     /**
398      * Safe clone for the fields of this object.
399      */
400     @Override
401     protected SchemaTable clone()
402     {
403         SchemaTable clonedObject = safeClone();
404 
405         clonedObject.keys = Collections.unmodifiableList(this.keys);
406         clonedObject.columns = Collections.unmodifiableMap(this.columns);
407         clonedObject.nameToIndex = Collections.unmodifiableMap(this.nameToIndex);
408         clonedObject.indexToName = Collections.unmodifiableMap(this.indexToName);
409 
410         return clonedObject;
411     }
412 
413 	private SchemaTable safeClone()
414 	{
415         SchemaTable.html#SchemaTable">SchemaTable clonedObject = new SchemaTable();
416 		clonedObject.quotedTableName = this.quotedTableName;
417         clonedObject.name = this.name;
418 		clonedObject.schema = this.schema;
419 		clonedObject.catalog = this.catalog;
420 		clonedObject.metaDataWorker = this.metaDataWorker;
421 		return clonedObject;
422 	}
423 
424 	/**
425 	 * Compares the name of table, columns, keys, and indexed columns.
426 	 */
427 	@Override
428 	public boolean equals(Object obj)
429 	{
430 		if (obj == null) { return false; }
431 		if (obj == this) { return true; }
432 		if (obj.getClass() != getClass()) {
433 			return false;
434 		}
435 
436 		SchemaTable rhs = (SchemaTable)obj;
437 		return new EqualsBuilder()
438 			.append(this.name, rhs.name)
439 			.append(this.columns, rhs.columns)
440 			.append(this.keys, rhs.keys)
441 			.isEquals();
442 	}
443 
444 	/**
445 	 * Hashes the name of table, columns, keys, and indexed columns.
446 	 */
447 	@Override
448 	public int hashCode()
449 	{
450 		return new HashCodeBuilder(494534511, 401552629)
451 			.append(name)
452 			.append(keys)
453 			.append(columns)
454 		.toHashCode();
455 	}
456 
457 	@Override
458 	public String toString()
459 	{
460 		return String.format(
461 			"Table: %s.%s.\"%s\". Columns [%s]",
462 			catalog.orElse(NULL_NAMING),
463 			schema.orElse(NULL_NAMING),
464 			name,
465 			columns.values().stream()
466 				.map(column -> column.getName())
467 				.collect(Collectors.joining(",", "\"", "\""))
468 		);
469 	}
470 }
471