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     *  &lt;init-param&gt;
340     *      &lt;param-name&gt;olapConn.User&lt;/param-name&gt;
341     *      &lt;param-value&gt;mrossi&lt;/param-value&gt;
342     *  &lt;/init-param&gt;
343     *  &lt;init-param&gt;
344     *      &lt;param-name&gt;olapConn.Password&lt;/param-name&gt;
345     *      &lt;param-value&gt;manhattan&lt;/param-value&gt;
346     *  &lt;/init-param&gt;
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