/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org)
*
* 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.
*/
package org.jkiss.dbeaver.model.impl.jdbc;
import org.eclipse.core.runtime.IAdaptable;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.ModelPreferences;
import org.jkiss.dbeaver.model.*;
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
import org.jkiss.dbeaver.model.data.DBDDataFormatterProfile;
import org.jkiss.dbeaver.model.data.DBDPreferences;
import org.jkiss.dbeaver.model.data.DBDValueHandler;
import org.jkiss.dbeaver.model.exec.*;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCDatabaseMetaData;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCFactory;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCSession;
import org.jkiss.dbeaver.model.impl.jdbc.data.handlers.JDBCObjectValueHandler;
import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCConnectionImpl;
import org.jkiss.dbeaver.model.impl.jdbc.exec.JDBCFactoryDefault;
import org.jkiss.dbeaver.model.messages.ModelMessages;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.DBRRunnableWithProgress;
import org.jkiss.dbeaver.model.sql.SQLDataSource;
import org.jkiss.dbeaver.model.sql.SQLDialect;
import org.jkiss.dbeaver.model.sql.SQLState;
import org.jkiss.dbeaver.model.struct.DBSDataType;
import org.jkiss.dbeaver.model.struct.DBSObject;
import org.jkiss.dbeaver.model.struct.DBSObjectContainer;
import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.dbeaver.utils.RuntimeUtils;
import org.jkiss.utils.CommonUtils;
import java.lang.reflect.InvocationTargetException;
import java.net.SocketException;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* JDBC data source
*/
public abstract class JDBCDataSource
implements
DBPDataSource,
SQLDataSource,
DBPDataTypeProvider,
DBPErrorAssistant,
DBPRefreshableObject,
DBDPreferences,
DBSObject,
DBSObjectContainer,
DBCQueryTransformProvider,
IAdaptable
{
private static final Log log = Log.getLog(JDBCDataSource.class);
public static final int DISCONNECT_TIMEOUT = 5000;
@NotNull
private final DBPDataSourceContainer container;
@NotNull
protected JDBCExecutionContext executionContext;
@Nullable
protected JDBCExecutionContext metaContext;
@NotNull
private final List<JDBCExecutionContext> allContexts = new ArrayList<>();
@NotNull
protected volatile DBPDataSourceInfo dataSourceInfo;
protected final SQLDialect sqlDialect;
protected final JDBCFactory jdbcFactory;
private int databaseMajorVersion;
private int databaseMinorVersion;
protected JDBCDataSource(@NotNull DBRProgressMonitor monitor, @NotNull DBPDataSourceContainer container, @NotNull SQLDialect dialect)
throws DBException
{
this(monitor, container, dialect, true);
}
protected JDBCDataSource(@NotNull DBRProgressMonitor monitor, @NotNull DBPDataSourceContainer container, @NotNull SQLDialect dialect, boolean initContext)
throws DBException
{
this.dataSourceInfo = new JDBCDataSourceInfo(container);
this.sqlDialect = dialect;
this.jdbcFactory = createJdbcFactory();
this.container = container;
if (initContext) {
initializeMainContext(monitor);
}
}
protected void initializeMainContext(@NotNull DBRProgressMonitor monitor) throws DBCException {
this.executionContext = new JDBCExecutionContext(this, "Main");
this.executionContext.connect(monitor, null, null, false, true);
}
protected Connection openConnection(@NotNull DBRProgressMonitor monitor, @NotNull String purpose)
throws DBCException
{
// It MUST be a JDBC driver
Driver driverInstance;
try {
driverInstance = getDriverInstance(monitor);
} catch (DBException e) {
throw new DBCConnectException("Can't create driver instance", e, this);
}
// Set properties
Properties connectProps = new Properties();
{
// Use properties defined by datasource itself
Map<String,String> internalProps = getInternalConnectionProperties(monitor, purpose);
if (internalProps != null) {
connectProps.putAll(internalProps);
}
}
{
// Use driver properties
final Map<Object, Object> driverProperties = container.getDriver().getConnectionProperties();
for (Map.Entry<Object,Object> prop : driverProperties.entrySet()) {
connectProps.setProperty(CommonUtils.toString(prop.getKey()), CommonUtils.toString(prop.getValue()));
}
}
DBPConnectionConfiguration connectionInfo = container.getActualConnectionConfiguration();
for (Map.Entry<String, String> prop : connectionInfo.getProperties().entrySet()) {
connectProps.setProperty(CommonUtils.toString(prop.getKey()), CommonUtils.toString(prop.getValue()));
}
if (!CommonUtils.isEmpty(connectionInfo.getUserName())) {
connectProps.put(DBConstants.DATA_SOURCE_PROPERTY_USER, getConnectionUserName(connectionInfo));
}
if (!CommonUtils.isEmpty(connectionInfo.getUserPassword())) {
connectProps.put(DBConstants.DATA_SOURCE_PROPERTY_PASSWORD, getConnectionUserPassword(connectionInfo));
}
// Obtain connection
try {
final String url = getConnectionURL(connectionInfo);
if (driverInstance != null) {
try {
if (!driverInstance.acceptsURL(url)) {
// Just write a warning in log. Some drivers are poorly coded and always returns false here.
log.error("Bad URL: " + url);
}
} catch (Throwable e) {
log.debug("Error in " + driverInstance.getClass().getName() + ".acceptsURL() - " + url, e);
}
}
monitor.subTask("Connecting " + purpose);
Connection connection;
if (driverInstance == null) {
connection = DriverManager.getConnection(url, connectProps);
} else {
connection = driverInstance.connect(url, connectProps);
}
if (connection == null) {
throw new DBCException("Null connection returned");
}
// Set read-only flag
if (container.isConnectionReadOnly() && !isConnectionReadOnlyBroken()) {
connection.setReadOnly(true);
}
return connection;
}
catch (SQLException ex) {
throw new DBCConnectException(ex.getMessage(), ex, this);
}
catch (DBCException ex) {
throw ex;
}
catch (Throwable e) {
throw new DBCConnectException("Unexpected driver error occurred while connecting to database", e);
}
}
protected String getConnectionURL(DBPConnectionConfiguration connectionInfo) {
return connectionInfo.getUrl();
}
protected void closeConnection(final Connection connection, String purpose)
{
if (connection != null) {
// Close datasource (in async task)
RuntimeUtils.runTask(new DBRRunnableWithProgress() {
@Override
public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
try {
// If we in transaction - rollback it.
// Any valuable transaction changes should be committed by UI
// so here we do it just in case to avoid error messages on close with open transaction
if (!connection.getAutoCommit()) {
connection.rollback();
}
} catch (Throwable e) {
// Do not write warning because connection maybe broken before the moment of close
log.debug("Error closing transaction", e);
}
try {
connection.close();
}
catch (Throwable ex) {
log.error("Error closing connection", ex);
}
}
}, "Close JDBC connection (" + purpose + ")", DISCONNECT_TIMEOUT);
}
}
/*
@Override
public JDBCSession openSession(DBRProgressMonitor monitor, DBCExecutionPurpose purpose, String taskTitle)
{
if (metaContext != null && (purpose == DBCExecutionPurpose.META || purpose == DBCExecutionPurpose.META_DDL)) {
return createConnection(monitor, this.metaContext, purpose, taskTitle);
}
return createConnection(monitor, executionContext, purpose, taskTitle);
}
*/
@NotNull
@Override
public DBCExecutionContext openIsolatedContext(@NotNull DBRProgressMonitor monitor, @NotNull String purpose) throws DBException
{
JDBCExecutionContext context = new JDBCExecutionContext(this, purpose);
context.connect(monitor, null, null, true, true);
return context;
}
protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, boolean setActiveObject) throws DBCException {
}
@NotNull
protected JDBCConnectionImpl createConnection(
@NotNull DBRProgressMonitor monitor,
@NotNull JDBCExecutionContext context,
@NotNull DBCExecutionPurpose purpose,
@NotNull String taskTitle)
{
return new JDBCConnectionImpl(context, monitor, purpose, taskTitle);
}
@NotNull
@Override
public DBPDataSourceContainer getContainer()
{
return container;
}
@NotNull
@Override
public DBPDataSourceInfo getInfo()
{
return dataSourceInfo;
}
@NotNull
@Override
public SQLDialect getSQLDialect() {
return sqlDialect;
}
@NotNull
public JDBCFactory getJdbcFactory() {
return jdbcFactory;
}
@NotNull
@Override
public synchronized JDBCExecutionContext getDefaultContext(boolean meta) {
if (metaContext != null && meta) {
return this.metaContext;
}
return executionContext;
}
@NotNull
@Override
public JDBCExecutionContext[] getAllContexts() {
synchronized (allContexts) {
return allContexts.toArray(new JDBCExecutionContext[allContexts.size()]);
}
}
void addContext(JDBCExecutionContext context) {
synchronized (allContexts) {
allContexts.add(context);
}
}
boolean removeContext(JDBCExecutionContext context) {
synchronized (allContexts) {
return allContexts.remove(context);
}
}
@Override
public void initialize(@NotNull DBRProgressMonitor monitor)
throws DBException
{
if (!container.getDriver().isEmbedded() && container.getPreferenceStore().getBoolean(ModelPreferences.META_SEPARATE_CONNECTION)) {
synchronized (allContexts) {
this.metaContext = new JDBCExecutionContext(this, "Metadata");
this.metaContext.connect(monitor, true, null, false, true);
}
}
try (JDBCSession session = DBUtils.openMetaSession(monitor, this, ModelMessages.model_jdbc_read_database_meta_data)) {
JDBCDatabaseMetaData metaData = session.getMetaData();
if (this.sqlDialect instanceof JDBCSQLDialect) {
try {
((JDBCSQLDialect) this.sqlDialect).initDriverSettings(this, metaData);
} catch (Throwable e) {
log.error("Error initializing dialect driver settings", e);
}
}
try {
databaseMajorVersion = metaData.getDatabaseMajorVersion();
databaseMinorVersion = metaData.getDatabaseMinorVersion();
} catch (Throwable e) {
log.error("Error determining server version", e);
}
try {
dataSourceInfo = createDataSourceInfo(metaData);
} catch (Throwable e) {
log.error("Error obtaining database info");
}
} catch (SQLException ex) {
throw new DBException("Error getting JDBC meta data", ex, this);
} finally {
if (dataSourceInfo == null) {
log.warn("NULL datasource info was created");
dataSourceInfo = new JDBCDataSourceInfo(container);
}
}
}
@Override
public void shutdown(DBRProgressMonitor monitor)
{
// [JDBC] Need sync here because real connection close could take some time
// while UI may invoke callbacks to operate with connection
synchronized (allContexts) {
List<JDBCExecutionContext> ctxCopy = new ArrayList<>(allContexts);
for (JDBCExecutionContext context : ctxCopy) {
monitor.subTask("Close context '" + context.getContextName() + "'");
context.close();
monitor.worked(1);
}
}
}
public boolean isServerVersionAtLeast(int major, int minor) {
if (databaseMajorVersion < major) {
return false;
} else if (databaseMajorVersion == major && databaseMinorVersion < minor) {
return false;
}
return true;
}
@NotNull
@Override
@Property(viewable = true, order = 1)
public String getName()
{
return container.getName();
}
@Nullable
@Override
public String getDescription()
{
return container.getDescription();
}
@Override
public DBSObject getParentObject()
{
return container;
}
@Override
public boolean isPersisted()
{
return true;
}
@Override
public DBSObject refreshObject(@NotNull DBRProgressMonitor monitor) throws DBException {
this.dataSourceInfo = new JDBCDataSourceInfo(container);
return this;
}
@Nullable
@Override
public DBCQueryTransformer createQueryTransformer(@NotNull DBCQueryTransformType type)
{
// if (type == DBCQueryTransformType.ORDER_BY) {
//
// } else if (type == DBCQueryTransformType.FILTER) {
//
// }
return null;
}
private static int getValueTypeByTypeName(@NotNull String typeName, int valueType)
{
// [JDBC: SQLite driver uses VARCHAR value type for all LOBs]
if (valueType == Types.OTHER || valueType == Types.VARCHAR) {
if ("BLOB".equalsIgnoreCase(typeName)) {
return Types.BLOB;
} else if ("CLOB".equalsIgnoreCase(typeName)) {
return Types.CLOB;
} else if ("NCLOB".equalsIgnoreCase(typeName)) {
return Types.NCLOB;
}
} else if (valueType == Types.BIT) {
// Workaround for MySQL (and maybe others) when TINYINT(1) == BOOLEAN
if ("TINYINT".equalsIgnoreCase(typeName)) {
return Types.TINYINT;
}
}
return valueType;
}
@NotNull
public DBPDataKind resolveDataKind(@NotNull String typeName, int valueType)
{
return getDataKind(typeName, valueType);
}
@NotNull
public static DBPDataKind getDataKind(@NotNull String typeName, int valueType)
{
switch (getValueTypeByTypeName(typeName, valueType)) {
case Types.BOOLEAN:
return DBPDataKind.BOOLEAN;
case Types.CHAR:
case Types.VARCHAR:
case Types.NVARCHAR:
return DBPDataKind.STRING;
case Types.BIGINT:
case Types.DECIMAL:
case Types.DOUBLE:
case Types.FLOAT:
case Types.INTEGER:
case Types.NUMERIC:
case Types.REAL:
case Types.SMALLINT:
return DBPDataKind.NUMERIC;
case Types.BIT:
case Types.TINYINT:
if (typeName.toLowerCase().contains("bool")) {
// Declared as numeric but actually it's a boolean
return DBPDataKind.BOOLEAN;
}
return DBPDataKind.NUMERIC;
case Types.DATE:
case Types.TIME:
case Types.TIME_WITH_TIMEZONE:
case Types.TIMESTAMP:
case Types.TIMESTAMP_WITH_TIMEZONE:
return DBPDataKind.DATETIME;
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
return DBPDataKind.BINARY;
case Types.BLOB:
case Types.CLOB:
case Types.NCLOB:
case Types.LONGVARCHAR:
case Types.LONGNVARCHAR:
return DBPDataKind.CONTENT;
case Types.SQLXML:
return DBPDataKind.CONTENT;
case Types.STRUCT:
return DBPDataKind.STRUCT;
case Types.ARRAY:
return DBPDataKind.ARRAY;
case Types.ROWID:
return DBPDataKind.ROWID;
case Types.REF:
return DBPDataKind.REFERENCE;
case Types.OTHER:
// TODO: really?
return DBPDataKind.OBJECT;
}
return DBPDataKind.UNKNOWN;
}
@Nullable
@Override
public DBSDataType resolveDataType(@NotNull DBRProgressMonitor monitor, @NotNull String typeFullName) throws DBException
{
return getLocalDataType(typeFullName);
}
@Override
public String getDefaultDataTypeName(@NotNull DBPDataKind dataKind)
{
switch (dataKind) {
case BOOLEAN: return "BOOLEAN";
case NUMERIC: return "NUMERIC";
case STRING: return "VARCHAR";
case DATETIME: return "TIMESTAMP";
case BINARY: return "BLOB";
case CONTENT: return "BLOB";
case STRUCT: return "VARCHAR";
case ARRAY: return "VARCHAR";
case OBJECT: return "VARCHAR";
case REFERENCE: return "VARCHAR";
case ROWID: return "ROWID";
case ANY: return "VARCHAR";
default: return "VARCHAR";
}
}
/////////////////////////////////////////////////
// Overridable functions
protected boolean isConnectionReadOnlyBroken() {
return false;
}
protected String getConnectionUserName(@NotNull DBPConnectionConfiguration connectionInfo)
{
return connectionInfo.getUserName();
}
protected String getConnectionUserPassword(@NotNull DBPConnectionConfiguration connectionInfo)
{
return connectionInfo.getUserPassword();
}
@Nullable
protected Driver getDriverInstance(@NotNull DBRProgressMonitor monitor)
throws DBException
{
return Driver.class.cast(
container.getDriver().getDriverInstance(monitor));
}
/**
* Could be overridden by extenders. May contain any additional connection properties.
* Note: these properties may be overwritten by connection advanced properties.
* @return predefined connection properties
*/
@Nullable
protected Map<String, String> getInternalConnectionProperties(DBRProgressMonitor monitor, String purpose)
throws DBCException
{
return null;
}
protected DBPDataSourceInfo createDataSourceInfo(@NotNull JDBCDatabaseMetaData metaData)
{
return new JDBCDataSourceInfo(metaData);
}
@NotNull
protected JDBCFactory createJdbcFactory() {
return new JDBCFactoryDefault();
}
/////////////////////////////////////////////////
// Error assistance
@Override
public ErrorType discoverErrorType(@NotNull Throwable error)
{
String sqlState = SQLState.getStateFromException(error);
if (sqlState != null) {
if (SQLState.SQL_08000.getCode().equals(sqlState) ||
SQLState.SQL_08003.getCode().equals(sqlState) ||
SQLState.SQL_08006.getCode().equals(sqlState) ||
SQLState.SQL_08007.getCode().equals(sqlState) ||
SQLState.SQL_08S01.getCode().equals(sqlState)) {
return ErrorType.CONNECTION_LOST;
}
}
if (GeneralUtils.getRootCause(error) instanceof SocketException) {
return ErrorType.CONNECTION_LOST;
}
if (error instanceof DBCConnectException) {
Throwable rootCause = GeneralUtils.getRootCause(error);
if (rootCause instanceof ClassNotFoundException) {
// Looks like bad driver configuration
return ErrorType.DRIVER_CLASS_MISSING;
}
}
return ErrorType.NORMAL;
}
@Nullable
@Override
public ErrorPosition[] getErrorPosition(@NotNull DBRProgressMonitor monitor, @NotNull DBCExecutionContext context, @NotNull String query, @NotNull Throwable error) {
return null;
}
@Override
public <T> T getAdapter(Class<T> adapter) {
if (adapter == DBCTransactionManager.class) {
return adapter.cast(executionContext);
}
return null;
}
/////////////////////////////////////////////////
// DBDPreferences
@Override
public DBDDataFormatterProfile getDataFormatterProfile() {
return container.getDataFormatterProfile();
}
@Override
public void setDataFormatterProfile(DBDDataFormatterProfile formatterProfile) {
container.setDataFormatterProfile(formatterProfile);
}
@NotNull
@Override
public DBDValueHandler getDefaultValueHandler() {
return JDBCObjectValueHandler.INSTANCE;
}
}