/* * Copyright 2008-2014 by Emeric Vernat * * This file is part of Java Melody. * * 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 net.bull.javamelody; import java.io.Serializable; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.ResourceBundle; import javax.naming.NamingException; import javax.sql.DataSource; /** * Informations sur la base de données. * @author Emeric Vernat */ class DatabaseInformations implements Serializable { private static final long serialVersionUID = -6105478981257689782L; static enum Database { // base de données connues avec les noms retournés par connection.getMetaData().getDatabaseProductName() // (inspirés par Hibernate) POSTGRESQL("PostgreSQL"), MYSQL("MySQL"), MYSQL4("MySQL"), ORACLE("Oracle"), DB2("DB2 UDB for AS/400", "DB2/"), H2("H2"), HSQLDB("HSQL Database Engine"), SQLSERVER("Microsoft SQL Server"), SYBASE("Sybase SQL Server", "Adaptive Server Enterprise"); // RESOURCE_BUNDLE_BASE_NAME vaut "net.bull.javamelody.resource.databaseInformations" // ce qui charge net.bull.javamelody.resource.databaseInformations.properties // (Parameters.getResourcePath("databaseInformations") seul ne fonctionne pas si on est dans un jar/war) private static final String RESOURCE_BUNDLE_BASE_NAME = Parameters .getResourcePath("databaseInformations").replace('/', '.').substring(1); private List<String> databaseNames; private Database(String... databaseNames) { this.databaseNames = Arrays.asList(databaseNames); } List<String> getRequestNames() { final List<String> tmp; switch (this) { case POSTGRESQL: tmp = Arrays.asList("pg_stat_activity", "pg_locks", "pg_database", "pg_tablespace", "pg_stat_database", "pg_stat_user_tables", "pg_stat_user_indexes", "pg_statio_user_tables", "pg_statio_user_indexes", "pg_statio_user_sequences", "pg_settings"); break; case MYSQL: case MYSQL4: // les noms des requêtes sont les mêmes, mais la requête SQL correspondant à "innodb_status" // n'est pas identique entre MYSQL 5+ et MYSQL 4 (issue 195) tmp = Arrays.asList("processlist", "databases", "variables", "global_status", "innodb_status"); break; case ORACLE: tmp = Arrays .asList("sessions", "locks", "sqlTimes", "foreignKeysWithoutIndexes", "invalidObjects", "disabledConstraints", "instance", "database", "nlsParameters", "tablespaceFreespace", "datafileIo", "tablespaceExtents", "ratios", "parameters", "rollbackSegmentStatistics", "statistics", "events"); break; case DB2: tmp = Arrays.asList("mon_current_sql", "mon_db_summary", "mon_lockwaits", "mon_service_subclass_summary", "mon_current_uow", "mon_workload_summary", "mon_get_connection", "current_queries"); break; case H2: tmp = Arrays.asList("memory", "sessions", "locks", "settings"); break; case HSQLDB: tmp = Arrays.asList("system_sessions", "system_cacheinfo", "system_properties", "system_schemas"); break; case SQLSERVER: tmp = Arrays.asList("version", "connections"); break; case SYBASE: tmp = Arrays.asList("sp_who", "connections", "sp_lock", "lock", "running_stored_procedure", "used_temporary_tables", "used_tables", "sp_version"); break; default: throw new IllegalStateException(); } return addPrefix(tmp); } private List<String> addPrefix(List<String> requestNames) { final List<String> result = new ArrayList<String>(requestNames.size()); final String prefix = this.toString().toLowerCase(Locale.ENGLISH) + '.'; for (final String requestName : requestNames) { result.add(prefix + requestName); } return result; } String getUrlIdentifier() { if (this == MYSQL4) { return MYSQL.toString().toLowerCase(Locale.ENGLISH); } return this.toString().toLowerCase(Locale.ENGLISH); } String getRequestByName(String requestName) { return ResourceBundle.getBundle(RESOURCE_BUNDLE_BASE_NAME).getString(requestName); } List<String> getDatabaseNames() { return databaseNames; } private boolean isRecognized(String databaseName, String url) { for (final String name : getDatabaseNames()) { if (databaseName.startsWith(name)) { return true; } } if (url != null && url.contains(getUrlIdentifier())) { return true; } return false; } static Database getDatabaseForConnection(Connection connection) throws SQLException { final DatabaseMetaData metaData = connection.getMetaData(); final String databaseName = metaData.getDatabaseProductName(); final String url = metaData.getURL(); for (final Database database : Database.values()) { if (database.isRecognized(databaseName, url)) { if (database == MYSQL && metaData.getDatabaseMajorVersion() <= 4) { // si mysql et version 4 alors c'est MYSQL4 et non MYSQL return MYSQL4; } return database; } } throw new IllegalArgumentException(I18N.getFormattedString( "type_base_de_donnees_inconnu", databaseName)); } } private final Database database; @SuppressWarnings("all") private final List<String> requestNames; private final int selectedRequestIndex; private final String[][] result; DatabaseInformations(int selectedRequestIndex) throws SQLException, NamingException { super(); this.selectedRequestIndex = selectedRequestIndex; final Connection connection = getConnection(); assert connection != null; try { database = Database.getDatabaseForConnection(connection); requestNames = database.getRequestNames(); final String request = database .getRequestByName(requestNames.get(selectedRequestIndex)); result = executeRequest(connection, request, null); } finally { connection.close(); } } static int parseRequestIndex(String requestIndex) { if (requestIndex != null) { return Integer.parseInt(requestIndex); } return 0; } int getNbColumns() { final String selectedRequestName = getSelectedRequestName(); if ("oracle.statistics".equals(selectedRequestName)) { return 2; } else if ("oracle.events".equals(selectedRequestName)) { return 2; } else if ("mysql.variables".equals(selectedRequestName)) { return 2; } else if ("mysql.global_status".equals(selectedRequestName)) { return 4; } else if ("h2.settings".equals(selectedRequestName)) { return 2; } return 1; } int getSelectedRequestIndex() { return selectedRequestIndex; } String getSelectedRequestName() { return requestNames.get(getSelectedRequestIndex()); } String[][] getResult() { return result; // NOPMD } List<String> getRequestNames() { return requestNames; } private static String[][] executeRequest(Connection connection, String request, List<?> parametersValues) throws SQLException { final PreparedStatement statement = connection.prepareStatement(request); try { if (parametersValues != null) { int i = 1; for (final Object parameterValue : parametersValues) { statement.setObject(i, parameterValue); i++; } } return executeQuery(statement); } catch (final SQLException e) { if (e.getErrorCode() == 942 && e.getMessage() != null && e.getMessage().startsWith("ORA-")) { final String userName = connection.getMetaData().getUserName(); final String message = I18N.getFormattedString("oracle.grantSelectAnyDictionnary", userName); throw new SQLException(message, e); } throw e; } finally { statement.close(); } } private static String[][] executeQuery(PreparedStatement statement) throws SQLException { final ResultSet resultSet = statement.executeQuery(); try { final ResultSetMetaData metaData = resultSet.getMetaData(); final int columnCount = metaData.getColumnCount(); final List<String[]> list = new ArrayList<String[]>(); String[] values = new String[columnCount]; for (int i = 1; i <= columnCount; i++) { values[i - 1] = metaData.getColumnName(i) + '\n' + metaData.getColumnTypeName(i) + '(' + metaData.getColumnDisplaySize(i) + ')'; } list.add(values); while (resultSet.next()) { values = new String[columnCount]; for (int i = 1; i <= columnCount; i++) { values[i - 1] = resultSet.getString(i); } list.add(values); } return list.toArray(new String[list.size()][]); } finally { resultSet.close(); } } private static Connection getConnection() throws SQLException, NamingException { // on commence par voir si le driver jdbc a été utilisé // car s'il n'y a pas de datasource une exception est déclenchée if (Parameters.getLastConnectUrl() != null) { final Connection connection = DriverManager.getConnection( Parameters.getLastConnectUrl(), Parameters.getLastConnectInfo()); connection.setAutoCommit(false); return connection; } // on cherche une datasource avec InitialContext // (le nom de la dataSource recherchée dans JNDI est du genre jdbc/Xxx qui est le nom standard d'une DataSource) final Collection<DataSource> dataSources = JdbcWrapper.getJndiAndSpringDataSources() .values(); for (final DataSource dataSource : dataSources) { try { final Connection connection = dataSource.getConnection(); // on ne doit pas changer autoCommit pour la connection d'une DataSource // (ou alors il faudrait remettre l'autoCommit après, issue 189) // connection.setAutoCommit(false); return connection; } catch (final Exception e) { // si cette dataSource ne fonctionne pas, on suppose que la bonne dataSource est une des suivantes // (par exemple, sur GlassFish il y a des dataSources par défaut qui ne fonctionne pas forcément) continue; } } if (!dataSources.isEmpty()) { // this will probably throw an exception like above return dataSources.iterator().next().getConnection(); } return null; } static String explainPlanFor(String sqlRequest) throws SQLException, NamingException { final Connection connection = getConnection(); if (connection != null) { try { final Database database = Database.getDatabaseForConnection(connection); if (database == Database.ORACLE) { // Si oracle, on demande le plan d'exécution avec la table PLAN_TABLE par défaut // avec "explain plan set statement_id = <statement_id> for ..." // (si mysql ou postgresql on pourrait faire "explain ...", // sauf que les paramètres bindés ne seraient pas acceptés // et les requêtes update/insert/delete non plus). // (si db2, la syntaxe serait "explain plan for ...") // Si mysql il suffit de lire le ResultSet de executeQuery("explain ...") // qui pourrait être affiché en tableau à partir de String[][], // mais en oracle il faut aller lire la table plan_table // (http://www.java2s.com/Open-Source/Java-Document/Database-Client/squirrel-sql-2.6.5a/net/sourceforge/squirrel_sql/plugins/oracle/explainplan/ExplainPlanExecuter.java.htm) // le hashCode est une clé suffisamment unique car il y a peu de plans d'exécution // affichés simultanément, et en tout cas CounterRequest.getId() est trop long // pour la table oracle par défaut (SYS.PLAN_TABLE$.STATEMENT_ID a une longueur de 30) final String statementId = String.valueOf(sqlRequest.hashCode()); final String explainRequest = buildExplainRequest(sqlRequest, statementId); // exécution de la demande final Statement statement = connection.createStatement(); try { statement.execute(explainRequest); } finally { statement.close(); } // récupération du résultat return getPlanOutput(connection, statementId); } } finally { if (!connection.getAutoCommit()) { connection.rollback(); } connection.close(); } } return null; } private static String buildExplainRequest(String sqlRequest, String statementId) { // rq : il semble qu'une requête explain plan ne puisse avoir la requête en paramètre bindé // (donc les requêtes "explain ..." seront ignorées dans JdbcWrapper) int i = 1; String request = sqlRequest; if (Parameters.getParameter(Parameter.SQL_TRANSFORM_PATTERN) != null) { // si les requêtes SQL peuvent avoir été transformées par SQL_TRANSFORM_PATTERN, // alors on remplace le '$' par '?' en espérant avec un plan d'exécution même simplifié // (sinon, il serait impossible d'avoir un plan d'exécution pour certaines requêtes SQL // transformées par SQL_TRANSFORM_PATTERN) request = request.replace(Counter.TRANSFORM_REPLACEMENT_CHAR, '?'); } // utilisation de la table PLAN_TABLE par défaut // (il faut que cette table soit créée auparavant dans oracle // et elle peut être créée par : @$ORACLE_HOME/rdbms/admin/catplan.sql // ou par @$ORACLE_HOME/rdbms/admin/utlxplan.sql si oracle 9g ou avant) String explainRequest = "explain plan set statement_id = '" + statementId + "' for " + request; // dans le cas où la requête contient ';' (requêtes multiples), je ne sais pas si explain // plan considère que cela fait partie de la requête à analyser où si certaines versions // d'oracle considèrent que cela vient après l'explain plan; par sécurité on interdit cela if (explainRequest.indexOf(';') != -1) { explainRequest = explainRequest.substring(0, explainRequest.indexOf(';')); } // on remplace les paramètres bindés "?" par ":n" int index = explainRequest.indexOf('?'); while (index != -1) { explainRequest = explainRequest.substring(0, index) + ':' + i + explainRequest.substring(index + 1); i++; index = explainRequest.indexOf('?'); } return explainRequest; } private static String getPlanOutput(Connection connection, String statementId) throws SQLException { // table PLAN_TABLE par défaut et format par défaut final String planTableRequest = "select * from table(dbms_xplan.display(null,?, null))"; final String[][] planTableOutput = executeRequest(connection, planTableRequest, Collections.singletonList(statementId)); final StringBuilder sb = new StringBuilder(); for (final String[] row : planTableOutput) { for (final String value : row) { sb.append(value); } sb.append('\n'); } if (sb.indexOf("-") != -1) { sb.delete(0, sb.indexOf("-")); } return sb.toString(); } }