/*
* Copyright (c) 2012 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* HUMBOLDT EU Integrated Project #030962
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.hale.io.jdbc;
import static eu.esdihumboldt.hale.io.jdbc.JDBCUtil.determineNamespace;
import static eu.esdihumboldt.hale.io.jdbc.JDBCUtil.determineQuoteString;
import static eu.esdihumboldt.hale.io.jdbc.JDBCUtil.unquote;
import java.io.IOException;
import java.net.URI;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.xml.namespace.QName;
import eu.esdihumboldt.hale.common.core.io.IOProviderConfigurationException;
import eu.esdihumboldt.hale.common.core.io.ProgressIndicator;
import eu.esdihumboldt.hale.common.core.io.Text;
import eu.esdihumboldt.hale.common.core.io.Value;
import eu.esdihumboldt.hale.common.core.io.report.IOReporter;
import eu.esdihumboldt.hale.common.core.io.report.impl.IOMessageImpl;
import eu.esdihumboldt.hale.common.schema.model.ChildDefinition;
import eu.esdihumboldt.hale.common.schema.model.Schema;
import eu.esdihumboldt.hale.common.schema.model.TypeDefinition;
import eu.esdihumboldt.hale.common.schema.model.constraint.property.Cardinality;
import eu.esdihumboldt.hale.common.schema.model.constraint.property.NillableFlag;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.HasValueFlag;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.MappableFlag;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.MappingRelevantFlag;
import eu.esdihumboldt.hale.common.schema.model.impl.DefaultPropertyDefinition;
import eu.esdihumboldt.hale.common.schema.model.impl.DefaultSchema;
import eu.esdihumboldt.hale.common.schema.model.impl.DefaultTypeDefinition;
import eu.esdihumboldt.hale.common.schema.persist.AbstractCachedSchemaReader;
import eu.esdihumboldt.hale.io.jdbc.constraints.SQLArray;
import eu.esdihumboldt.hale.io.jdbc.constraints.SQLQuery;
import eu.esdihumboldt.hale.io.jdbc.extension.JDBCSchemaReaderAdvisor;
import eu.esdihumboldt.hale.io.jdbc.extension.internal.SchemaReaderAdvisorExtension;
import schemacrawler.schema.Catalog;
import schemacrawler.schema.ResultsColumn;
import schemacrawler.schema.ResultsColumns;
import schemacrawler.schemacrawler.SchemaCrawlerOptions;
import schemacrawler.schemacrawler.SchemaInfoLevel;
import schemacrawler.utility.SchemaCrawlerUtility;
/**
* Reads the schema of an SQL query via JDBC.
*
* @author Simon Templer
*/
public class SQLSchemaReader extends AbstractCachedSchemaReader
implements JDBCConstants, JDBCProvider {
/**
* Name of the parameter specifying the SQL query.
*/
public static final String PARAM_SQL = "sql";
/**
* Name of the parameter specifying the type name to use for the SQL query.
*/
public static final String PARAM_TYPE_NAME = "typename";
/**
* Fixed namespace for SQL types.
*/
public static final String NAMESPACE = "jdbc:sql";
/**
* Default constructor
*/
public SQLSchemaReader() {
super();
addSupportedParameter(PARAM_USER);
addSupportedParameter(PARAM_PASSWORD);
}
@Override
protected boolean useCache(Value cache) {
// check if a connection can be established
try {
Connection conn = JDBCConnection.getConnection(this);
conn.close();
return false;
} catch (Exception e) {
// on error, use cache
return true;
}
}
/**
* To get Connection. Override this to load the customized connection
*
* @return Connection object after loading driver.
* @throws SQLException if connection could not be made.
*/
@Override
public Connection getConnection() throws SQLException {
return JDBCConnection.getConnection(this);
}
@Override
protected Schema loadFromSource(ProgressIndicator progress, IOReporter reporter)
throws IOProviderConfigurationException, IOException {
DefaultSchema typeIndex = null;
String query = null;
Text text = getParameter(PARAM_SQL).as(Text.class);
if (text != null) {
query = text.getText();
}
if (query == null) {
query = getParameter(PARAM_SQL).as(String.class);
}
if (query == null) {
reporter.setSuccess(false);
reporter.setSummary("No SQL query specified");
return null;
}
String typename = getParameter(PARAM_TYPE_NAME).as(String.class);
if (typename == null) {
reporter.setSuccess(false);
reporter.setSummary(
"Name of the type that the SQL query should be represented as must be specified");
return null;
}
progress.begin("Read SQL query schema", ProgressIndicator.UNKNOWN);
Connection connection = null;
try {
// connect to the database
try {
connection = getConnection();
} catch (Exception e) {
reporter.error(new IOMessageImpl(e.getLocalizedMessage(), e));
reporter.setSuccess(false);
reporter.setSummary("Failed to connect to database.");
return null;
}
// don't fail if Connection.setReadOnly() throws an exception (e.g.
// SQLite JDBC driver does not allow changing the flag after the
// connection has been created), report a warning message instead
try {
connection.setReadOnly(true);
} catch (SQLException e) {
// ignore
// reporter.warn(new IOMessageImpl(e.getLocalizedMessage(), e));
}
connection.setAutoCommit(false);
// get advisor
JDBCSchemaReaderAdvisor advisor = SchemaReaderAdvisorExtension.getInstance()
.getAdvisor(connection);
// determine quotes character
@SuppressWarnings("unused")
String quotes = determineQuoteString(connection);
// FIXME not actually used here or in JDBC schema reader
URI jdbcURI = getSource().getLocation();
String dbNamespace = determineNamespace(jdbcURI, advisor);
String namespace = NAMESPACE;
SchemaCrawlerOptions options = new SchemaCrawlerOptions();
SchemaInfoLevel level = new SchemaInfoLevel();
level.setTag("hale");
// these are enabled by default, we don't need them (yet)
level.setRetrieveSchemaCrawlerInfo(false);
level.setRetrieveJdbcDriverInfo(false);
level.setRetrieveDatabaseInfo(false);
level.setRetrieveTables(false);
level.setRetrieveTableColumns(false);
level.setRetrieveForeignKeys(false);
// set what we need
level.setRetrieveColumnDataTypes(true);
level.setRetrieveUserDefinedColumnDataTypes(true);
options.setSchemaInfoLevel(level);
if (advisor != null) {
advisor.configureSchemaCrawler(options);
}
final Catalog database = SchemaCrawlerUtility.getCatalog(connection, options);
// create the type index
typeIndex = new DefaultSchema(dbNamespace, jdbcURI);
Statement st = null;
try {
st = JDBCUtil.createReadStatement(connection, 1);
// support project variables
String processedQuery = JDBCUtil.replaceVariables(query, getServiceProvider());
ResultSet result = st.executeQuery(processedQuery);
// the query represents a type
// get the type definition
TypeDefinition type = addTableType(query, namespace, typeIndex, connection,
reporter, typename);
ResultsColumns additionalInfo = SchemaCrawlerUtility.getResultColumns(result);
for (final ResultsColumn column : additionalInfo.getColumns()) {
getOrCreateProperty(type, column, namespace, typeIndex, connection, reporter,
database);
}
} finally {
if (st != null) {
st.close();
}
}
reporter.setSuccess(true);
} catch (Exception e) {
throw new IOProviderConfigurationException("Failed to read database schema", e);
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
// ignore
}
}
progress.end();
}
return typeIndex;
}
/**
* Gets or creates a property definition for the given column. Its type
* definition is created, too, if necessary.
*
* @param tableType the type definition of the parent table this column
* belongs too
* @param column the column to get or create a property definition for
* @param namespace the schema namespace
* @param typeIndex the type index
* @param connection the database connection
* @param reporter the reporter
* @param catalog the database information
* @return the property definition for the given column
*/
private DefaultPropertyDefinition getOrCreateProperty(TypeDefinition tableType,
ResultsColumn column, String namespace, DefaultSchema typeIndex, Connection connection,
IOReporter reporter, Catalog catalog) {
QName name = new QName(unquote(column.getName()));
// check for existing property definition
ChildDefinition<?> existing = tableType.getChild(name);
if (existing != null)
return (DefaultPropertyDefinition) existing;
// create new one
// determine the column type
TypeDefinition columnType = JDBCSchemaReader.getOrCreateColumnType(column, namespace,
typeIndex, connection, tableType, reporter, catalog);
SQLArray arrayInfo = columnType.getConstraint(SQLArray.class);
// create the property
DefaultPropertyDefinition property = new DefaultPropertyDefinition(name, tableType,
columnType);
// configure property
if (column.getRemarks() != null && !column.getRemarks().isEmpty()) {
property.setDescription(column.getRemarks());
}
property.setConstraint(NillableFlag.get(column.isNullable()));
if (arrayInfo.isArray() && arrayInfo.getDimension() <= 1) {
// XXX for now, use multiple occurrence representation also if
// dimension is not known (0)
if (!arrayInfo.hasSize(0)) {
property.setConstraint(Cardinality.CC_ANY_NUMBER);
}
else {
long min = 0; // XXX what is appropriate as minimum?
long max = arrayInfo.getSize(0);
property.setConstraint(Cardinality.get(min, max));
}
}
else {
property.setConstraint(Cardinality.CC_EXACTLY_ONCE);
}
return property;
}
/**
* Create the type definition for a query.
*
* @param query the SQL query
* @param namespace the namespace for the type
* @param types the schema to add the type to
* @param connection the database connection
* @param reporter the reporter
* @param typename the name to use for the type
*
* @return the type definition for the given table
*/
private TypeDefinition addTableType(String query, String namespace, DefaultSchema types,
Connection connection, IOReporter reporter, String typename) {
QName typeName = new QName(namespace, unquote(typename));
// check for existing type
TypeDefinition existingType = types.getType(typeName);
if (existingType != null)
return existingType;
// create new type
DefaultTypeDefinition type = new DefaultTypeDefinition(typeName);
// set SQL query as description
type.setDescription(query);
type.setConstraint(new SQLQuery(query));
// configure type
type.setConstraint(MappableFlag.ENABLED);
type.setConstraint(MappingRelevantFlag.ENABLED);
type.setConstraint(HasValueFlag.DISABLED);
types.addType(type);
return type;
}
@Override
protected String getDefaultTypeName() {
return "Database";
}
}