/* * 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.ext.mysql.model; 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.ext.mysql.MySQLConstants; import org.jkiss.dbeaver.ext.mysql.MySQLDataSourceProvider; import org.jkiss.dbeaver.ext.mysql.MySQLUtils; import org.jkiss.dbeaver.ext.mysql.model.plan.MySQLPlanAnalyser; import org.jkiss.dbeaver.ext.mysql.model.session.MySQLSessionManager; import org.jkiss.dbeaver.model.DBPDataSourceContainer; import org.jkiss.dbeaver.model.DBPDataSourceInfo; import org.jkiss.dbeaver.model.DBPErrorAssistant; import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.admin.sessions.DBAServerSessionManager; import org.jkiss.dbeaver.model.app.DBACertificateStorage; import org.jkiss.dbeaver.model.exec.*; import org.jkiss.dbeaver.model.exec.jdbc.*; import org.jkiss.dbeaver.model.exec.plan.DBCPlan; import org.jkiss.dbeaver.model.exec.plan.DBCPlanStyle; import org.jkiss.dbeaver.model.exec.plan.DBCQueryPlanner; import org.jkiss.dbeaver.model.impl.jdbc.JDBCDataSource; import org.jkiss.dbeaver.model.impl.jdbc.JDBCExecutionContext; import org.jkiss.dbeaver.model.impl.jdbc.JDBCUtils; import org.jkiss.dbeaver.model.impl.jdbc.cache.JDBCBasicDataTypeCache; import org.jkiss.dbeaver.model.impl.jdbc.cache.JDBCObjectCache; import org.jkiss.dbeaver.model.impl.sql.QueryTransformerLimit; import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.sql.SQLHelpProvider; import org.jkiss.dbeaver.model.sql.SQLState; import org.jkiss.dbeaver.model.struct.*; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; import java.io.File; import java.net.MalformedURLException; import java.sql.Connection; import java.sql.SQLException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * GenericDataSource */ public class MySQLDataSource extends JDBCDataSource implements DBSObjectSelector, DBCQueryPlanner, IAdaptable { private static final Log log = Log.getLog(MySQLDataSource.class); private final JDBCBasicDataTypeCache dataTypeCache; private List<MySQLEngine> engines; private final CatalogCache catalogCache = new CatalogCache(); private List<MySQLPrivilege> privileges; private List<MySQLUser> users; private List<MySQLCharset> charsets; private Map<String, MySQLCollation> collations; private String activeCatalogName; private SQLHelpProvider helpProvider; public MySQLDataSource(DBRProgressMonitor monitor, DBPDataSourceContainer container) throws DBException { super(monitor, container, new MySQLDialect()); dataTypeCache = new JDBCBasicDataTypeCache(container); } @Override protected Map<String, String> getInternalConnectionProperties(DBRProgressMonitor monitor, String purpose) throws DBCException { Map<String, String> props = new LinkedHashMap<>(MySQLDataSourceProvider.getConnectionsProps()); final DBWHandlerConfiguration sslConfig = getContainer().getActualConnectionConfiguration().getDeclaredHandler(MySQLConstants.HANDLER_SSL); if (sslConfig != null && sslConfig.isEnabled()) { try { initSSL(monitor, props, sslConfig); } catch (Exception e) { throw new DBCException("Error configuring SSL certificates", e); } } else { // Newer MySQL servers/connectors requires explicit SSL disable props.put("useSSL", "false"); } /* if (CommonUtils.toBoolean(connectionInfo.getProperty(MySQLConstants.PROP_USE_SSL))) { url.append("?useSSL=true&requireSSL=true"); } */ return props; } @Override protected DBPDataSourceInfo createDataSourceInfo(@NotNull JDBCDatabaseMetaData metaData) { return new MySQLDataSourceInfo(metaData); } private void initSSL(DBRProgressMonitor monitor, Map<String, String> props, DBWHandlerConfiguration sslConfig) throws Exception { monitor.subTask("Install SSL certificates"); final DBACertificateStorage securityManager = getContainer().getPlatform().getCertificateStorage(); props.put("useSSL", "true"); props.put("verifyServerCertificate", String.valueOf(CommonUtils.toBoolean(sslConfig.getProperties().get(MySQLConstants.PROP_VERIFY_SERVER_SERT)))); props.put("requireSSL", String.valueOf(CommonUtils.toBoolean(sslConfig.getProperties().get(MySQLConstants.PROP_REQUIRE_SSL)))); final String caCertProp = sslConfig.getProperties().get(MySQLConstants.PROP_SSL_CA_CERT); final String clientCertProp = sslConfig.getProperties().get(MySQLConstants.PROP_SSL_CLIENT_CERT); final String clientCertKeyProp = sslConfig.getProperties().get(MySQLConstants.PROP_SSL_CLIENT_KEY); { // Trust keystore if (!CommonUtils.isEmpty(caCertProp) || !CommonUtils.isEmpty(clientCertProp)) { byte[] caCertData = CommonUtils.isEmpty(caCertProp) ? null : IOUtils.readFileToBuffer(new File(caCertProp)); byte[] clientCertData = CommonUtils.isEmpty(clientCertProp) ? null : IOUtils.readFileToBuffer(new File(clientCertProp)); byte[] keyData = CommonUtils.isEmpty(clientCertKeyProp) ? null : IOUtils.readFileToBuffer(new File(clientCertKeyProp)); securityManager.addCertificate(getContainer(), "ssl", caCertData, clientCertData, keyData); } else { securityManager.deleteCertificate(getContainer(), "ssl"); } final String ksPath = makeKeyStorePath(securityManager.getKeyStorePath(getContainer(), "ssl")); props.put("clientCertificateKeyStoreUrl", ksPath); props.put("trustCertificateKeyStoreUrl", ksPath); } final String cipherSuites = sslConfig.getProperties().get(MySQLConstants.PROP_SSL_CIPHER_SUITES); if (!CommonUtils.isEmpty(cipherSuites)) { props.put("enabledSSLCipherSuites;", cipherSuites); } final boolean retrievePublicKey = CommonUtils.getBoolean(sslConfig.getProperties().get(MySQLConstants.PROP_SSL_PUBLIC_KEY_RETRIEVE), false); if (retrievePublicKey) { props.put("allowPublicKeyRetrieval", "true"); } if (CommonUtils.getBoolean(sslConfig.getProperties().get(MySQLConstants.PROP_SSL_DEBUG), false)) { System.setProperty("javax.net.debug", "all"); } } private String makeKeyStorePath(File keyStorePath) throws MalformedURLException { if (isMariaDB()) { return keyStorePath.getAbsolutePath(); } else { return keyStorePath.toURI().toURL().toString(); } } protected void initializeContextState(@NotNull DBRProgressMonitor monitor, @NotNull JDBCExecutionContext context, boolean setActiveObject) throws DBCException { if (setActiveObject) { MySQLCatalog object = getDefaultObject(); if (object != null) { useDatabase(monitor, context, object); } } } public String[] getTableTypes() { return MySQLConstants.TABLE_TYPES; } public CatalogCache getCatalogCache() { return catalogCache; } public Collection<MySQLCatalog> getCatalogs() { return catalogCache.getCachedObjects(); } public MySQLCatalog getCatalog(String name) { return catalogCache.getCachedObject(name); } @Override public void initialize(@NotNull DBRProgressMonitor monitor) throws DBException { super.initialize(monitor); dataTypeCache.getAllObjects(monitor, this); try (JDBCSession session = DBUtils.openMetaSession(monitor, this, "Load basic datasource metadata")) { // Read engines { engines = new ArrayList<>(); try (JDBCPreparedStatement dbStat = session.prepareStatement("SHOW ENGINES")) { try (JDBCResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { MySQLEngine engine = new MySQLEngine(this, dbResult); engines.add(engine); } } } catch (SQLException ex) { // Engines are not supported. Shame on it. Leave this list empty } } // Read charsets and collations { charsets = new ArrayList<>(); try (JDBCPreparedStatement dbStat = session.prepareStatement("SHOW CHARSET")) { try (JDBCResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { MySQLCharset charset = new MySQLCharset(this, dbResult); charsets.add(charset); } } } catch (SQLException ex) { // Engines are not supported. Shame on it. Leave this list empty } Collections.sort(charsets, DBUtils.<MySQLCharset>nameComparator()); collations = new LinkedHashMap<>(); try (JDBCPreparedStatement dbStat = session.prepareStatement("SHOW COLLATION")) { try (JDBCResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { String charsetName = JDBCUtils.safeGetString(dbResult, MySQLConstants.COL_CHARSET); MySQLCharset charset = getCharset(charsetName); if (charset == null) { log.warn("Charset '" + charsetName + "' not found."); continue; } MySQLCollation collation = new MySQLCollation(charset, dbResult); collations.put(collation.getName(), collation); charset.addCollation(collation); } } } catch (SQLException ex) { // Engines are not supported. Shame on it. Leave this list empty } } // Read catalogs catalogCache.getAllObjects(monitor, this); activeCatalogName = MySQLUtils.determineCurrentDatabase(session); } } @Override public DBSObject refreshObject(@NotNull DBRProgressMonitor monitor) throws DBException { super.refreshObject(monitor); this.engines = null; this.catalogCache.clearCache(); this.users = null; this.activeCatalogName = null; this.initialize(monitor); return this; } MySQLTable findTable(DBRProgressMonitor monitor, String catalogName, String tableName) throws DBException { if (CommonUtils.isEmpty(catalogName)) { return null; } MySQLCatalog catalog = getCatalog(catalogName); if (catalog == null) { log.error("Catalog " + catalogName + " not found"); return null; } return catalog.getTable(monitor, tableName); } @Override public Collection<? extends MySQLCatalog> getChildren(@NotNull DBRProgressMonitor monitor) throws DBException { return getCatalogs(); } @Override public MySQLCatalog getChild(@NotNull DBRProgressMonitor monitor, @NotNull String childName) throws DBException { return getCatalog(childName); } @Override public Class<? extends MySQLCatalog> getChildType(@NotNull DBRProgressMonitor monitor) throws DBException { return MySQLCatalog.class; } @Override public void cacheStructure(@NotNull DBRProgressMonitor monitor, int scope) throws DBException { } @Override public boolean supportsDefaultChange() { return true; } @Override public MySQLCatalog getDefaultObject() { return CommonUtils.isEmpty(activeCatalogName) ? null : getCatalog(activeCatalogName); } @Override public void setDefaultObject(@NotNull DBRProgressMonitor monitor, @NotNull DBSObject object) throws DBException { final MySQLCatalog oldSelectedEntity = getDefaultObject(); if (!(object instanceof MySQLCatalog)) { throw new DBException("Invalid object type: " + object); } for (JDBCExecutionContext context : getAllContexts()) { useDatabase(monitor, context, (MySQLCatalog) object); } activeCatalogName = object.getName(); // Send notifications if (oldSelectedEntity != null) { DBUtils.fireObjectSelect(oldSelectedEntity, false); } if (this.activeCatalogName != null) { DBUtils.fireObjectSelect(object, true); } } @Override public boolean refreshDefaultObject(@NotNull DBCSession session) throws DBException { final String newCatalogName = MySQLUtils.determineCurrentDatabase((JDBCSession) session); if (!CommonUtils.equalObjects(newCatalogName, activeCatalogName)) { final MySQLCatalog newCatalog = getCatalog(newCatalogName); if (newCatalog != null) { setDefaultObject(session.getProgressMonitor(), newCatalog); return true; } } return false; } private void useDatabase(DBRProgressMonitor monitor, JDBCExecutionContext context, MySQLCatalog catalog) throws DBCException { if (catalog == null) { log.debug("Null current database"); return; } try (JDBCSession session = context.openSession(monitor, DBCExecutionPurpose.UTIL, "Set active catalog")) { try (JDBCPreparedStatement dbStat = session.prepareStatement("use " + DBUtils.getQuotedIdentifier(catalog))) { dbStat.execute(); } } catch (SQLException e) { throw new DBCException(e, this); } } @Override protected Connection openConnection(@NotNull DBRProgressMonitor monitor, @NotNull String purpose) throws DBCException { Connection mysqlConnection = super.openConnection(monitor, purpose); { // Provide client info try { mysqlConnection.setClientInfo("ApplicationName", DBUtils.getClientApplicationName(getContainer(), purpose)); } catch (Throwable e) { // just ignore log.debug(e); } } return mysqlConnection; } public List<MySQLUser> getUsers(DBRProgressMonitor monitor) throws DBException { if (users == null) { users = loadUsers(monitor); } return users; } public MySQLUser getUser(DBRProgressMonitor monitor, String name) throws DBException { return DBUtils.findObject(getUsers(monitor), name); } private List<MySQLUser> loadUsers(DBRProgressMonitor monitor) throws DBException { try (JDBCSession session = DBUtils.openMetaSession(monitor, this, "Load users")) { try (JDBCPreparedStatement dbStat = session.prepareStatement("SELECT * FROM mysql.user ORDER BY user")) { try (JDBCResultSet dbResult = dbStat.executeQuery()) { List<MySQLUser> userList = new ArrayList<>(); while (dbResult.next()) { MySQLUser user = new MySQLUser(this, dbResult); userList.add(user); } return userList; } } } catch (SQLException ex) { throw new DBException(ex, this); } } public List<MySQLEngine> getEngines() { return engines; } public MySQLEngine getEngine(String name) { return DBUtils.findObject(engines, name); } public MySQLEngine getDefaultEngine() { for (MySQLEngine engine : engines) { if (engine.getSupport() == MySQLEngine.Support.DEFAULT) { return engine; } } return null; } public Collection<MySQLCharset> getCharsets() { return charsets; } public MySQLCharset getCharset(String name) { for (MySQLCharset charset : charsets) { if (charset.getName().equals(name)) { return charset; } } return null; } public MySQLCollation getCollation(String name) { return collations.get(name); } public List<MySQLPrivilege> getPrivileges(DBRProgressMonitor monitor) throws DBException { if (privileges == null) { privileges = loadPrivileges(monitor); } return privileges; } public List<MySQLPrivilege> getPrivilegesByKind(DBRProgressMonitor monitor, MySQLPrivilege.Kind kind) throws DBException { List<MySQLPrivilege> privs = new ArrayList<>(); for (MySQLPrivilege priv : getPrivileges(monitor)) { if (priv.getKind() == kind) { privs.add(priv); } } return privs; } public MySQLPrivilege getPrivilege(DBRProgressMonitor monitor, String name) throws DBException { return DBUtils.findObject(getPrivileges(monitor), name); } private List<MySQLPrivilege> loadPrivileges(DBRProgressMonitor monitor) throws DBException { try (JDBCSession session = DBUtils.openMetaSession(monitor, this, "Load privileges")) { try (JDBCPreparedStatement dbStat = session.prepareStatement("SHOW PRIVILEGES")) { try (JDBCResultSet dbResult = dbStat.executeQuery()) { List<MySQLPrivilege> privileges = new ArrayList<>(); while (dbResult.next()) { MySQLPrivilege user = new MySQLPrivilege(this, dbResult); privileges.add(user); } return privileges; } } } catch (SQLException ex) { throw new DBException(ex, this); } } public List<MySQLParameter> getSessionStatus(DBRProgressMonitor monitor) throws DBException { return loadParameters(monitor, true, false); } public List<MySQLParameter> getGlobalStatus(DBRProgressMonitor monitor) throws DBException { return loadParameters(monitor, true, true); } public List<MySQLParameter> getSessionVariables(DBRProgressMonitor monitor) throws DBException { return loadParameters(monitor, false, false); } public List<MySQLParameter> getGlobalVariables(DBRProgressMonitor monitor) throws DBException { return loadParameters(monitor, false, true); } public List<MySQLDataSource> getInformation() { return Collections.singletonList(this); } private List<MySQLParameter> loadParameters(DBRProgressMonitor monitor, boolean status, boolean global) throws DBException { try (JDBCSession session = DBUtils.openMetaSession(monitor, this, "Load status")) { try (JDBCPreparedStatement dbStat = session.prepareStatement( "SHOW " + (global ? "GLOBAL " : "") + (status ? "STATUS" : "VARIABLES"))) { try (JDBCResultSet dbResult = dbStat.executeQuery()) { List<MySQLParameter> parameters = new ArrayList<>(); while (dbResult.next()) { MySQLParameter parameter = new MySQLParameter( this, JDBCUtils.safeGetString(dbResult, "variable_name"), JDBCUtils.safeGetString(dbResult, "value")); parameters.add(parameter); } return parameters; } } } catch (SQLException ex) { throw new DBException(ex, this); } } @Override public DBCQueryTransformer createQueryTransformer(@NotNull DBCQueryTransformType type) { if (type == DBCQueryTransformType.RESULT_SET_LIMIT) { return new QueryTransformerLimit(); } else if (type == DBCQueryTransformType.FETCH_ALL_TABLE) { return new QueryTransformerFetchAll(); } return super.createQueryTransformer(type); } @NotNull @Override public DBCPlan planQueryExecution(@NotNull DBCSession session, @NotNull String query) throws DBCException { MySQLPlanAnalyser plan = new MySQLPlanAnalyser(this, query); plan.explain(session); return plan; } @NotNull @Override public DBCPlanStyle getPlanStyle() { return DBCPlanStyle.PLAN; } @Override public <T> T getAdapter(Class<T> adapter) { if (adapter == DBSStructureAssistant.class) { return adapter.cast(new MySQLStructureAssistant(this)); } else if (adapter == SQLHelpProvider.class) { if (helpProvider == null) { helpProvider = new MySQLHelpProvider(this); } return adapter.cast(helpProvider); } else if (adapter == DBAServerSessionManager.class) { return adapter.cast(new MySQLSessionManager(this)); } return super.getAdapter(adapter); } @NotNull @Override public MySQLDataSource getDataSource() { return this; } @Override public Collection<? extends DBSDataType> getLocalDataTypes() { return dataTypeCache.getCachedObjects(); } @Override public DBSDataType getLocalDataType(String typeName) { return dataTypeCache.getCachedObject(typeName); } static class CatalogCache extends JDBCObjectCache<MySQLDataSource, MySQLCatalog> { @Override protected JDBCStatement prepareObjectsStatement(@NotNull JDBCSession session, @NotNull MySQLDataSource owner) throws SQLException { StringBuilder catalogQuery = new StringBuilder("SELECT * FROM " + MySQLConstants.META_TABLE_SCHEMATA); DBSObjectFilter catalogFilters = owner.getContainer().getObjectFilter(MySQLCatalog.class, null, false); if (catalogFilters != null) { JDBCUtils.appendFilterClause(catalogQuery, catalogFilters, MySQLConstants.COL_SCHEMA_NAME, true); } JDBCPreparedStatement dbStat = session.prepareStatement(catalogQuery.toString()); if (catalogFilters != null) { JDBCUtils.setFilterParameters(dbStat, 1, catalogFilters); } return dbStat; } @Override protected MySQLCatalog fetchObject(@NotNull JDBCSession session, @NotNull MySQLDataSource owner, @NotNull JDBCResultSet resultSet) throws SQLException, DBException { return new MySQLCatalog(owner, resultSet); } } public boolean isMariaDB() { return MySQLConstants.DRIVER_CLASS_MARIA_DB.equals( getContainer().getDriver().getDriverClassName()); } @Override public ErrorType discoverErrorType(@NotNull Throwable error) { if (isMariaDB()) { // MariaDB-specific. They have bad SQLState support if ("08".equals(SQLState.getStateFromException(error))) { return ErrorType.CONNECTION_LOST; } } return super.discoverErrorType(error); } private Pattern ERROR_POSITION_PATTERN = Pattern.compile(" at line ([0-9]+)"); @Nullable @Override public ErrorPosition[] getErrorPosition(@NotNull DBRProgressMonitor monitor, @NotNull DBCExecutionContext context, @NotNull String query, @NotNull Throwable error) { String message = error.getMessage(); if (!CommonUtils.isEmpty(message)) { Matcher matcher = ERROR_POSITION_PATTERN.matcher(message); if (matcher.find()) { DBPErrorAssistant.ErrorPosition pos = new DBPErrorAssistant.ErrorPosition(); pos.line = Integer.parseInt(matcher.group(1)) - 1; return new ErrorPosition[] { pos }; } } return null; } }