/**
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* Copyright 2012-2016 the original author or authors.
*/
package org.assertj.db.type;
import org.assertj.db.exception.AssertJDBException;
import org.assertj.db.type.lettercase.LetterCase;
import org.assertj.db.util.NameComparator;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A table in the database to read to get the values.
* <p>
* The different informations of the table are {@link Source} or {@link DataSource}, name of the table and optionally
* the columns to check and to exclude.
* </p>
* <p>
* Examples of instantiation :
* </p>
* <ul>
* <li>
* <p>
* This {@link Table} point to a table called {@code movie} in a H2 database in memory like indicated in the
* {@link Source}.
* </p>
*
* <pre>
* <code class='java'>
* Source source = new Source("jdbc:h2:mem:test", "sa", "");
* Table table = new Table(source, "movie");
* </code>
* </pre>
*
* </li>
* <li>
* <p>
* Below the {@link Table} {@code table1} point to a table called {@code song} (but only on the columns called
* {@code number} and {@code title}).<br>
* And the {@link Table} {@code table2} point to a table called {@code musician} (but ignore on the column called
* {@code birthday}).<br>
* The {@link Table} use a {@code DataSource} instead of a {@link Source} like above.
* </p>
*
* <pre>
* <code class='java'>
* DataSource dataSource = ...;
* Table table1 = new Table(dataSource, "song", new String[] { "number", "title" }, null);
* Table table2 = new Table(dataSource, "musician", null, new String[] { "birthday" });
* </code>
* </pre>
*
* </li>
* </ul>
*
* @author RĂ©gis Pouiller
*/
public class Table extends AbstractDbData<Table> {
/**
* The name of the table.
*/
private String name;
/**
* The list of columns of the table.
*/
private List<String> columnsList;
/**
* The columns to check.
*/
private String[] columnsToCheck;
/**
* The columns to exclude.
*/
private String[] columnsToExclude;
/**
* The columns to order.
* @since 1.2.0
*/
private Order[] columnsToOrder;
/**
* Indicates an order with the name on which is the order and the type.
* @see org.assertj.db.type.Table.Order.OrderType
*/
public static class Order {
/**
* The name of the order.
*/
private String name;
/**
* The type of the order.
*/
private OrderType type;
/**
* Enumeration of the type of order.
*/
public enum OrderType {
/**
* Ascending order.
*/
ASC,
/**
* Descending order.
*/
DESC;
}
/**
* Builds an ascending order.
* @param name The name of the order.
* @return An ascending order.
*/
public static Order asc(String name) {
return getOrder(name, OrderType.ASC);
}
/**
* Builds the descending order.
* @param name The name of the order.
* @return The descending order.
*/
public static Order desc(String name) {
return getOrder(name, OrderType.DESC);
}
/**
* Builds an order.
* @param name The name of the order.
* @param type The type of the order.
* @return The instantiated order.
*/
private static Order getOrder(String name, OrderType type) {
return new Order(name, type);
}
/**
* Constructor.
* @param name The name of the order.
* @param type The type of the order.
*/
private Order(String name, OrderType type) {
this.name = name;
this.type = type;
}
/**
* Returns the name of the order.
* @return The name of the order.
*/
public String getName() {
return name;
}
/**
* Returns the type of the order.
* @return The type of the order.
*/
public OrderType getType() {
return type;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Order) {
Order order = (Order) obj;
if (order.type == type) {
if ((name == null && order.name == null) ||
(name != null && name.equals(order.name))) {
return true;
}
}
}
return false;
}
}
/**
* Default constructor.
*/
public Table() {
super(Table.class, DataType.TABLE);
}
/**
* Constructor with a {@link Source} and the name of the table.
*
* @param source {@link Source} to connect to the database.
* @param name Name of the table.
*/
public Table(Source source, String name) {
this(source, name, null, null, null);
}
/**
* Constructor with a {@link Source}, the name of the table and the columns to check and to exclude.
*
* @param source {@link Source} to connect to the database.
* @param name Name of the table.
* @param columnsToCheck Array of the name of the columns to check. If {@code null} that means to check all the
* columns.
* @param columnsToExclude Array of the name of the columns to exclude. If {@code null} that means to exclude no
* column.
*/
public Table(Source source, String name, String[] columnsToCheck, String[] columnsToExclude) {
this(source, name, null, columnsToCheck, columnsToExclude);
}
/**
* Constructor with a dataSource and the name of the table.
*
* @param dataSource DataSource of the database.
* @param name Name of the table.
*/
public Table(DataSource dataSource, String name) {
this(dataSource, name, null, null, null);
}
/**
* Constructor with a connection, the name of the table and the columns to check and to exclude.
*
* @param dataSource DataSource of the database.
* @param name Name of the table.
* @param columnsToCheck Array of the name of the columns to check. If {@code null} that means to check all the
* columns.
* @param columnsToExclude Array of the name of the columns to exclude. If {@code null} that means to exclude no
* column.
*/
public Table(DataSource dataSource, String name, String[] columnsToCheck, String[] columnsToExclude) {
this(dataSource, name, null, columnsToCheck, columnsToExclude);
}
/**
* Constructor with a {@link Source} and the name of the table.
*
* @param source {@link Source} to connect to the database.
* @param name Name of the table.
* @since 1.2.0
*/
public Table(Source source, String name, Order[] columnsToOrder) {
this(source, name, columnsToOrder, null, null);
}
/**
* Constructor with a {@link Source}, the name of the table and the columns to check and to exclude.
*
* @param source {@link Source} to connect to the database.
* @param name Name of the table.
* @param columnsToCheck Array of the name of the columns to check. If {@code null} that means to check all the
* columns.
* @param columnsToExclude Array of the name of the columns to exclude. If {@code null} that means to exclude no
* column.
* @since 1.2.0
*/
public Table(Source source, String name, Order[] columnsToOrder, String[] columnsToCheck, String[] columnsToExclude) {
super(Table.class, DataType.TABLE, source);
setName(name);
setColumnsToOrder(columnsToOrder);
setColumnsToCheck(columnsToCheck);
setColumnsToExclude(columnsToExclude);
}
/**
* Constructor with a dataSource and the name of the table.
*
* @param dataSource DataSource of the database.
* @param name Name of the table.
* @since 1.2.0
*/
public Table(DataSource dataSource, String name, Order[] columnsToOrder) {
this(dataSource, name, columnsToOrder, null, null);
}
/**
* Constructor with a connection, the name of the table and the columns to check and to exclude.
*
* @param dataSource DataSource of the database.
* @param name Name of the table.
* @param columnsToCheck Array of the name of the columns to check. If {@code null} that means to check all the
* columns.
* @param columnsToExclude Array of the name of the columns to exclude. If {@code null} that means to exclude no
* column.
* @since 1.2.0
*/
public Table(DataSource dataSource, String name, Order[] columnsToOrder, String[] columnsToCheck, String[] columnsToExclude) {
super(Table.class, DataType.TABLE, dataSource);
setName(name);
setColumnsToOrder(columnsToOrder);
setColumnsToCheck(columnsToCheck);
setColumnsToExclude(columnsToExclude);
}
/**
* Return the name of the table.
*
* @return the name of the table.
* @see #setName(String)
*/
public String getName() {
return name;
}
/**
* Sets the name of the table.
*
* @param name The name of the table.
* @return The actual instance.
* @see #getName()
*/
public Table setName(String name) {
if (name == null) {
throw new NullPointerException("name can not be null");
}
this.name = name;
setNameFromDb();
return this;
}
/** {@inheritDoc} */
@Override
public Table setDataSource(DataSource dataSource) {
Table table = super.setDataSource(dataSource);
setNameFromDb();
return table;
}
/** {@inheritDoc} */
@Override
public Table setSource(Source source) {
Table table = super.setSource(source);
setNameFromDb();
return table;
}
/**
* Set the name from the corresponding name in the database.
*/
private void setNameFromDb() {
if (name != null && (getSource() != null || getDataSource() != null)) {
try (Connection connection = getConnection()) {
LetterCase tableLetterCase = getTableLetterCase();
LetterCase columnLetterCase = getColumnLetterCase();
DatabaseMetaData metaData = connection.getMetaData();
try (ResultSet tableResultSet = metaData.getTables(getCatalog(connection), getSchema(connection), null,
new String[] { "TABLE" })) {
while (tableResultSet.next()) {
String tableName = tableResultSet.getString("TABLE_NAME");
if (tableLetterCase.isEqual(tableName, name)) {
name = tableLetterCase.convert(tableName);
break;
}
}
}
columnsList = new ArrayList<>();
try (ResultSet columnsResultSet = metaData.getColumns(getCatalog(connection), getSchema(connection), name, null)) {
while (columnsResultSet.next()) {
String column = columnsResultSet.getString("COLUMN_NAME");
columnsList.add(columnLetterCase.convert(column));
}
}
} catch (SQLException e) {
throw new AssertJDBException(e);
}
}
}
/**
* Returns the columns to check (which are present in {@link AbstractDbData#getColumnsNameList()}).
*
* @return Array of the name of the columns to check. If {@code null} that means to check all the columns.
* @see #setColumnsToCheck(String[])
*/
public String[] getColumnsToCheck() {
if (columnsToCheck == null) {
return null;
}
return columnsToCheck.clone();
}
/**
* Sets the columns to check (which are present in {@link AbstractDbData#getColumnsNameList()}).
*
* @param columnsToCheck Array of the name of the columns to check. If {@code null} that means to check all the
* columns.
* @return The actual instance.
* @throws NullPointerException If one of the name in {@code columnsToCheck} is {@code null}.
* @see #getColumnsToCheck()
*/
public Table setColumnsToCheck(String[] columnsToCheck) {
if (columnsList == null) {
throw new AssertJDBException("The table name and the source or datasource must be set first");
}
if (columnsToCheck != null) {
LetterCase letterCase = getColumnLetterCase();
// If the parameter is not null, all the names are convert
// before setting the instance field
this.columnsToCheck = new String[columnsToCheck.length];
for (int index = 0; index < columnsToCheck.length; index++) {
String column = columnsToCheck[index];
if (column == null) {
throw new NullPointerException("The name of the column can not be null");
}
int indexOf = NameComparator.INSTANCE.indexOf(columnsList, column, letterCase);
if (indexOf != -1) {
this.columnsToCheck[index] = columnsList.get(indexOf);
}
}
} else {
this.columnsToCheck = null;
}
return this;
}
/**
* Returns the columns to exclude (which are not present in {@link AbstractDbData#getColumnsNameList()}).
*
* @return The columns.
* @see #setColumnsToExclude(String[])
*/
public String[] getColumnsToExclude() {
if (columnsToExclude == null) {
return null;
}
return columnsToExclude.clone();
}
/**
* Sets the columns to exclude (which are not present in {@link AbstractDbData#getColumnsNameList()}).
*
* @param columnsToExclude The columns.
* @return The actual instance.
* @see #getColumnsToExclude()
*/
public Table setColumnsToExclude(String[] columnsToExclude) {
if (columnsList == null) {
throw new AssertJDBException("The table name and the source or datasource must be set first");
}
if (columnsToExclude != null) {
LetterCase letterCase = getColumnLetterCase();
this.columnsToExclude = new String[columnsToExclude.length];
for (int index = 0; index < columnsToExclude.length; index++) {
String column = columnsToExclude[index];
if (column == null) {
throw new NullPointerException("The name of the column can not be null");
}
int indexOf = NameComparator.INSTANCE.indexOf(columnsList, column, letterCase);
if (indexOf != -1) {
this.columnsToExclude[index] = columnsList.get(indexOf);
}
}
} else {
this.columnsToExclude = null;
}
return this;
}
/**
* Returns the columns to order (which are used in {@code ORDER BY}).
*
* @return Array of the name of the columns to order. If {@code null} that means not to do order.
* @see #setColumnsToOrder(Order[])
*/
public Order[] getColumnsToOrder() {
if (columnsToOrder == null) {
return null;
}
return columnsToOrder.clone();
}
/**
* Sets the columns to order (which are used in {@code ORDER BY}).
*
* @param columnsToOrder The columns.
* @return The actual instance.
* @see #getColumnsToOrder()
*/
public Table setColumnsToOrder(Order[] columnsToOrder) {
if (columnsList == null) {
throw new AssertJDBException("The table name and the source or datasource must be set first");
}
if (columnsToOrder != null) {
LetterCase letterCase = getColumnLetterCase();
this.columnsToOrder = new Order[columnsToOrder.length];
for (int index = 0; index < columnsToOrder.length; index++) {
Order order = columnsToOrder[index];
if (order == null) {
throw new NullPointerException("The order can not be null");
}
String column = order.getName();
if (order == null) {
throw new NullPointerException("The name of the column for order can not be null");
}
int indexOf = NameComparator.INSTANCE.indexOf(columnsList, column, letterCase);
if (indexOf != -1) {
String columnName = columnsList.get(indexOf);
this.columnsToOrder[index] = Order.getOrder(columnName, order.getType());
}
}
} else {
this.columnsToOrder = null;
}
return this;
}
/**
* Returns the SQL request.
*
* @return The SQL request.
* @throws NullPointerException If the {@link #name} field is {@code null}.
* @see AbstractDbData#getRequest()
*/
public String getRequest() {
if (name == null) {
throw new NullPointerException("name can not be null");
}
// Get the request about the name of the table and the columns to check
StringBuilder stringBuilder = new StringBuilder("SELECT ");
if (columnsToCheck == null) {
stringBuilder.append("*");
} else {
for (String column : columnsToCheck) {
if (stringBuilder.length() > 7) {
stringBuilder.append(", ");
}
stringBuilder.append(column);
}
}
stringBuilder.append(" FROM ");
stringBuilder.append(name);
if (columnsToOrder != null) {
for (int index = 0 ; index < columnsToOrder.length ; index++) {
if (index == 0) {
stringBuilder.append(" ORDER BY ");
}
else {
stringBuilder.append(", ");
}
stringBuilder.append(columnsToOrder[index].getName());
if (columnsToOrder[index].getType() == Order.OrderType.DESC) {
stringBuilder.append(" DESC");
}
}
}
return stringBuilder.toString();
}
/**
* Collects the columns name from the {@code ResultSet} on the table.
* <p>
* This method use the {@link ResultSetMetaData} from the <code>resultSet</code> parameter to list the name of the
* columns. But the columns to exclude are not collected.
* </p>
*
* @param resultSet The {@code ResultSet}.
* @throws SQLException SQL Exception.
*/
private void collectColumnsNameFromResultSet(ResultSet resultSet) throws SQLException {
LetterCase letterCase = getColumnLetterCase();
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
List<String> columnsNameList = new ArrayList<>();
List<String> columnsToExcludeList = null;
if (columnsToExclude != null) {
columnsToExcludeList = Arrays.asList(columnsToExclude);
}
for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) {
String columnName = letterCase.convert(resultSetMetaData.getColumnLabel(i));
if (columnsToExcludeList == null
|| !NameComparator.INSTANCE.contains(columnsToExcludeList, columnName, letterCase)) {
columnsNameList.add(columnName);
}
}
setColumnsNameList(columnsNameList);
}
/**
* Collects the primary key name from the {@code Connection} to the database.
* <p>
* This method use the {@link DatabaseMetaData} from the {@code Connection} parameter to list the primary keys of the
* table.
* </p>
*
* @param connection The {@code Connection} to the database.
* @throws SQLException SQL Exception.
*/
private void collectPrimaryKeyName(Connection connection) throws SQLException {
String catalog = getCatalog(connection);
String schema = getSchema(connection);
List<String> pksNameList = new ArrayList<>();
DatabaseMetaData metaData = connection.getMetaData();
String tableName = name;
try (ResultSet resultSet = metaData.getTables(catalog, schema, null, new String[] { "TABLE" })) {
LetterCase letterCase = getTableLetterCase();
while (resultSet.next()) {
String tableResult = resultSet.getString("TABLE_NAME");
if (letterCase.isEqual(tableName, tableResult)) {
tableName = tableResult;
break;
}
}
}
try (ResultSet resultSet = metaData.getPrimaryKeys(catalog, schema, tableName)) {
LetterCase letterCase = getPrimaryKeyLetterCase();
while (resultSet.next()) {
String columnName = resultSet.getString("COLUMN_NAME");
if (NameComparator.INSTANCE.contains(getColumnsNameList(), columnName, letterCase)) {
String pkName = letterCase.convert(columnName);
pksNameList.add(pkName);
}
}
}
setPksNameList(pksNameList);
}
/**
* Specific implementation of the loading for a {@code Table}.
*
* @param connection {@link Connection} to the database provided by {@link AbstractDbData#load()} private method.
* @throws NullPointerException If the {@link #name} field is {@code null}.
* @throws SQLException SQL Exception.
* @see AbstractDbData#loadImpl(Connection)
*/
@Override
protected void loadImpl(Connection connection) throws SQLException {
if (name == null) {
throw new NullPointerException("name can not be null");
}
try (Statement statement = connection.createStatement()) {
try (ResultSet resultSet = statement.executeQuery(getRequest())) {
collectColumnsNameFromResultSet(resultSet);
collectRowsFromResultSet(resultSet);
}
}
collectPrimaryKeyName(connection);
if (columnsToOrder == null) {
sortRows();
}
}
}