001/* 002// This software is subject to the terms of the Eclipse Public License v1.0 003// Agreement, available at the following URL: 004// http://www.eclipse.org/legal/epl-v10.html. 005// You must accept the terms of that agreement to use this software. 006// 007// Copyright (C) 2011-2012 Pentaho 008// All Rights Reserved. 009*/ 010package mondrian.xmla.impl; 011 012import mondrian.olap.Util; 013import mondrian.xmla.XmlaHandler; 014 015import org.apache.commons.dbcp.BasicDataSource; 016import org.apache.commons.dbcp.DelegatingConnection; 017import org.apache.log4j.Logger; 018 019import org.olap4j.OlapConnection; 020import org.olap4j.OlapWrapper; 021 022import java.lang.reflect.*; 023import java.sql.Connection; 024import java.sql.SQLException; 025import java.util.*; 026import javax.servlet.ServletConfig; 027import javax.servlet.ServletException; 028 029/** 030 * XMLA servlet that gets its connections from an olap4j data source. 031 * 032 * @author Julian Hyde 033 * @author Michele Rossi 034 */ 035public class Olap4jXmlaServlet extends DefaultXmlaServlet { 036 private static final Logger LOGGER = 037 Logger.getLogger(Olap4jXmlaServlet.class); 038 039 private static final String OLAP_DRIVER_CLASS_NAME_PARAM = 040 "OlapDriverClassName"; 041 042 private static final String OLAP_DRIVER_CONNECTION_STRING_PARAM = 043 "OlapDriverConnectionString"; 044 045 private static final String OLAP_DRIVER_CONNECTION_PROPERTIES_PREFIX = 046 "OlapDriverConnectionProperty."; 047 048 private static final String 049 OLAP_DRIVER_PRECONFIGURED_DISCOVER_DATASOURCES_RESPONSE = 050 "OlapDriverUsePreConfiguredDiscoverDatasourcesResponse"; 051 052 private static final String OLAP_DRIVER_IDLE_CONNECTIONS_TIMEOUT_MINUTES = 053 "OlapDriverIdleConnectionsTimeoutMinutes"; 054 055 private static final String 056 OLAP_DRIVER_PRECONFIGURED_DISCOVER_DATASOURCES_PREFIX = 057 "OlapDriverDiscoverDatasources."; 058 059 /** 060 * Name of property used by JDBC to hold user name. 061 */ 062 private static final String JDBC_USER = "user"; 063 064 /** 065 * Name of property used by JDBC to hold password. 066 */ 067 private static final String JDBC_PASSWORD = "password"; 068 069 /** idle connections are cleaned out after 5 minutes by default */ 070 private static final int DEFAULT_IDLE_CONNECTIONS_TIMEOUT_MS = 071 5 * 60 * 1000; 072 073 private static final String OLAP_DRIVER_MAX_NUM_CONNECTIONS_PER_USER = 074 "OlapDriverMaxNumConnectionsPerUser"; 075 076 /** 077 * Unwraps a given interface from a given connection. 078 * 079 * @param connection Connection object 080 * @param clazz Interface to unwrap 081 * @param <T> Type of interface 082 * @return Unwrapped object; never null 083 * @throws java.sql.SQLException if cannot convert 084 */ 085 private static <T> T unwrap(Connection connection, Class<T> clazz) 086 throws SQLException 087 { 088 // Invoke Wrapper.unwrap(). Works for JDK 1.6 and later, but we use 089 // reflection so that it compiles on JDK 1.5. 090 try { 091 final Class<?> wrapperClass = Class.forName("java.sql.Wrapper"); 092 if (wrapperClass.isInstance(connection)) { 093 Method unwrapMethod = wrapperClass.getMethod("unwrap"); 094 return clazz.cast(unwrapMethod.invoke(connection, clazz)); 095 } 096 } catch (ClassNotFoundException e) { 097 // ignore 098 } catch (NoSuchMethodException e) { 099 // ignore 100 } catch (InvocationTargetException e) { 101 // ignore 102 } catch (IllegalAccessException e) { 103 // ignore 104 } 105 if (connection instanceof OlapWrapper) { 106 OlapWrapper olapWrapper = (OlapWrapper) connection; 107 return olapWrapper.unwrap(clazz); 108 } 109 throw new SQLException("not an instance"); 110 } 111 112 @Override 113 protected XmlaHandler.ConnectionFactory createConnectionFactory( 114 ServletConfig servletConfig) 115 throws ServletException 116 { 117 final String olap4jDriverClassName = 118 servletConfig.getInitParameter(OLAP_DRIVER_CLASS_NAME_PARAM); 119 final String olap4jDriverConnectionString = 120 servletConfig.getInitParameter(OLAP_DRIVER_CONNECTION_STRING_PARAM); 121 final String olap4jUsePreConfiguredDiscoverDatasourcesRes = 122 servletConfig.getInitParameter( 123 OLAP_DRIVER_PRECONFIGURED_DISCOVER_DATASOURCES_RESPONSE); 124 boolean hardcodedDiscoverDatasources = 125 olap4jUsePreConfiguredDiscoverDatasourcesRes != null 126 && Boolean.parseBoolean( 127 olap4jUsePreConfiguredDiscoverDatasourcesRes); 128 129 final String idleConnTimeoutStr = 130 servletConfig.getInitParameter( 131 OLAP_DRIVER_IDLE_CONNECTIONS_TIMEOUT_MINUTES); 132 final int idleConnectionsCleanupTimeoutMs = 133 idleConnTimeoutStr != null 134 ? Integer.parseInt(idleConnTimeoutStr) * 60 * 1000 135 : DEFAULT_IDLE_CONNECTIONS_TIMEOUT_MS; 136 137 final String maxNumConnPerUserStr = 138 servletConfig.getInitParameter( 139 OLAP_DRIVER_MAX_NUM_CONNECTIONS_PER_USER); 140 int maxNumConnectionsPerUser = 141 maxNumConnPerUserStr != null 142 ? Integer.parseInt(maxNumConnPerUserStr) 143 : 1; 144 try { 145 Map<String, String> connectionProperties = 146 getOlap4jConnectionProperties( 147 servletConfig, 148 OLAP_DRIVER_CONNECTION_PROPERTIES_PREFIX); 149 final Map<String, Object> ddhcRes; 150 if (hardcodedDiscoverDatasources) { 151 ddhcRes = 152 getDiscoverDatasourcesPreConfiguredResponse(servletConfig); 153 } else { 154 ddhcRes = null; 155 } 156 157 return new Olap4jPoolingConnectionFactory( 158 olap4jDriverClassName, 159 olap4jDriverConnectionString, 160 connectionProperties, 161 idleConnectionsCleanupTimeoutMs, 162 maxNumConnectionsPerUser, 163 ddhcRes); 164 } catch (Exception ex) { 165 String msg = 166 "Exception [" + ex + "] while trying to create " 167 + "olap4j connection to [" 168 + olap4jDriverConnectionString + "] using driver " 169 + "[" + olap4jDriverClassName + "]"; 170 LOGGER.error(msg, ex); 171 throw new ServletException(msg, ex); 172 } 173 } 174 175 private static Map<String, Object> 176 getDiscoverDatasourcesPreConfiguredResponse( 177 ServletConfig servletConfig) 178 { 179 final Map<String, Object> map = new LinkedHashMap<String, Object>(); 180 foo(map, "DataSourceName", servletConfig, "dataSourceName"); 181 foo( 182 map, "DataSourceDescription", 183 servletConfig, "dataSourceDescription"); 184 foo(map, "URL", servletConfig, "url"); 185 foo(map, "DataSourceInfo", servletConfig, "dataSourceInfo"); 186 foo(map, "ProviderName", servletConfig, "providerName"); 187 foo(map, "ProviderType", servletConfig, "providerType"); 188 foo(map, "AuthenticationMode", servletConfig, "authenticationMode"); 189 return map; 190 } 191 192 private static void foo( 193 Map<String, Object> map, 194 String targetProp, 195 ServletConfig servletConfig, 196 String sourceProp) 197 { 198 final String value = 199 servletConfig.getInitParameter( 200 OLAP_DRIVER_PRECONFIGURED_DISCOVER_DATASOURCES_PREFIX 201 + sourceProp); 202 map.put(targetProp, value); 203 } 204 205 private static class Olap4jPoolingConnectionFactory 206 implements XmlaHandler.ConnectionFactory 207 { 208 private final String olap4jDriverConnectionString; 209 private final Properties connProperties; 210 private final Map<String, Object> discoverDatasourcesResponse; 211 private final String olap4jDriverClassName; 212 private final Map<String, BasicDataSource> datasourcesPool = 213 new HashMap<String, BasicDataSource>(); 214 private final int idleConnectionsCleanupTimeoutMs; 215 private final int maxPerUserConnectionCount; 216 217 /** 218 * Creates an Olap4jPoolingConnectionFactory. 219 * 220 * @param olap4jDriverClassName Driver class name 221 * @param olap4jDriverConnectionString Connect string 222 * @param connectionProperties Connection properties 223 * @param maxPerUserConnectionCount max number of connections to create 224 * for every different username 225 * @param idleConnectionsCleanupTimeoutMs pooled connections inactive 226 * for longer than this period of time can be cleaned up 227 * @param discoverDatasourcesResponse Pre-configured response to 228 * DISCOVER_DATASOURCES request, or null 229 * @throws ClassNotFoundException if driver class is not found 230 */ 231 public Olap4jPoolingConnectionFactory( 232 final String olap4jDriverClassName, 233 final String olap4jDriverConnectionString, 234 final Map<String, String> connectionProperties, 235 final int idleConnectionsCleanupTimeoutMs, 236 final int maxPerUserConnectionCount, 237 final Map<String, Object> discoverDatasourcesResponse) 238 throws ClassNotFoundException 239 { 240 Class.forName(olap4jDriverClassName); 241 this.maxPerUserConnectionCount = maxPerUserConnectionCount; 242 this.idleConnectionsCleanupTimeoutMs = 243 idleConnectionsCleanupTimeoutMs; 244 this.olap4jDriverClassName = olap4jDriverClassName; 245 this.olap4jDriverConnectionString = olap4jDriverConnectionString; 246 this.connProperties = new Properties(); 247 this.connProperties.putAll(connectionProperties); 248 this.discoverDatasourcesResponse = discoverDatasourcesResponse; 249 } 250 251 public OlapConnection getConnection( 252 String catalog, 253 String schema, 254 String roleName, 255 Properties props) 256 throws SQLException 257 { 258 final String user = props.getProperty(JDBC_USER); 259 final String pwd = props.getProperty(JDBC_PASSWORD); 260 261 // note: this works also for un-authenticated connections; they will 262 // simply all be created by the same BasicDataSource object 263 final String dataSourceKey = user + "_" + pwd; 264 265 BasicDataSource bds; 266 synchronized (datasourcesPool) { 267 bds = datasourcesPool.get(dataSourceKey); 268 if (bds == null) { 269 bds = new BasicDataSource() { 270 { 271 connectionProperties.putAll(connProperties); 272 } 273 }; 274 bds.setDefaultReadOnly(true); 275 bds.setDriverClassName(olap4jDriverClassName); 276 bds.setPassword(pwd); 277 bds.setUsername(user); 278 bds.setUrl(olap4jDriverConnectionString); 279 bds.setPoolPreparedStatements(false); 280 bds.setMaxIdle(maxPerUserConnectionCount); 281 bds.setMaxActive(maxPerUserConnectionCount); 282 bds.setMinEvictableIdleTimeMillis( 283 idleConnectionsCleanupTimeoutMs); 284 bds.setAccessToUnderlyingConnectionAllowed(true); 285 bds.setInitialSize(1); 286 bds.setTimeBetweenEvictionRunsMillis(60000); 287 if (catalog != null) { 288 bds.setDefaultCatalog(catalog); 289 } 290 datasourcesPool.put(dataSourceKey, bds); 291 } 292 } 293 294 Connection connection = bds.getConnection(); 295 DelegatingConnection dc = (DelegatingConnection) connection; 296 Connection underlyingOlapConnection = dc.getInnermostDelegate(); 297 OlapConnection olapConnection = 298 unwrap(underlyingOlapConnection, OlapConnection.class); 299 300 if (LOGGER.isDebugEnabled()) { 301 LOGGER.debug( 302 "Obtained connection object [" + olapConnection 303 + "] (ext pool wrapper " + connection + ") for key " 304 + dataSourceKey); 305 } 306 if (catalog != null) { 307 olapConnection.setCatalog(catalog); 308 } 309 if (schema != null) { 310 olapConnection.setSchema(schema); 311 } 312 if (roleName != null) { 313 olapConnection.setRoleName(roleName); 314 } 315 316 return createDelegatingOlapConnection(connection, olapConnection); 317 } 318 319 public Map<String, Object> getPreConfiguredDiscoverDatasourcesResponse() 320 { 321 return discoverDatasourcesResponse; 322 } 323 } 324 325 /** 326 * Obtains connection properties from the 327 * ServletConfig init parameters and from System properties. 328 * 329 * <p>The properties found in the System properties override the ones in 330 * the ServletConfig. 331 * 332 * <p>copies the values of init parameters / properties which 333 * start with the given prefix to a target Map object stripping out the 334 * configured prefix from the property name. 335 * 336 * <p>The following example uses prefix "olapConn.": 337 * 338 * <code><pre> 339 * <init-param> 340 * <param-name>olapConn.User</param-name> 341 * <param-value>mrossi</param-value> 342 * </init-param> 343 * <init-param> 344 * <param-name>olapConn.Password</param-name> 345 * <param-value>manhattan</param-value> 346 * </init-param> 347 * 348 * </pre></code> 349 * 350 * <p>This will result in a connection properties object with entries 351 * <code>{("User", "mrossi"), ("Password", "manhattan")}</code>. 352 * 353 * @param prefix Prefix to property name 354 * @param servletConfig Servlet config 355 * @return Map containing property names and values 356 */ 357 private static Map<String, String> getOlap4jConnectionProperties( 358 final ServletConfig servletConfig, 359 final String prefix) 360 { 361 Map<String, String> options = new LinkedHashMap<String, String>(); 362 363 // Get properties from servlet config. 364 @SuppressWarnings({"unchecked"}) 365 java.util.Enumeration<String> en = 366 servletConfig.getInitParameterNames(); 367 while (en.hasMoreElements()) { 368 String paramName = en.nextElement(); 369 if (paramName.startsWith(prefix)) { 370 String paramValue = servletConfig.getInitParameter(paramName); 371 String prefixRemovedParamName = 372 paramName.substring(prefix.length()); 373 options.put(prefixRemovedParamName, paramValue); 374 } 375 } 376 377 // Get system properties. 378 final Map<String, String> systemProps = 379 Util.toMap(System.getProperties()); 380 for (Map.Entry<String, String> entry : systemProps.entrySet()) { 381 String sk = entry.getKey(); 382 if (sk.startsWith(prefix)) { 383 String value = entry.getValue(); 384 String prefixRemovedKey = sk.substring(prefix.length()); 385 options.put(prefixRemovedKey, value); 386 } 387 } 388 389 return options; 390 } 391 392 /** 393 * Returns something that implements {@link OlapConnection} but still 394 * behaves as the wrapper returned by the connection pool. 395 * 396 * <p>In other words we want the "close" method to play nice and do all the 397 * pooling actions while we want all the olap methods to execute directly on 398 * the un-wrapped OlapConnection object. 399 */ 400 private static OlapConnection createDelegatingOlapConnection( 401 final Connection connection, 402 final OlapConnection olapConnection) 403 { 404 return (OlapConnection) Proxy.newProxyInstance( 405 olapConnection.getClass().getClassLoader(), 406 new Class[] {OlapConnection.class}, 407 new InvocationHandler() { 408 public Object invoke( 409 Object proxy, 410 Method method, 411 Object[] args) 412 throws Throwable 413 { 414 if ("unwrap".equals(method.getName()) 415 || OlapConnection.class 416 .isAssignableFrom(method.getDeclaringClass())) 417 { 418 return method.invoke(olapConnection, args); 419 } else { 420 return method.invoke(connection, args); 421 } 422 } 423 } 424 ); 425 } 426} 427 428// End Olap4jXmlaServlet.java