net.bull.javamelody.JdbcWrapperHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of javamelody-core Show documentation
Show all versions of javamelody-core Show documentation
Monitoring of JavaEE applications
/*
* Copyright 2008-2019 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.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameClassPair;
import javax.naming.NamingException;
import javax.naming.NoInitialContextException;
import javax.naming.Referenceable;
import javax.servlet.ServletContext;
import javax.sql.DataSource;
import org.apache.tomcat.dbcp.dbcp.BasicDataSource;
/**
* Classe utilitaire pour {@link JdbcWrapper}.
* @author Emeric Vernat
*/
final class JdbcWrapperHelper {
private static final String MAX_ACTIVE_PROPERTY_NAME = "maxActive";
private static final Map SPRING_DATASOURCES = new LinkedHashMap();
private static final Map JNDI_DATASOURCES_BACKUP = new LinkedHashMap();
private static final Map REWRAPPED_DATASOURCES_BACKUP = new LinkedHashMap();
private static final BasicDataSourcesProperties TOMCAT_BASIC_DATASOURCES_PROPERTIES = new BasicDataSourcesProperties();
private static final BasicDataSourcesProperties DBCP_BASIC_DATASOURCES_PROPERTIES = new BasicDataSourcesProperties();
private static final BasicDataSourcesProperties TOMCAT_JDBC_DATASOURCES_PROPERTIES = new BasicDataSourcesProperties();
private static final Map, Constructor>> PROXY_CACHE = Collections
.synchronizedMap(new WeakHashMap, Constructor>>());
/**
* Propriétés des BasicDataSources si elles viennent de Tomcat-DBCP ou de DBCP seul.
* @author Emeric Vernat
*/
private static class BasicDataSourcesProperties {
private final Map> properties = new LinkedHashMap>();
BasicDataSourcesProperties() {
super();
}
boolean isEmpty() {
return properties.isEmpty();
}
int getMaxActive() {
int result = 0;
for (final Map dataSourceProperties : properties.values()) {
final Integer maxActive = (Integer) dataSourceProperties
.get(MAX_ACTIVE_PROPERTY_NAME);
if (maxActive == null) {
return -1;
}
result += maxActive;
}
return result;
}
Map> getDataSourcesProperties() {
final Map> result = new LinkedHashMap>();
for (final Map.Entry> entry : properties.entrySet()) {
result.put(entry.getKey(), Collections.unmodifiableMap(entry.getValue()));
}
return Collections.unmodifiableMap(result);
}
void put(String dataSourceName, String key, Object value) {
Map dataSourceProperties = properties.get(dataSourceName);
if (dataSourceProperties == null) {
dataSourceProperties = new LinkedHashMap();
properties.put(dataSourceName, dataSourceProperties);
}
dataSourceProperties.put(key, value);
}
}
private JdbcWrapperHelper() {
super();
}
static void registerSpringDataSource(String name, DataSource dataSource) {
SPRING_DATASOURCES.put(name, dataSource);
}
static void registerRewrappedDataSource(String name, DataSource dataSource) {
REWRAPPED_DATASOURCES_BACKUP.put(name, dataSource);
}
static Map getRewrappedDataSources() {
return REWRAPPED_DATASOURCES_BACKUP;
}
static void rebindDataSource(ServletContext servletContext, String jndiName,
DataSource dataSource, DataSource dataSourceProxy) throws Throwable {
final Object lock = changeContextWritable(servletContext, null);
final InitialContext initialContext = new InitialContext();
initialContext.rebind(jndiName, dataSourceProxy);
JNDI_DATASOURCES_BACKUP.put(jndiName, dataSource);
changeContextWritable(servletContext, lock);
initialContext.close();
}
static void rebindInitialDataSources(ServletContext servletContext) throws Throwable {
try {
final InitialContext initialContext = new InitialContext();
for (final Map.Entry entry : JNDI_DATASOURCES_BACKUP.entrySet()) {
final String jndiName = entry.getKey();
final DataSource dataSource = entry.getValue();
final Object lock = changeContextWritable(servletContext, null);
initialContext.rebind(jndiName, dataSource);
changeContextWritable(servletContext, lock);
}
initialContext.close();
} finally {
JNDI_DATASOURCES_BACKUP.clear();
}
}
static Map getJndiAndSpringDataSources() throws NamingException {
Map dataSources;
try {
dataSources = new LinkedHashMap(getJndiDataSources());
} catch (final NoInitialContextException e) {
dataSources = new LinkedHashMap();
}
dataSources.putAll(SPRING_DATASOURCES);
return dataSources;
}
static Map getJndiDataSources() throws NamingException {
final Map dataSources = new LinkedHashMap(2);
final String datasourcesParameter = Parameter.DATASOURCES.getValue();
if (datasourcesParameter == null) {
dataSources.putAll(getJndiDataSourcesAt("java:comp/env/jdbc"));
// pour jboss sans jboss-env.xml ou sans resource-ref dans web.xml :
dataSources.putAll(getJndiDataSourcesAt("java:/jdbc"));
// pour JavaEE 6 :
// (voir par exemple http://smokeandice.blogspot.com/2009/12/datasourcedefinition-hidden-gem-from.html)
dataSources.putAll(getJndiDataSourcesAt("java:global/jdbc"));
// pour WebLogic 10 et WebSphere 7, cf issue 68
dataSources.putAll(getJndiDataSourcesAt("jdbc"));
} else if (!datasourcesParameter.trim().isEmpty()) {
final InitialContext initialContext = new InitialContext();
for (final String datasource : datasourcesParameter.split(",")) {
final String jndiName = datasource.trim();
// ici, on n'ajoute pas java:/comp/env
// et on suppose qu'il n'en faut pas ou que cela a été ajouté dans le paramétrage
final DataSource dataSource = (DataSource) initialContext.lookup(jndiName);
dataSources.put(jndiName, dataSource);
}
initialContext.close();
}
return Collections.unmodifiableMap(dataSources);
}
private static Map getJndiDataSourcesAt(String jndiPrefix)
throws NamingException {
final InitialContext initialContext = new InitialContext();
final Map dataSources = new LinkedHashMap(2);
try {
for (final NameClassPair nameClassPair : Collections
.list(initialContext.list(jndiPrefix))) {
// note: il ne suffit pas de tester
// (DataSource.class.isAssignableFrom(Class.forName(nameClassPair.getClassName())))
// car nameClassPair.getClassName() vaut "javax.naming.LinkRef" sous jboss 5.1.0.GA
// par exemple, donc on fait le lookup pour voir
final String jndiName;
if (nameClassPair.getName().startsWith("java:")) {
// pour glassfish v3
jndiName = nameClassPair.getName();
} else {
jndiName = jndiPrefix + '/' + nameClassPair.getName();
}
final Object value = initialContext.lookup(jndiName);
if (value instanceof DataSource) {
dataSources.put(jndiName, (DataSource) value);
}
}
} catch (final NamingException e) {
// le préfixe ("comp/env/jdbc", "/jdbc" ou "java:global/jdbc", etc) n'existe pas dans jndi,
// (dans glassfish 3.0.1, c'est une NamingException et non une NameNotFoundException)
return dataSources;
}
initialContext.close();
return dataSources;
}
static int getMaxConnectionCount() {
if (!TOMCAT_BASIC_DATASOURCES_PROPERTIES.isEmpty()) {
return TOMCAT_BASIC_DATASOURCES_PROPERTIES.getMaxActive();
} else if (!DBCP_BASIC_DATASOURCES_PROPERTIES.isEmpty()) {
return DBCP_BASIC_DATASOURCES_PROPERTIES.getMaxActive();
} else if (!TOMCAT_JDBC_DATASOURCES_PROPERTIES.isEmpty()) {
return TOMCAT_JDBC_DATASOURCES_PROPERTIES.getMaxActive();
}
return -1;
}
static Map> getBasicDataSourceProperties() {
if (!TOMCAT_BASIC_DATASOURCES_PROPERTIES.isEmpty()) {
return TOMCAT_BASIC_DATASOURCES_PROPERTIES.getDataSourcesProperties();
} else if (!DBCP_BASIC_DATASOURCES_PROPERTIES.isEmpty()) {
return DBCP_BASIC_DATASOURCES_PROPERTIES.getDataSourcesProperties();
} else if (!TOMCAT_JDBC_DATASOURCES_PROPERTIES.isEmpty()) {
return TOMCAT_JDBC_DATASOURCES_PROPERTIES.getDataSourcesProperties();
}
return Collections.emptyMap();
}
// CHECKSTYLE:OFF
static void pullDataSourceProperties(String name, DataSource dataSource) {
// CHECKSTYLE:ON
final String dataSourceClassName = dataSource.getClass().getName();
if ("org.apache.tomcat.dbcp.dbcp.BasicDataSource".equals(dataSourceClassName)
&& dataSource instanceof BasicDataSource) {
pullTomcatDbcpDataSourceProperties(name, dataSource);
} else if ("org.apache.tomcat.dbcp.dbcp2.BasicDataSource".equals(dataSourceClassName)
&& dataSource instanceof org.apache.tomcat.dbcp.dbcp2.BasicDataSource) {
pullTomcatDbcp2DataSourceProperties(name, dataSource);
} else if ("org.apache.commons.dbcp.BasicDataSource".equals(dataSourceClassName)
&& dataSource instanceof org.apache.commons.dbcp.BasicDataSource) {
pullCommonsDbcpDataSourceProperties(name, dataSource);
} else if ("org.apache.commons.dbcp2.BasicDataSource".equals(dataSourceClassName)
&& dataSource instanceof org.apache.commons.dbcp2.BasicDataSource) {
pullCommonsDbcp2DataSourceProperties(name, dataSource);
} else if ("org.apache.tomcat.jdbc.pool.DataSource".equals(dataSourceClassName)
&& dataSource instanceof org.apache.tomcat.jdbc.pool.DataSource) {
pullTomcatJdbcDataSourceProperties(name, dataSource);
}
}
private static void pullTomcatDbcpDataSourceProperties(String name, DataSource dataSource) {
// si tomcat et si dataSource standard, alors on récupère des infos
final BasicDataSource tomcatDbcpDataSource = (BasicDataSource) dataSource;
final BasicDataSourcesProperties properties = TOMCAT_BASIC_DATASOURCES_PROPERTIES;
// basicDataSource.getNumActive() est en théorie égale à USED_CONNECTION_COUNT à un instant t,
// numIdle + numActive est le nombre de connexions ouvertes dans la bdd pour ce serveur à un instant t
// les propriétés généralement importantes en premier (se méfier aussi de testOnBorrow)
properties.put(name, MAX_ACTIVE_PROPERTY_NAME, tomcatDbcpDataSource.getMaxActive());
properties.put(name, "poolPreparedStatements",
tomcatDbcpDataSource.isPoolPreparedStatements());
properties.put(name, "defaultCatalog", tomcatDbcpDataSource.getDefaultCatalog());
properties.put(name, "defaultAutoCommit", tomcatDbcpDataSource.getDefaultAutoCommit());
properties.put(name, "defaultReadOnly", tomcatDbcpDataSource.getDefaultReadOnly());
properties.put(name, "defaultTransactionIsolation",
tomcatDbcpDataSource.getDefaultTransactionIsolation());
properties.put(name, "driverClassName", tomcatDbcpDataSource.getDriverClassName());
properties.put(name, "initialSize", tomcatDbcpDataSource.getInitialSize());
properties.put(name, "maxIdle", tomcatDbcpDataSource.getMaxIdle());
properties.put(name, "maxOpenPreparedStatements",
tomcatDbcpDataSource.getMaxOpenPreparedStatements());
properties.put(name, "maxWait", tomcatDbcpDataSource.getMaxWait());
properties.put(name, "minEvictableIdleTimeMillis",
tomcatDbcpDataSource.getMinEvictableIdleTimeMillis());
properties.put(name, "minIdle", tomcatDbcpDataSource.getMinIdle());
properties.put(name, "numTestsPerEvictionRun",
tomcatDbcpDataSource.getNumTestsPerEvictionRun());
properties.put(name, "testOnBorrow", tomcatDbcpDataSource.getTestOnBorrow());
properties.put(name, "testOnReturn", tomcatDbcpDataSource.getTestOnReturn());
properties.put(name, "testWhileIdle", tomcatDbcpDataSource.getTestWhileIdle());
properties.put(name, "timeBetweenEvictionRunsMillis",
tomcatDbcpDataSource.getTimeBetweenEvictionRunsMillis());
properties.put(name, "validationQuery", tomcatDbcpDataSource.getValidationQuery());
}
private static void pullCommonsDbcpDataSourceProperties(String name, DataSource dataSource) {
// si dbcp et si dataSource standard, alors on récupère des infos
final org.apache.commons.dbcp.BasicDataSource dbcpDataSource = (org.apache.commons.dbcp.BasicDataSource) dataSource;
final BasicDataSourcesProperties properties = DBCP_BASIC_DATASOURCES_PROPERTIES;
// basicDataSource.getNumActive() est en théorie égale à USED_CONNECTION_COUNT à un instant t,
// numIdle + numActive est le nombre de connexions ouvertes dans la bdd pour ce serveur à un instant t
// les propriétés généralement importantes en premier (se méfier aussi de testOnBorrow)
properties.put(name, MAX_ACTIVE_PROPERTY_NAME, dbcpDataSource.getMaxActive());
properties.put(name, "poolPreparedStatements", dbcpDataSource.isPoolPreparedStatements());
properties.put(name, "defaultCatalog", dbcpDataSource.getDefaultCatalog());
properties.put(name, "defaultAutoCommit", dbcpDataSource.getDefaultAutoCommit());
properties.put(name, "defaultReadOnly", dbcpDataSource.getDefaultReadOnly());
properties.put(name, "defaultTransactionIsolation",
dbcpDataSource.getDefaultTransactionIsolation());
properties.put(name, "driverClassName", dbcpDataSource.getDriverClassName());
properties.put(name, "initialSize", dbcpDataSource.getInitialSize());
properties.put(name, "maxIdle", dbcpDataSource.getMaxIdle());
properties.put(name, "maxOpenPreparedStatements",
dbcpDataSource.getMaxOpenPreparedStatements());
properties.put(name, "maxWait", dbcpDataSource.getMaxWait());
properties.put(name, "minEvictableIdleTimeMillis",
dbcpDataSource.getMinEvictableIdleTimeMillis());
properties.put(name, "minIdle", dbcpDataSource.getMinIdle());
properties.put(name, "numTestsPerEvictionRun", dbcpDataSource.getNumTestsPerEvictionRun());
properties.put(name, "testOnBorrow", dbcpDataSource.getTestOnBorrow());
properties.put(name, "testOnReturn", dbcpDataSource.getTestOnReturn());
properties.put(name, "testWhileIdle", dbcpDataSource.getTestWhileIdle());
properties.put(name, "timeBetweenEvictionRunsMillis",
dbcpDataSource.getTimeBetweenEvictionRunsMillis());
properties.put(name, "validationQuery", dbcpDataSource.getValidationQuery());
}
private static void pullTomcatDbcp2DataSourceProperties(String name, DataSource dataSource) {
// si tomcat et si dataSource standard, alors on récupère des infos
final org.apache.tomcat.dbcp.dbcp2.BasicDataSource tomcatDbcp2DataSource = (org.apache.tomcat.dbcp.dbcp2.BasicDataSource) dataSource;
final BasicDataSourcesProperties properties = TOMCAT_BASIC_DATASOURCES_PROPERTIES;
// basicDataSource.getNumActive() est en théorie égale à USED_CONNECTION_COUNT à un instant t,
// numIdle + numActive est le nombre de connexions ouvertes dans la bdd pour ce serveur à un instant t
// les propriétés généralement importantes en premier (se méfier aussi de testOnBorrow)
properties.put(name, MAX_ACTIVE_PROPERTY_NAME, tomcatDbcp2DataSource.getMaxTotal());
properties.put(name, "poolPreparedStatements",
tomcatDbcp2DataSource.isPoolPreparedStatements());
properties.put(name, "defaultCatalog", tomcatDbcp2DataSource.getDefaultCatalog());
properties.put(name, "defaultAutoCommit", tomcatDbcp2DataSource.getDefaultAutoCommit());
properties.put(name, "defaultReadOnly", tomcatDbcp2DataSource.getDefaultReadOnly());
properties.put(name, "defaultTransactionIsolation",
tomcatDbcp2DataSource.getDefaultTransactionIsolation());
properties.put(name, "driverClassName", tomcatDbcp2DataSource.getDriverClassName());
properties.put(name, "initialSize", tomcatDbcp2DataSource.getInitialSize());
properties.put(name, "maxIdle", tomcatDbcp2DataSource.getMaxIdle());
properties.put(name, "maxOpenPreparedStatements",
tomcatDbcp2DataSource.getMaxOpenPreparedStatements());
properties.put(name, "maxWait", tomcatDbcp2DataSource.getMaxWaitMillis());
properties.put(name, "minEvictableIdleTimeMillis",
tomcatDbcp2DataSource.getMinEvictableIdleTimeMillis());
properties.put(name, "minIdle", tomcatDbcp2DataSource.getMinIdle());
properties.put(name, "numTestsPerEvictionRun",
tomcatDbcp2DataSource.getNumTestsPerEvictionRun());
properties.put(name, "testOnBorrow", tomcatDbcp2DataSource.getTestOnBorrow());
properties.put(name, "testOnReturn", tomcatDbcp2DataSource.getTestOnReturn());
properties.put(name, "testWhileIdle", tomcatDbcp2DataSource.getTestWhileIdle());
properties.put(name, "timeBetweenEvictionRunsMillis",
tomcatDbcp2DataSource.getTimeBetweenEvictionRunsMillis());
properties.put(name, "validationQuery", tomcatDbcp2DataSource.getValidationQuery());
}
private static void pullCommonsDbcp2DataSourceProperties(String name, DataSource dataSource) {
// si dbcp et si dataSource standard, alors on récupère des infos
final org.apache.commons.dbcp2.BasicDataSource dbcp2DataSource = (org.apache.commons.dbcp2.BasicDataSource) dataSource;
final BasicDataSourcesProperties properties = DBCP_BASIC_DATASOURCES_PROPERTIES;
// basicDataSource.getNumActive() est en théorie égale à USED_CONNECTION_COUNT à un instant t,
// numIdle + numActive est le nombre de connexions ouvertes dans la bdd pour ce serveur à un instant t
// les propriétés généralement importantes en premier (se méfier aussi de testOnBorrow)
properties.put(name, MAX_ACTIVE_PROPERTY_NAME, dbcp2DataSource.getMaxTotal());
properties.put(name, "poolPreparedStatements", dbcp2DataSource.isPoolPreparedStatements());
properties.put(name, "defaultCatalog", dbcp2DataSource.getDefaultCatalog());
properties.put(name, "defaultAutoCommit", dbcp2DataSource.getDefaultAutoCommit());
properties.put(name, "defaultReadOnly", dbcp2DataSource.getDefaultReadOnly());
properties.put(name, "defaultTransactionIsolation",
dbcp2DataSource.getDefaultTransactionIsolation());
properties.put(name, "driverClassName", dbcp2DataSource.getDriverClassName());
properties.put(name, "initialSize", dbcp2DataSource.getInitialSize());
properties.put(name, "maxIdle", dbcp2DataSource.getMaxIdle());
properties.put(name, "maxOpenPreparedStatements",
dbcp2DataSource.getMaxOpenPreparedStatements());
properties.put(name, "maxWait", dbcp2DataSource.getMaxWaitMillis());
properties.put(name, "minEvictableIdleTimeMillis",
dbcp2DataSource.getMinEvictableIdleTimeMillis());
properties.put(name, "minIdle", dbcp2DataSource.getMinIdle());
properties.put(name, "numTestsPerEvictionRun", dbcp2DataSource.getNumTestsPerEvictionRun());
properties.put(name, "testOnBorrow", dbcp2DataSource.getTestOnBorrow());
properties.put(name, "testOnReturn", dbcp2DataSource.getTestOnReturn());
properties.put(name, "testWhileIdle", dbcp2DataSource.getTestWhileIdle());
properties.put(name, "timeBetweenEvictionRunsMillis",
dbcp2DataSource.getTimeBetweenEvictionRunsMillis());
properties.put(name, "validationQuery", dbcp2DataSource.getValidationQuery());
}
private static void pullTomcatJdbcDataSourceProperties(String name, DataSource dataSource) {
// si tomcat-jdbc, alors on récupère des infos
final org.apache.tomcat.jdbc.pool.DataSource jdbcDataSource = (org.apache.tomcat.jdbc.pool.DataSource) dataSource;
final BasicDataSourcesProperties properties = TOMCAT_JDBC_DATASOURCES_PROPERTIES;
// basicDataSource.getNumActive() est en théorie égale à USED_CONNECTION_COUNT à un instant t,
// numIdle + numActive est le nombre de connexions ouvertes dans la bdd pour ce serveur à un instant t
// les propriétés généralement importantes en premier (se méfier aussi de testOnBorrow)
properties.put(name, MAX_ACTIVE_PROPERTY_NAME, jdbcDataSource.getMaxActive());
properties.put(name, "defaultCatalog", jdbcDataSource.getDefaultCatalog());
properties.put(name, "defaultAutoCommit", jdbcDataSource.getDefaultAutoCommit());
properties.put(name, "defaultReadOnly", jdbcDataSource.getDefaultReadOnly());
properties.put(name, "defaultTransactionIsolation",
jdbcDataSource.getDefaultTransactionIsolation());
properties.put(name, "driverClassName", jdbcDataSource.getDriverClassName());
properties.put(name, "connectionProperties", jdbcDataSource.getConnectionProperties());
properties.put(name, "initSQL", jdbcDataSource.getInitSQL());
properties.put(name, "initialSize", jdbcDataSource.getInitialSize());
properties.put(name, "maxIdle", jdbcDataSource.getMaxIdle());
properties.put(name, "maxWait", jdbcDataSource.getMaxWait());
properties.put(name, "maxAge", jdbcDataSource.getMaxAge());
properties.put(name, "faireQueue", jdbcDataSource.isFairQueue());
properties.put(name, "jmxEnabled", jdbcDataSource.isJmxEnabled());
properties.put(name, "minEvictableIdleTimeMillis",
jdbcDataSource.getMinEvictableIdleTimeMillis());
properties.put(name, "minIdle", jdbcDataSource.getMinIdle());
properties.put(name, "numTestsPerEvictionRun", jdbcDataSource.getNumTestsPerEvictionRun());
properties.put(name, "testOnBorrow", jdbcDataSource.isTestOnBorrow());
properties.put(name, "testOnConnect", jdbcDataSource.isTestOnConnect());
properties.put(name, "testOnReturn", jdbcDataSource.isTestOnReturn());
properties.put(name, "testWhileIdle", jdbcDataSource.isTestWhileIdle());
properties.put(name, "timeBetweenEvictionRunsMillis",
jdbcDataSource.getTimeBetweenEvictionRunsMillis());
properties.put(name, "validationInterval", jdbcDataSource.getValidationInterval());
properties.put(name, "validationQuery", jdbcDataSource.getValidationQuery());
properties.put(name, "validatorClassName", jdbcDataSource.getValidatorClassName());
}
@SuppressWarnings("all")
private static Object changeContextWritable(ServletContext servletContext, Object lock)
throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException,
NamingException {
// cette méthode ne peut pas être utilisée avec un simple JdbcDriver
assert servletContext != null;
final String serverInfo = servletContext.getServerInfo();
if (serverInfo.contains("jetty")) {
// on n'exécute cela que si c'est jetty
final Context jdbcContext = (Context) new InitialContext().lookup("java:comp");
final Field field = getAccessibleField(jdbcContext, "_env");
@SuppressWarnings("unchecked")
final Hashtable