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) 2001-2005 Julian Hyde
008// Copyright (C) 2005-2013 Pentaho and others
009// All Rights Reserved.
010*/
011package mondrian.rolap;
012
013import mondrian.calc.*;
014import mondrian.calc.impl.DelegatingTupleList;
015import mondrian.olap.*;
016import mondrian.parser.MdxParserValidator;
017import mondrian.resource.MondrianResource;
018import mondrian.server.*;
019import mondrian.spi.*;
020import mondrian.spi.impl.JndiDataSourceResolver;
021import mondrian.util.*;
022
023import org.apache.log4j.Logger;
024
025import org.eigenbase.util.property.StringProperty;
026
027import org.olap4j.Scenario;
028
029import java.io.PrintWriter;
030import java.lang.reflect.InvocationTargetException;
031import java.lang.reflect.Method;
032import java.sql.Connection;
033import java.sql.SQLException;
034import java.util.*;
035import java.util.concurrent.Callable;
036import java.util.concurrent.atomic.AtomicInteger;
037import javax.sql.DataSource;
038
039/**
040 * A <code>RolapConnection</code> is a connection to a Mondrian OLAP Server.
041 *
042 * <p>Typically, you create a connection via
043 * {@link DriverManager#getConnection(String, mondrian.spi.CatalogLocator)}.
044 * {@link RolapConnectionProperties} describes allowable keywords.</p>
045 *
046 * @see RolapSchema
047 * @see DriverManager
048 * @author jhyde
049 * @since 2 October, 2002
050 */
051public class RolapConnection extends ConnectionBase {
052    private static final Logger LOGGER =
053        Logger.getLogger(RolapConnection.class);
054    private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
055
056    private final MondrianServer server;
057
058    private final Util.PropertyList connectInfo;
059
060    /**
061     * Factory for JDBC connections to talk to the RDBMS. This factory will
062     * usually use a connection pool.
063     */
064    private final DataSource dataSource;
065    private final String catalogUrl;
066    private final RolapSchema schema;
067    private SchemaReader schemaReader;
068    protected Role role;
069    private Locale locale = Locale.getDefault();
070    private Scenario scenario;
071    private boolean closed = false;
072
073    private static DataSourceResolver dataSourceResolver;
074    private final int id;
075    private final Statement internalStatement;
076
077    /**
078     * Creates a connection.
079     *
080     * @param server Server instance this connection belongs to
081     * @param connectInfo Connection properties; keywords are described in
082     *   {@link RolapConnectionProperties}.
083     * @param dataSource JDBC data source
084     */
085    public RolapConnection(
086        MondrianServer server,
087        Util.PropertyList connectInfo,
088        DataSource dataSource)
089    {
090        this(server, connectInfo, null, dataSource);
091    }
092
093    /**
094     * Creates a RolapConnection.
095     *
096     * <p>Only {@link RolapSchemaPool#get} calls this with
097     * schema != null (to create a schema's internal connection).
098     * Other uses retrieve a schema from the cache based upon
099     * the <code>Catalog</code> property.
100     *
101     * @param server Server instance this connection belongs to
102     * @param connectInfo Connection properties; keywords are described in
103     *   {@link RolapConnectionProperties}.
104     * @param schema Schema for the connection. Must be null unless this is to
105     *   be an internal connection.
106     * @param dataSource If not null an external DataSource to be used
107     *        by Mondrian
108     */
109    RolapConnection(
110        MondrianServer server,
111        Util.PropertyList connectInfo,
112        RolapSchema schema,
113        DataSource dataSource)
114    {
115        super();
116        assert server != null;
117        this.server = server;
118        this.id = ID_GENERATOR.getAndIncrement();
119
120        assert connectInfo != null;
121        String provider = connectInfo.get(
122            RolapConnectionProperties.Provider.name(), "mondrian");
123        Util.assertTrue(provider.equalsIgnoreCase("mondrian"));
124        this.connectInfo = connectInfo;
125        this.catalogUrl =
126            connectInfo.get(RolapConnectionProperties.Catalog.name());
127        final String jdbcUser =
128            connectInfo.get(RolapConnectionProperties.JdbcUser.name());
129        final String jdbcConnectString =
130            connectInfo.get(RolapConnectionProperties.Jdbc.name());
131        final String strDataSource =
132            connectInfo.get(RolapConnectionProperties.DataSource.name());
133        StringBuilder buf = new StringBuilder();
134        this.dataSource =
135            createDataSource(dataSource, connectInfo, buf);
136        Role role = null;
137
138        // Register this connection before we register its internal statement.
139        server.addConnection(this);
140
141        if (schema == null) {
142            // If RolapSchema.Pool.get were to call this with schema == null,
143            // we would loop.
144            Statement bootstrapStatement = createInternalStatement(false);
145            final Locus locus =
146                new Locus(
147                    new Execution(bootstrapStatement, 0),
148                    null,
149                    "Initializing connection");
150            Locus.push(locus);
151            try {
152                if (dataSource == null) {
153                    // If there is no external data source is passed in, we
154                    // expect the properties Jdbc, JdbcUser, DataSource to be
155                    // set, as they are used to generate the schema cache key.
156                    final String connectionKey =
157                        jdbcConnectString
158                        + getJdbcProperties(connectInfo).toString();
159
160                    schema = RolapSchemaPool.instance().get(
161                        catalogUrl,
162                        connectionKey,
163                        jdbcUser,
164                        strDataSource,
165                        connectInfo);
166                } else {
167                    schema = RolapSchemaPool.instance().get(
168                        catalogUrl,
169                        dataSource,
170                        connectInfo);
171                }
172            } finally {
173                Locus.pop(locus);
174                bootstrapStatement.close();
175            }
176            internalStatement =
177                schema.getInternalConnection().getInternalStatement();
178            String roleNameList =
179                connectInfo.get(RolapConnectionProperties.Role.name());
180            if (roleNameList != null) {
181                List<String> roleNames = Util.parseCommaList(roleNameList);
182                List<Role> roleList = new ArrayList<Role>();
183                for (String roleName : roleNames) {
184                    final LockBox.Entry entry =
185                        server.getLockBox().get(roleName);
186                    Role role1;
187                    if (entry != null) {
188                        try {
189                            role1 = (Role) entry.getValue();
190                        } catch (ClassCastException e) {
191                            role1 = null;
192                        }
193                    } else {
194                        role1 = schema.lookupRole(roleName);
195                    }
196                    if (role1 == null) {
197                        throw Util.newError(
198                            "Role '" + roleName + "' not found");
199                    }
200                    roleList.add(role1);
201                }
202                switch (roleList.size()) {
203                case 0:
204                    // If they specify 'Role=;', the list of names will be
205                    // empty, and the effect will be as if they did specify
206                    // Role at all.
207                    role = null;
208                    break;
209                case 1:
210                    role = roleList.get(0);
211                    break;
212                default:
213                    role = RoleImpl.union(roleList);
214                    break;
215                }
216            }
217        } else {
218            this.internalStatement = createInternalStatement(true);
219
220            // We are creating an internal connection. Now is a great time to
221            // make sure that the JDBC credentials are valid, for this
222            // connection and for external connections built on top of this.
223            Connection conn = null;
224            java.sql.Statement statement = null;
225            try {
226                conn = this.dataSource.getConnection();
227                Dialect dialect =
228                    DialectManager.createDialect(this.dataSource, conn);
229                if (dialect.getDatabaseProduct()
230                    == Dialect.DatabaseProduct.DERBY)
231                {
232                    // Derby requires a little extra prodding to do the
233                    // validation to detect an error.
234                    statement = conn.createStatement();
235                    statement.executeQuery("select * from bogustable");
236                }
237            } catch (SQLException e) {
238                if (e.getMessage().equals(
239                        "Table/View 'BOGUSTABLE' does not exist."))
240                {
241                    // Ignore. This exception comes from Derby when the
242                    // connection is valid. If the connection were invalid, we
243                    // would receive an error such as "Schema 'BOGUSUSER' does
244                    // not exist"
245                } else {
246                    throw Util.newError(
247                        e,
248                        "Error while creating SQL connection: " + buf);
249                }
250            } finally {
251                try {
252                    if (statement != null) {
253                        statement.close();
254                    }
255                    if (conn != null) {
256                        conn.close();
257                    }
258                } catch (SQLException e) {
259                    // ignore
260                }
261            }
262        }
263
264        if (role == null) {
265            role = schema.getDefaultRole();
266        }
267
268        // Set the locale.
269        String localeString =
270            connectInfo.get(RolapConnectionProperties.Locale.name());
271        if (localeString != null) {
272            this.locale = Util.parseLocale(localeString);
273            assert locale != null;
274        }
275
276        this.schema = schema;
277        setRole(role);
278    }
279
280    @Override
281    protected void finalize() throws Throwable {
282        try {
283            super.finalize();
284            close();
285        } catch (Throwable t) {
286            LOGGER.info(
287                MondrianResource.instance()
288                    .FinalizerErrorRolapConnection.baseMessage,
289                t);
290        }
291    }
292
293    /**
294     * Returns the identifier of this connection. Unique within the lifetime of
295     * this JVM.
296     *
297     * @return Identifier of this connection
298     */
299    public int getId() {
300        return id;
301    }
302
303    protected Logger getLogger() {
304        return LOGGER;
305    }
306
307    /**
308     * Creates a JDBC data source from the JDBC credentials contained within a
309     * set of mondrian connection properties.
310     *
311     * <p>This method is package-level so that it can be called from the
312     * RolapConnectionTest unit test.
313     *
314     * @param dataSource Anonymous data source from user, or null
315     * @param connectInfo Mondrian connection properties
316     * @param buf Into which method writes a description of the JDBC credentials
317     * @return Data source
318     */
319    static DataSource createDataSource(
320        DataSource dataSource,
321        Util.PropertyList connectInfo,
322        StringBuilder buf)
323    {
324        assert buf != null;
325        final String jdbcConnectString =
326            connectInfo.get(RolapConnectionProperties.Jdbc.name());
327        final String jdbcUser =
328            connectInfo.get(RolapConnectionProperties.JdbcUser.name());
329        final String jdbcPassword =
330            connectInfo.get(RolapConnectionProperties.JdbcPassword.name());
331        final String dataSourceName =
332            connectInfo.get(RolapConnectionProperties.DataSource.name());
333
334        if (dataSource != null) {
335            appendKeyValue(buf, "Anonymous data source", dataSource);
336            appendKeyValue(
337                buf, RolapConnectionProperties.JdbcUser.name(), jdbcUser);
338            appendKeyValue(
339                buf,
340                RolapConnectionProperties.JdbcPassword.name(),
341                jdbcPassword);
342            if (jdbcUser != null || jdbcPassword != null) {
343                dataSource =
344                    new UserPasswordDataSource(
345                        dataSource, jdbcUser, jdbcPassword);
346            }
347            return dataSource;
348
349        } else if (jdbcConnectString != null) {
350            // Get connection through own pooling datasource
351            appendKeyValue(
352                buf, RolapConnectionProperties.Jdbc.name(), jdbcConnectString);
353            appendKeyValue(
354                buf, RolapConnectionProperties.JdbcUser.name(), jdbcUser);
355            appendKeyValue(
356                buf,
357                RolapConnectionProperties.JdbcPassword.name(),
358                jdbcPassword);
359            String jdbcDrivers =
360                connectInfo.get(RolapConnectionProperties.JdbcDrivers.name());
361            if (jdbcDrivers != null) {
362                RolapUtil.loadDrivers(jdbcDrivers);
363            }
364            final String jdbcDriversProp =
365                    MondrianProperties.instance().JdbcDrivers.get();
366            RolapUtil.loadDrivers(jdbcDriversProp);
367
368            Properties jdbcProperties = getJdbcProperties(connectInfo);
369            final Map<String, String> map = Util.toMap(jdbcProperties);
370            for (Map.Entry<String, String> entry : map.entrySet()) {
371                // FIXME ordering is non-deterministic
372                appendKeyValue(buf, entry.getKey(), entry.getValue());
373            }
374
375            if (jdbcUser != null) {
376                jdbcProperties.put("user", jdbcUser);
377            }
378            if (jdbcPassword != null) {
379                jdbcProperties.put("password", jdbcPassword);
380            }
381
382            // JDBC connections are dumb beasts, so we assume they're not
383            // pooled. Therefore the default is true.
384            final boolean poolNeeded =
385                connectInfo.get(
386                    RolapConnectionProperties.PoolNeeded.name(),
387                    "true").equalsIgnoreCase("true");
388
389            if (!poolNeeded) {
390                // Connection is already pooled; don't pool it again.
391                return new DriverManagerDataSource(
392                    jdbcConnectString,
393                    jdbcProperties);
394            }
395
396            if (jdbcConnectString.toLowerCase().indexOf("mysql") > -1) {
397                // mysql driver needs this autoReconnect parameter
398                jdbcProperties.setProperty("autoReconnect", "true");
399            }
400            return RolapConnectionPool.instance()
401                .getDriverManagerPoolingDataSource(
402                    jdbcConnectString, jdbcProperties);
403
404        } else if (dataSourceName != null) {
405            appendKeyValue(
406                buf,
407                RolapConnectionProperties.DataSource.name(),
408                dataSourceName);
409            appendKeyValue(
410                buf,
411                RolapConnectionProperties.JdbcUser.name(),
412                jdbcUser);
413            appendKeyValue(
414                buf,
415                RolapConnectionProperties.JdbcPassword.name(),
416                jdbcPassword);
417
418            // Data sources are fairly smart, so we assume they look after
419            // their own pooling. Therefore the default is false.
420            final boolean poolNeeded =
421                connectInfo.get(
422                    RolapConnectionProperties.PoolNeeded.name(),
423                    "false").equalsIgnoreCase("true");
424
425            // Get connection from datasource.
426            DataSourceResolver dataSourceResolver = getDataSourceResolver();
427            try {
428                dataSource = dataSourceResolver.lookup(dataSourceName);
429            } catch (Exception e) {
430                throw Util.newInternal(
431                    e,
432                    "Error while looking up data source ("
433                    + dataSourceName + ")");
434            }
435            if (poolNeeded) {
436                dataSource =
437                    RolapConnectionPool.instance()
438                        .getDataSourcePoolingDataSource(
439                            dataSource, dataSourceName, jdbcUser, jdbcPassword);
440            } else {
441                if (jdbcUser != null || jdbcPassword != null) {
442                    dataSource =
443                        new UserPasswordDataSource(
444                            dataSource, jdbcUser, jdbcPassword);
445                }
446            }
447            return dataSource;
448        } else {
449            throw Util.newInternal(
450                "Connect string '" + connectInfo.toString()
451                + "' must contain either '" + RolapConnectionProperties.Jdbc
452                + "' or '" + RolapConnectionProperties.DataSource + "'");
453        }
454    }
455
456    /**
457     * Returns the instance of the {@link mondrian.spi.DataSourceResolver}
458     * plugin.
459     *
460     * @return data source resolver
461     */
462    private static synchronized DataSourceResolver getDataSourceResolver() {
463        if (dataSourceResolver == null) {
464            final StringProperty property =
465                MondrianProperties.instance().DataSourceResolverClass;
466            final String className =
467                property.get(
468                    JndiDataSourceResolver.class.getName());
469            try {
470                dataSourceResolver =
471                    ClassResolver.INSTANCE.instantiateSafe(className);
472            } catch (ClassCastException e) {
473                throw Util.newInternal(
474                    e,
475                    "Plugin class specified by property "
476                    + property.getPath()
477                    + " must implement "
478                    + DataSourceResolver.class.getName());
479            }
480        }
481        return dataSourceResolver;
482    }
483
484    /**
485     * Appends "key=value" to a buffer, if value is not null.
486     *
487     * @param buf Buffer
488     * @param key Key
489     * @param value Value
490     */
491    private static void appendKeyValue(
492        StringBuilder buf,
493        String key,
494        Object value)
495    {
496        if (value != null) {
497            if (buf.length() > 0) {
498                buf.append("; ");
499            }
500            buf.append(key).append('=').append(value);
501        }
502    }
503
504    /**
505     * Creates a {@link Properties} object containing all of the JDBC
506     * connection properties present in the
507     * {@link mondrian.olap.Util.PropertyList connectInfo}.
508     *
509     * @param connectInfo Connection properties
510     * @return The JDBC connection properties.
511     */
512    private static Properties getJdbcProperties(Util.PropertyList connectInfo) {
513        Properties jdbcProperties = new Properties();
514        for (Pair<String, String> entry : connectInfo) {
515            if (entry.left.startsWith(
516                    RolapConnectionProperties.JdbcPropertyPrefix))
517            {
518                jdbcProperties.put(
519                    entry.left.substring(
520                        RolapConnectionProperties.JdbcPropertyPrefix.length()),
521                    entry.right);
522            }
523        }
524        return jdbcProperties;
525    }
526
527    public Util.PropertyList getConnectInfo() {
528        return connectInfo;
529    }
530
531    public void close() {
532        if (!closed) {
533            closed = true;
534            server.removeConnection(this);
535        }
536        if (internalStatement != null) {
537            internalStatement.close();
538        }
539    }
540
541    public RolapSchema getSchema() {
542        return schema;
543    }
544
545    public String getConnectString() {
546        return connectInfo.toString();
547    }
548
549    public String getCatalogName() {
550        return catalogUrl;
551    }
552
553    public Locale getLocale() {
554        return locale;
555    }
556
557    public void setLocale(Locale locale) {
558        if (locale == null) {
559            throw new IllegalArgumentException("locale must not be null");
560        }
561        this.locale = locale;
562    }
563
564    public SchemaReader getSchemaReader() {
565        return schemaReader;
566    }
567
568    public Object getProperty(String name) {
569        // Mask out the values of certain properties.
570        if (name.equals(RolapConnectionProperties.JdbcPassword.name())
571            || name.equals(RolapConnectionProperties.CatalogContent.name()))
572        {
573            return "";
574        }
575        return connectInfo.get(name);
576    }
577
578    public CacheControl getCacheControl(PrintWriter pw) {
579        return getServer().getAggregationManager().getCacheControl(this, pw);
580    }
581
582    /**
583     * Executes a Query.
584     *
585     * @param query Query parse tree
586     *
587     * @throws ResourceLimitExceededException if some resource limit specified
588     *     in the property file was exceeded
589     * @throws QueryCanceledException if query was canceled during execution
590     * @throws QueryTimeoutException if query exceeded timeout specified in
591     *     the property file
592     *
593     * @deprecated Use {@link #execute(mondrian.server.Execution)}; this method
594     *     will be removed in mondrian-4.0
595     */
596    public Result execute(Query query) {
597        final Statement statement = query.getStatement();
598        Execution execution =
599            new Execution(statement, statement.getQueryTimeoutMillis());
600        return execute(execution);
601    }
602
603    /**
604     * Executes a statement.
605     *
606     * @param execution Execution context (includes statement, query)
607     *
608     * @throws ResourceLimitExceededException if some resource limit specified
609     *     in the property file was exceeded
610     * @throws QueryCanceledException if query was canceled during execution
611     * @throws QueryTimeoutException if query exceeded timeout specified in
612     *     the property file
613     */
614    public Result execute(final Execution execution) {
615        execution.copyMDC();
616        return
617            server.getResultShepherd()
618                .shepherdExecution(
619                    execution,
620                    new Callable<Result>() {
621                        public Result call() throws Exception {
622                            return executeInternal(execution);
623                        }
624                    });
625    }
626
627    private Result executeInternal(final Execution execution) {
628        execution.setContextMap();
629        final Statement statement = execution.getMondrianStatement();
630        // Cleanup any previous executions still running
631        synchronized (statement) {
632            final Execution previousExecution =
633                statement.getCurrentExecution();
634            if (previousExecution != null) {
635                statement.end(previousExecution);
636            }
637        }
638        final Query query = statement.getQuery();
639        final MemoryMonitor.Listener listener = new MemoryMonitor.Listener() {
640            public void memoryUsageNotification(long used, long max) {
641                execution.setOutOfMemory(
642                    "OutOfMemory used="
643                    + used
644                    + ", max="
645                    + max
646                    + " for connection: "
647                    + getConnectString());
648            }
649        };
650        MemoryMonitor mm = MemoryMonitorFactory.getMemoryMonitor();
651        final long currId = execution.getId();
652        try {
653            mm.addListener(listener);
654            // Check to see if we must punt
655            execution.checkCancelOrTimeout();
656
657            if (LOGGER.isDebugEnabled()) {
658                LOGGER.debug(Util.unparse(query));
659            }
660
661            if (RolapUtil.MDX_LOGGER.isDebugEnabled()) {
662                RolapUtil.MDX_LOGGER.debug(currId + ": " + Util.unparse(query));
663            }
664
665            final Locus locus = new Locus(execution, null, "Loading cells");
666            Locus.push(locus);
667            Result result;
668            try {
669                statement.start(execution);
670                ((RolapCube) query.getCube()).clearCachedAggregations(true);
671                result = new RolapResult(execution, true);
672                int i = 0;
673                for (QueryAxis axis : query.getAxes()) {
674                    if (axis.isNonEmpty()) {
675                        result = new NonEmptyResult(result, execution, i);
676                    }
677                    ++i;
678                }
679            } finally {
680                Locus.pop(locus);
681                ((RolapCube) query.getCube()).clearCachedAggregations(true);
682            }
683            statement.end(execution);
684            return result;
685        } catch (ResultLimitExceededException e) {
686            // query has been punted
687            throw e;
688        } catch (Exception e) {
689            try {
690                if (!execution.isCancelOrTimeout()) {
691                    statement.end(execution);
692                }
693            } catch (Exception e1) {
694                // We can safely ignore that cleanup exception.
695                // If an error is encountered here, it means that
696                // one was already encountered at statement.start()
697                // above and the exception we will throw after the
698                // cleanup is the same as the original one.
699            }
700            String queryString;
701            try {
702                queryString = Util.unparse(query);
703            } catch (Exception e1) {
704                queryString = "?";
705            }
706            throw Util.newError(
707                e,
708                "Error while executing query [" + queryString + "]");
709        } finally {
710            mm.removeListener(listener);
711            if (RolapUtil.MDX_LOGGER.isDebugEnabled()) {
712                final long elapsed = execution.getElapsedMillis();
713                RolapUtil.MDX_LOGGER.debug(
714                    currId + ": exec: " + elapsed + " ms");
715            }
716        }
717    }
718
719    public void setRole(Role role) {
720        assert role != null;
721
722        this.role = role;
723        this.schemaReader = new RolapSchemaReader(role, schema);
724    }
725
726    public Role getRole() {
727        Util.assertPostcondition(role != null, "role != null");
728
729        return role;
730    }
731
732    public void setScenario(Scenario scenario) {
733        this.scenario = scenario;
734    }
735
736    public Scenario getScenario() {
737        return scenario;
738    }
739
740    /**
741     * Returns the server (mondrian instance) that this connection belongs to.
742     * Usually there is only one server instance in a given JVM.
743     *
744     * @return Server instance; never null
745     */
746    public MondrianServer getServer() {
747        return server;
748    }
749
750    public QueryPart parseStatement(String query) {
751        Statement statement = createInternalStatement(false);
752        final Locus locus =
753            new Locus(
754                new Execution(statement, 0),
755                "Parse/validate MDX statement",
756                null);
757        Locus.push(locus);
758        try {
759            QueryPart queryPart =
760                parseStatement(statement, query, null, false);
761            if (queryPart instanceof Query) {
762                ((Query) queryPart).setOwnStatement(true);
763                statement = null;
764            }
765            return queryPart;
766        } finally {
767            Locus.pop(locus);
768            if (statement != null) {
769                statement.close();
770            }
771        }
772    }
773
774    public Exp parseExpression(String expr) {
775        boolean debug = false;
776        if (getLogger().isDebugEnabled()) {
777            //debug = true;
778            getLogger().debug(
779                Util.nl
780                + expr);
781        }
782        final Statement statement = getInternalStatement();
783        try {
784            MdxParserValidator parser = createParser();
785            final FunTable funTable = getSchema().getFunTable();
786            return parser.parseExpression(statement, expr, debug, funTable);
787        } catch (Throwable exception) {
788            throw MondrianResource.instance().FailedToParseQuery.ex(
789                expr,
790                exception);
791        }
792    }
793
794    public Statement getInternalStatement() {
795        if (internalStatement == null) {
796            return schema.getInternalConnection().getInternalStatement();
797        } else {
798            return internalStatement;
799        }
800    }
801
802    private Statement createInternalStatement(boolean reentrant) {
803        final Statement statement =
804            reentrant
805                ? new ReentrantInternalStatement()
806                : new InternalStatement();
807        server.addStatement(statement);
808        return statement;
809    }
810
811    /**
812     * Implementation of {@link DataSource} which calls the good ol'
813     * {@link java.sql.DriverManager}.
814     *
815     * <p>Overrides {@link #hashCode()} and {@link #equals(Object)} so that
816     * {@link Dialect} objects can be cached more effectively.
817     */
818    private static class DriverManagerDataSource implements DataSource {
819        private final String jdbcConnectString;
820        private PrintWriter logWriter;
821        private int loginTimeout;
822        private Properties jdbcProperties;
823
824        public DriverManagerDataSource(
825            String jdbcConnectString,
826            Properties properties)
827        {
828            this.jdbcConnectString = jdbcConnectString;
829            this.jdbcProperties = properties;
830        }
831
832        @Override
833        public int hashCode() {
834            int h = loginTimeout;
835            h = Util.hash(h, jdbcConnectString);
836            h = Util.hash(h, jdbcProperties);
837            return h;
838        }
839
840        @Override
841        public boolean equals(Object obj) {
842            if (obj instanceof DriverManagerDataSource) {
843                DriverManagerDataSource
844                    that = (DriverManagerDataSource) obj;
845                return this.loginTimeout == that.loginTimeout
846                    && this.jdbcConnectString.equals(that.jdbcConnectString)
847                    && this.jdbcProperties.equals(that.jdbcProperties);
848            }
849            return false;
850        }
851
852        public Connection getConnection() throws SQLException {
853            return new org.apache.commons.dbcp.DelegatingConnection(
854                java.sql.DriverManager.getConnection(
855                    jdbcConnectString, jdbcProperties));
856        }
857
858        public Connection getConnection(String username, String password)
859            throws SQLException
860        {
861            if (jdbcProperties == null) {
862                return java.sql.DriverManager.getConnection(
863                    jdbcConnectString, username, password);
864            } else {
865                Properties temp = (Properties)jdbcProperties.clone();
866                temp.put("user", username);
867                temp.put("password", password);
868                return java.sql.DriverManager.getConnection(
869                    jdbcConnectString, temp);
870            }
871        }
872
873        public PrintWriter getLogWriter() throws SQLException {
874            return logWriter;
875        }
876
877        public void setLogWriter(PrintWriter out) throws SQLException {
878            logWriter = out;
879        }
880
881        public void setLoginTimeout(int seconds) throws SQLException {
882            loginTimeout = seconds;
883        }
884
885        public int getLoginTimeout() throws SQLException {
886            return loginTimeout;
887        }
888
889        public java.util.logging.Logger getParentLogger() {
890            return java.util.logging.Logger.getLogger("");
891        }
892
893        public <T> T unwrap(Class<T> iface) throws SQLException {
894            throw new SQLException("not a wrapper");
895        }
896
897        public boolean isWrapperFor(Class<?> iface) throws SQLException {
898            return false;
899        }
900    }
901
902    public DataSource getDataSource() {
903        return dataSource;
904    }
905
906    /**
907     * Helper method to allow olap4j wrappers to implement
908     * {@link org.olap4j.OlapConnection#createScenario()}.
909     *
910     * @return new Scenario
911     */
912    public ScenarioImpl createScenario() {
913        final ScenarioImpl scenario = new ScenarioImpl();
914        scenario.register(schema);
915        return scenario;
916    }
917
918    /**
919     * A <code>NonEmptyResult</code> filters a result by removing empty rows
920     * on a particular axis.
921     */
922    static class NonEmptyResult extends ResultBase {
923
924        final Result underlying;
925        private final int axis;
926        private final Map<Integer, Integer> map;
927        /** workspace. Synchronized access only. */
928        private final int[] pos;
929
930        /**
931         * Creates a NonEmptyResult.
932         *
933         * @param result Result set
934         * @param execution Execution context
935         * @param axis Which axis to make non-empty
936         */
937        NonEmptyResult(Result result, Execution execution, int axis) {
938            super(execution, result.getAxes().clone());
939
940            this.underlying = result;
941            this.axis = axis;
942            this.map = new HashMap<Integer, Integer>();
943            int axisCount = underlying.getAxes().length;
944            this.pos = new int[axisCount];
945            this.slicerAxis = underlying.getSlicerAxis();
946            TupleList tupleList =
947                ((RolapAxis) underlying.getAxes()[axis]).getTupleList();
948
949            final TupleList filteredTupleList;
950            if (!tupleList.isEmpty()
951                && tupleList.get(0).get(0).getDimension().isHighCardinality())
952            {
953                filteredTupleList =
954                    new DelegatingTupleList(
955                        tupleList.getArity(),
956                        new FilteredIterableList<List<Member>>(
957                            tupleList,
958                            new FilteredIterableList.Filter<List<Member>>() {
959                                public boolean accept(final List<Member> p) {
960                                    return p.get(0) != null;
961                                }
962                            }
963                        ));
964            } else {
965                filteredTupleList =
966                    TupleCollections.createList(tupleList.getArity());
967                int i = -1;
968                TupleCursor tupleCursor = tupleList.tupleCursor();
969                while (tupleCursor.forward()) {
970                    ++i;
971                    if (! isEmpty(i, axis)) {
972                        map.put(filteredTupleList.size(), i);
973                        filteredTupleList.addCurrent(tupleCursor);
974                    }
975                }
976            }
977            this.axes[axis] = new RolapAxis(filteredTupleList);
978        }
979
980        protected Logger getLogger() {
981            return LOGGER;
982        }
983
984        /**
985         * Returns true if all cells at a given offset on a given axis are
986         * empty. For example, in a 2x2x2 dataset, <code>isEmpty(1,0)</code>
987         * returns true if cells <code>{(1,0,0), (1,0,1), (1,1,0),
988         * (1,1,1)}</code> are all empty. As you can see, we hold the 0th
989         * coordinate fixed at 1, and vary all other coordinates over all
990         * possible values.
991         */
992        private boolean isEmpty(int offset, int fixedAxis) {
993            int axisCount = getAxes().length;
994            pos[fixedAxis] = offset;
995            return isEmptyRecurse(fixedAxis, axisCount - 1);
996        }
997
998        private boolean isEmptyRecurse(int fixedAxis, int axis) {
999            if (axis < 0) {
1000                RolapCell cell = (RolapCell) underlying.getCell(pos);
1001                return cell.isNull();
1002            } else if (axis == fixedAxis) {
1003                return isEmptyRecurse(fixedAxis, axis - 1);
1004            } else {
1005                List<Position> positions = getAxes()[axis].getPositions();
1006                final int positionCount = positions.size();
1007                for (int i = 0; i < positionCount; i++) {
1008                    pos[axis] = i;
1009                    if (!isEmptyRecurse(fixedAxis, axis - 1)) {
1010                        return false;
1011                    }
1012                }
1013                return true;
1014            }
1015        }
1016
1017        // synchronized because we use 'pos'
1018        public synchronized Cell getCell(int[] externalPos) {
1019            try {
1020                System.arraycopy(
1021                    externalPos, 0, this.pos, 0, externalPos.length);
1022                int offset = externalPos[axis];
1023                int mappedOffset = mapOffsetToUnderlying(offset);
1024                this.pos[axis] = mappedOffset;
1025                return underlying.getCell(this.pos);
1026            } catch (NullPointerException npe) {
1027                return underlying.getCell(externalPos);
1028            }
1029        }
1030
1031        private int mapOffsetToUnderlying(int offset) {
1032            return map.get(offset);
1033        }
1034
1035        public void close() {
1036            underlying.close();
1037        }
1038    }
1039
1040    /**
1041     * Data source that delegates all methods to an underlying data source.
1042     */
1043    private static abstract class DelegatingDataSource implements DataSource {
1044        protected final DataSource dataSource;
1045
1046        public DelegatingDataSource(DataSource dataSource) {
1047            this.dataSource = dataSource;
1048        }
1049
1050        public Connection getConnection() throws SQLException {
1051            return dataSource.getConnection();
1052        }
1053
1054        public Connection getConnection(
1055            String username,
1056            String password)
1057            throws SQLException
1058        {
1059            return dataSource.getConnection(username, password);
1060        }
1061
1062        public PrintWriter getLogWriter() throws SQLException {
1063            return dataSource.getLogWriter();
1064        }
1065
1066        public void setLogWriter(PrintWriter out) throws SQLException {
1067            dataSource.setLogWriter(out);
1068        }
1069
1070        public void setLoginTimeout(int seconds) throws SQLException {
1071            dataSource.setLoginTimeout(seconds);
1072        }
1073
1074        public int getLoginTimeout() throws SQLException {
1075            return dataSource.getLoginTimeout();
1076        }
1077
1078        // JDBC 4.0 support (JDK 1.6 and higher)
1079        public <T> T unwrap(Class<T> iface) throws SQLException {
1080            if (Util.JdbcVersion >= 0x0400) {
1081                // Do
1082                //              return dataSource.unwrap(iface);
1083                // via reflection.
1084                try {
1085                    Method method =
1086                        DataSource.class.getMethod("unwrap", Class.class);
1087                    return iface.cast(method.invoke(dataSource, iface));
1088                } catch (IllegalAccessException e) {
1089                    throw Util.newInternal(e, "While invoking unwrap");
1090                } catch (InvocationTargetException e) {
1091                    throw Util.newInternal(e, "While invoking unwrap");
1092                } catch (NoSuchMethodException e) {
1093                    throw Util.newInternal(e, "While invoking unwrap");
1094                }
1095            } else {
1096                if (iface.isInstance(dataSource)) {
1097                    return iface.cast(dataSource);
1098                } else {
1099                    return null;
1100                }
1101            }
1102        }
1103
1104        // JDBC 4.0 support (JDK 1.6 and higher)
1105        public boolean isWrapperFor(Class<?> iface) throws SQLException {
1106            if (Util.JdbcVersion >= 0x0400) {
1107                // Do
1108                //              return dataSource.isWrapperFor(iface);
1109                // via reflection.
1110                try {
1111                    Method method =
1112                        DataSource.class.getMethod(
1113                            "isWrapperFor", boolean.class);
1114                    return (Boolean) method.invoke(dataSource, iface);
1115                } catch (IllegalAccessException e) {
1116                    throw Util.newInternal(e, "While invoking isWrapperFor");
1117                } catch (InvocationTargetException e) {
1118                    throw Util.newInternal(e, "While invoking isWrapperFor");
1119                } catch (NoSuchMethodException e) {
1120                    throw Util.newInternal(e, "While invoking isWrapperFor");
1121                }
1122            } else {
1123                return iface.isInstance(dataSource);
1124            }
1125        }
1126
1127        // JDBC 4.1 support (JDK 1.7 and higher)
1128        public java.util.logging.Logger getParentLogger() {
1129            if (Util.JdbcVersion >= 0x0401) {
1130                // Do
1131                //              return dataSource.getParentLogger();
1132                // via reflection.
1133                try {
1134                    Method method =
1135                        DataSource.class.getMethod("getParentLogger");
1136                    return (java.util.logging.Logger) method.invoke(dataSource);
1137                } catch (IllegalAccessException e) {
1138                    throw Util.newInternal(e, "While invoking getParentLogger");
1139                } catch (InvocationTargetException e) {
1140                    throw Util.newInternal(e, "While invoking getParentLogger");
1141                } catch (NoSuchMethodException e) {
1142                    throw Util.newInternal(e, "While invoking getParentLogger");
1143                }
1144            } else {
1145                // Can't throw SQLFeatureNotSupportedException... it doesn't
1146                // exist before JDBC 4.1.
1147                throw new UnsupportedOperationException();
1148            }
1149        }
1150    }
1151
1152    /**
1153     * Data source that gets connections from an underlying data source but
1154     * with different user name and password.
1155     */
1156    private static class UserPasswordDataSource extends DelegatingDataSource {
1157        private final String jdbcUser;
1158        private final String jdbcPassword;
1159
1160        /**
1161         * Creates a UserPasswordDataSource
1162         *
1163         * @param dataSource Underlying data source
1164         * @param jdbcUser User name
1165         * @param jdbcPassword Password
1166         */
1167        public UserPasswordDataSource(
1168            DataSource dataSource,
1169            String jdbcUser,
1170            String jdbcPassword)
1171        {
1172            super(dataSource);
1173            this.jdbcUser = jdbcUser;
1174            this.jdbcPassword = jdbcPassword;
1175        }
1176
1177        public Connection getConnection() throws SQLException {
1178            return dataSource.getConnection(jdbcUser, jdbcPassword);
1179        }
1180    }
1181
1182    /**
1183     * <p>Implementation of {@link Statement} for use when you don't have an
1184     * olap4j connection.</p>
1185     */
1186    private class InternalStatement extends StatementImpl {
1187        private boolean closed = false;
1188
1189        public void close() {
1190            if (!closed) {
1191                closed = true;
1192                server.removeStatement(this);
1193            }
1194        }
1195
1196        public RolapConnection getMondrianConnection() {
1197            return RolapConnection.this;
1198        }
1199    }
1200
1201    /**
1202     * <p>A statement that can be used for all of the various internal
1203     * operations, such as resolving MDX identifiers, that require a
1204     * {@link Statement} and an {@link Execution}.
1205     *
1206     * <p>The statement needs to be reentrant because there are many such
1207     * operations; several of these operations might be active at one time. We
1208     * don't want to create a new statement for each, but just one internal
1209     * statement for each connection. The statement shouldn't have a unique
1210     * execution. For this reason, we don't use the inherited {@link #execution}
1211     * field.</p>
1212     *
1213     * <p>But there is a drawback. If we can't find the unique execution, the
1214     * statement cannot be canceled or time out. If you want that behavior
1215     * from an internal statement, use the base class: create a new
1216     * {@link InternalStatement} for each operation.</p>
1217     */
1218    private class ReentrantInternalStatement extends InternalStatement {
1219        @Override
1220        public void start(Execution execution) {
1221            // Unlike StatementImpl, there is not a unique execution. An
1222            // internal statement can execute several at the same time. So,
1223            // we don't set this.execution.
1224            execution.start();
1225        }
1226
1227        @Override
1228        public void end(Execution execution) {
1229            execution.end();
1230        }
1231
1232        @Override
1233        public void close() {
1234            // do not close
1235        }
1236    }
1237}
1238
1239// End RolapConnection.java