/*
// This software is subject to the terms of the Eclipse Public License v1.0
// Agreement, available at the following URL:
// http://www.eclipse.org/legal/epl-v10.html.
// You must accept the terms of that agreement to use this software.
//
// Copyright (C) 2001-2005 Julian Hyde
// Copyright (C) 2005-2015 Pentaho and others
// All Rights Reserved.
*/
package mondrian.rolap;

import mondrian.calc.*;
import mondrian.calc.impl.DelegatingTupleList;
import mondrian.olap.*;
import mondrian.parser.MdxParserValidator;
import mondrian.resource.MondrianResource;
import mondrian.server.*;
import mondrian.spi.*;
import mondrian.spi.impl.JndiDataSourceResolver;
import mondrian.util.*;

import org.apache.log4j.Logger;

import org.eigenbase.util.property.StringProperty;

import org.olap4j.Scenario;

import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;

import javax.sql.DataSource;

/**
 * A <code>RolapConnection</code> is a connection to a Mondrian OLAP Server.
 *
 * <p>Typically, you create a connection via
 * {@link DriverManager#getConnection(String, mondrian.spi.CatalogLocator)}.
 * {@link RolapConnectionProperties} describes allowable keywords.</p>
 *
 * @see RolapSchema
 * @see DriverManager
 * @author jhyde
 * @since 2 October, 2002
 */
public class RolapConnection extends ConnectionBase {
    private static final Logger LOGGER =
        Logger.getLogger(RolapConnection.class);
    private static final AtomicInteger ID_GENERATOR = new AtomicInteger();

    private final MondrianServer server;

    private final Util.PropertyList connectInfo;

    /**
     * Factory for JDBC connections to talk to the RDBMS. This factory will
     * usually use a connection pool.
     */
    private final DataSource dataSource;
    private final String catalogUrl;
    private final RolapSchema schema;
    private SchemaReader schemaReader;
    protected Role role;
    private Locale locale = Locale.getDefault();
    private Scenario scenario;
    private boolean closed = false;

    private static DataSourceResolver dataSourceResolver;
    private final int id;
    private final Statement internalStatement;

    /**
     * Creates a connection.
     *
     * @param server Server instance this connection belongs to
     * @param connectInfo Connection properties; keywords are described in
     *   {@link RolapConnectionProperties}.
     * @param dataSource JDBC data source
     */
    public RolapConnection(
        MondrianServer server,
        Util.PropertyList connectInfo,
        DataSource dataSource)
    {
        this(server, connectInfo, null, dataSource);
    }

    /**
     * Creates a RolapConnection.
     *
     * <p>Only {@link RolapSchemaPool#get} calls this with
     * schema != null (to create a schema's internal connection).
     * Other uses retrieve a schema from the cache based upon
     * the <code>Catalog</code> property.
     *
     * @param server Server instance this connection belongs to
     * @param connectInfo Connection properties; keywords are described in
     *   {@link RolapConnectionProperties}.
     * @param schema Schema for the connection. Must be null unless this is to
     *   be an internal connection.
     * @param dataSource If not null an external DataSource to be used
     *        by Mondrian
     */
    RolapConnection(
        MondrianServer server,
        Util.PropertyList connectInfo,
        RolapSchema schema,
        DataSource dataSource)
    {
        super();
        assert server != null;
        this.server = server;
        this.id = ID_GENERATOR.getAndIncrement();

        assert connectInfo != null;
        String provider = connectInfo.get(
            RolapConnectionProperties.Provider.name(), "mondrian");
        Util.assertTrue(provider.equalsIgnoreCase("mondrian"));
        this.connectInfo = connectInfo;
        this.catalogUrl =
            connectInfo.get(RolapConnectionProperties.Catalog.name());
        final String jdbcUser =
            connectInfo.get(RolapConnectionProperties.JdbcUser.name());
        final String jdbcConnectString =
            connectInfo.get(RolapConnectionProperties.Jdbc.name());
        final String strDataSource =
            connectInfo.get(RolapConnectionProperties.DataSource.name());
        StringBuilder buf = new StringBuilder();
        this.dataSource =
            createDataSource(dataSource, connectInfo, buf);
        Role role = null;

        // Register this connection before we register its internal statement.
        server.addConnection(this);

        if (schema == null) {
            // If RolapSchema.Pool.get were to call this with schema == null,
            // we would loop.
            Statement bootstrapStatement = createInternalStatement(false);
            final Locus locus =
                new Locus(
                    new Execution(bootstrapStatement, 0),
                    null,
                    "Initializing connection");
            Locus.push(locus);
            try {
                if (dataSource == null) {
                    // If there is no external data source is passed in, we
                    // expect the properties Jdbc, JdbcUser, DataSource to be
                    // set, as they are used to generate the schema cache key.
                    final String connectionKey =
                        jdbcConnectString
                        + getJdbcProperties(connectInfo).toString();

                    schema = RolapSchemaPool.instance().get(
                        catalogUrl,
                        connectionKey,
                        jdbcUser,
                        strDataSource,
                        connectInfo);
                } else {
                    schema = RolapSchemaPool.instance().get(
                        catalogUrl,
                        dataSource,
                        connectInfo);
                }
            } finally {
                Locus.pop(locus);
                bootstrapStatement.close();
            }
            internalStatement =
                schema.getInternalConnection().getInternalStatement();
            String roleNameList =
                connectInfo.get(RolapConnectionProperties.Role.name());
            if (roleNameList != null) {
                List<String> roleNames = Util.parseCommaList(roleNameList);
                List<Role> roleList = new ArrayList<Role>();
                for (String roleName : roleNames) {
                    final LockBox.Entry entry =
                        server.getLockBox().get(roleName);
                    Role role1;
                    if (entry != null) {
                        try {
                            role1 = (Role) entry.getValue();
                        } catch (ClassCastException e) {
                            role1 = null;
                        }
                    } else {
                        role1 = schema.lookupRole(roleName);
                    }
                    if (role1 == null) {
                        throw Util.newError(
                            "Role '" + roleName + "' not found");
                    }
                    roleList.add(role1);
                }
                switch (roleList.size()) {
                case 0:
                    // If they specify 'Role=;', the list of names will be
                    // empty, and the effect will be as if they did specify
                    // Role at all.
                    role = null;
                    break;
                case 1:
                    role = roleList.get(0);
                    break;
                default:
                    role = RoleImpl.union(roleList);
                    break;
                }
            }
        } else {
            this.internalStatement = createInternalStatement(true);

            // We are creating an internal connection. Now is a great time to
            // make sure that the JDBC credentials are valid, for this
            // connection and for external connections built on top of this.
            Connection conn = null;
            java.sql.Statement statement = null;
            try {
                conn = this.dataSource.getConnection();
                Dialect dialect =
                    DialectManager.createDialect(this.dataSource, conn);
                if (dialect.getDatabaseProduct()
                    == Dialect.DatabaseProduct.DERBY)
                {
                    // Derby requires a little extra prodding to do the
                    // validation to detect an error.
                    statement = conn.createStatement();
                    statement.executeQuery("select * from bogustable");
                }
            } catch (SQLException e) {
                if (e.getMessage().equals(
                        "Table/View 'BOGUSTABLE' does not exist."))
                {
                    // Ignore. This exception comes from Derby when the
                    // connection is valid. If the connection were invalid, we
                    // would receive an error such as "Schema 'BOGUSUSER' does
                    // not exist"
                } else {
                    throw Util.newError(
                        e,
                        "Error while creating SQL connection: " + buf);
                }
            } finally {
                try {
                    if (statement != null) {
                        statement.close();
                    }
                    if (conn != null) {
                        conn.close();
                    }
                } catch (SQLException e) {
                    // ignore
                }
            }
        }

        if (role == null) {
            role = schema.getDefaultRole();
        }

        // Set the locale.
        String localeString =
            connectInfo.get(RolapConnectionProperties.Locale.name());
        if (localeString != null) {
            this.locale = Util.parseLocale(localeString);
            assert locale != null;
        }

        this.schema = schema;
        setRole(role);
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            super.finalize();
            close();
        } catch (Throwable t) {
            LOGGER.info(
                MondrianResource.instance()
                    .FinalizerErrorRolapConnection.baseMessage,
                t);
        }
    }

    /**
     * Returns the identifier of this connection. Unique within the lifetime of
     * this JVM.
     *
     * @return Identifier of this connection
     */
    public int getId() {
        return id;
    }

    protected Logger getLogger() {
        return LOGGER;
    }

    /**
     * Creates a JDBC data source from the JDBC credentials contained within a
     * set of mondrian connection properties.
     *
     * <p>This method is package-level so that it can be called from the
     * RolapConnectionTest unit test.
     *
     * @param dataSource Anonymous data source from user, or null
     * @param connectInfo Mondrian connection properties
     * @param buf Into which method writes a description of the JDBC credentials
     * @return Data source
     */
    static DataSource createDataSource(
        DataSource dataSource,
        Util.PropertyList connectInfo,
        StringBuilder buf)
    {
        assert buf != null;
        final String jdbcConnectString =
            connectInfo.get(RolapConnectionProperties.Jdbc.name());
        final String jdbcUser =
            connectInfo.get(RolapConnectionProperties.JdbcUser.name());
        final String jdbcPassword =
            connectInfo.get(RolapConnectionProperties.JdbcPassword.name());
        final String dataSourceName =
            connectInfo.get(RolapConnectionProperties.DataSource.name());

        if (dataSource != null) {
            appendKeyValue(buf, "Anonymous data source", dataSource);
            appendKeyValue(
                buf, RolapConnectionProperties.JdbcUser.name(), jdbcUser);
            appendKeyValue(
                buf,
                RolapConnectionProperties.JdbcPassword.name(),
                jdbcPassword);
            if (jdbcUser != null || jdbcPassword != null) {
                dataSource =
                    new UserPasswordDataSource(
                        dataSource, jdbcUser, jdbcPassword);
            }
            return dataSource;

        } else if (jdbcConnectString != null) {
            // Get connection through own pooling datasource
            appendKeyValue(
                buf, RolapConnectionProperties.Jdbc.name(), jdbcConnectString);
            appendKeyValue(
                buf, RolapConnectionProperties.JdbcUser.name(), jdbcUser);
            appendKeyValue(
                buf,
                RolapConnectionProperties.JdbcPassword.name(),
                jdbcPassword);
            String jdbcDrivers =
                connectInfo.get(RolapConnectionProperties.JdbcDrivers.name());
            if (jdbcDrivers != null) {
                RolapUtil.loadDrivers(jdbcDrivers);
            }
            final String jdbcDriversProp =
                    MondrianProperties.instance().JdbcDrivers.get();
            RolapUtil.loadDrivers(jdbcDriversProp);

            Properties jdbcProperties = getJdbcProperties(connectInfo);
            final Map<String, String> map = Util.toMap(jdbcProperties);
            for (Map.Entry<String, String> entry : map.entrySet()) {
                // FIXME ordering is non-deterministic
                appendKeyValue(buf, entry.getKey(), entry.getValue());
            }

            if (jdbcUser != null) {
                jdbcProperties.put("user", jdbcUser);
            }
            if (jdbcPassword != null) {
                jdbcProperties.put("password", jdbcPassword);
            }

            // JDBC connections are dumb beasts, so we assume they're not
            // pooled. Therefore the default is true.
            final boolean poolNeeded =
                connectInfo.get(
                    RolapConnectionProperties.PoolNeeded.name(),
                    "true").equalsIgnoreCase("true");

            if (!poolNeeded) {
                // Connection is already pooled; don't pool it again.
                return new DriverManagerDataSource(
                    jdbcConnectString,
                    jdbcProperties);
            }

            if (jdbcConnectString.toLowerCase().indexOf("mysql") > -1) {
                // mysql driver needs this autoReconnect parameter
                jdbcProperties.setProperty("autoReconnect", "true");
            }
            return RolapConnectionPool.instance()
                .getDriverManagerPoolingDataSource(
                    jdbcConnectString, jdbcProperties);

        } else if (dataSourceName != null) {
            appendKeyValue(
                buf,
                RolapConnectionProperties.DataSource.name(),
                dataSourceName);
            appendKeyValue(
                buf,
                RolapConnectionProperties.JdbcUser.name(),
                jdbcUser);
            appendKeyValue(
                buf,
                RolapConnectionProperties.JdbcPassword.name(),
                jdbcPassword);

            // Data sources are fairly smart, so we assume they look after
            // their own pooling. Therefore the default is false.
            final boolean poolNeeded =
                connectInfo.get(
                    RolapConnectionProperties.PoolNeeded.name(),
                    "false").equalsIgnoreCase("true");

            // Get connection from datasource.
            DataSourceResolver dataSourceResolver = getDataSourceResolver();
            try {
                dataSource = dataSourceResolver.lookup(dataSourceName);
            } catch (Exception e) {
                throw Util.newInternal(
                    e,
                    "Error while looking up data source ("
                    + dataSourceName + ")");
            }
            if (poolNeeded) {
                dataSource =
                    RolapConnectionPool.instance()
                        .getDataSourcePoolingDataSource(
                            dataSource, dataSourceName, jdbcUser, jdbcPassword);
            } else {
                if (jdbcUser != null || jdbcPassword != null) {
                    dataSource =
                        new UserPasswordDataSource(
                            dataSource, jdbcUser, jdbcPassword);
                }
            }
            return dataSource;
        } else {
            throw Util.newInternal(
                "Connect string '" + connectInfo.toString()
                + "' must contain either '" + RolapConnectionProperties.Jdbc
                + "' or '" + RolapConnectionProperties.DataSource + "'");
        }
    }

    /**
     * Returns the instance of the {@link mondrian.spi.DataSourceResolver}
     * plugin.
     *
     * @return data source resolver
     */
    private static synchronized DataSourceResolver getDataSourceResolver() {
        if (dataSourceResolver == null) {
            final StringProperty property =
                MondrianProperties.instance().DataSourceResolverClass;
            final String className =
                property.get(
                    JndiDataSourceResolver.class.getName());
            try {
                dataSourceResolver =
                    ClassResolver.INSTANCE.instantiateSafe(className);
            } catch (ClassCastException e) {
                throw Util.newInternal(
                    e,
                    "Plugin class specified by property "
                    + property.getPath()
                    + " must implement "
                    + DataSourceResolver.class.getName());
            }
        }
        return dataSourceResolver;
    }

    /**
     * Appends "key=value" to a buffer, if value is not null.
     *
     * @param buf Buffer
     * @param key Key
     * @param value Value
     */
    private static void appendKeyValue(
        StringBuilder buf,
        String key,
        Object value)
    {
        if (value != null) {
            if (buf.length() > 0) {
                buf.append("; ");
            }
            buf.append(key).append('=').append(value);
        }
    }

    /**
     * Creates a {@link Properties} object containing all of the JDBC
     * connection properties present in the
     * {@link mondrian.olap.Util.PropertyList connectInfo}.
     *
     * @param connectInfo Connection properties
     * @return The JDBC connection properties.
     */
    private static Properties getJdbcProperties(Util.PropertyList connectInfo) {
        Properties jdbcProperties = new Properties();
        for (Pair<String, String> entry : connectInfo) {
            if (entry.left.startsWith(
                    RolapConnectionProperties.JdbcPropertyPrefix))
            {
                jdbcProperties.put(
                    entry.left.substring(
                        RolapConnectionProperties.JdbcPropertyPrefix.length()),
                    entry.right);
            }
        }
        return jdbcProperties;
    }

    public Util.PropertyList getConnectInfo() {
        return connectInfo;
    }

    public void close() {
        if (!closed) {
            closed = true;
            server.removeConnection(this);
        }
        if (internalStatement != null) {
            internalStatement.close();
        }
    }

    public RolapSchema getSchema() {
        return schema;
    }

    public String getConnectString() {
        return connectInfo.toString();
    }

    public String getCatalogName() {
        return catalogUrl;
    }

    public Locale getLocale() {
        return locale;
    }

    public void setLocale(Locale locale) {
        if (locale == null) {
            throw new IllegalArgumentException("locale must not be null");
        }
        this.locale = locale;
    }

    public SchemaReader getSchemaReader() {
        return schemaReader;
    }

    public Object getProperty(String name) {
        // Mask out the values of certain properties.
        if (name.equals(RolapConnectionProperties.JdbcPassword.name())
            || name.equals(RolapConnectionProperties.CatalogContent.name()))
        {
            return "";
        }
        return connectInfo.get(name);
    }

    public CacheControl getCacheControl(PrintWriter pw) {
        return getServer().getAggregationManager().getCacheControl(this, pw);
    }

    /**
     * Executes a Query.
     *
     * @param query Query parse tree
     *
     * @throws ResourceLimitExceededException if some resource limit specified
     *     in the property file was exceeded
     * @throws QueryCanceledException if query was canceled during execution
     * @throws QueryTimeoutException if query exceeded timeout specified in
     *     the property file
     *
     * @deprecated Use {@link #execute(mondrian.server.Execution)}; this method
     *     will be removed in mondrian-4.0
     */
    public Result execute(Query query) {
        final Statement statement = query.getStatement();
        Execution execution =
            new Execution(statement, statement.getQueryTimeoutMillis());
        return execute(execution);
    }

    /**
     * Executes a statement.
     *
     * @param execution Execution context (includes statement, query)
     *
     * @throws ResourceLimitExceededException if some resource limit specified
     *     in the property file was exceeded
     * @throws QueryCanceledException if query was canceled during execution
     * @throws QueryTimeoutException if query exceeded timeout specified in
     *     the property file
     */
    public Result execute(final Execution execution) {
        execution.copyMDC();
        return
            server.getResultShepherd()
                .shepherdExecution(
                    execution,
                    new Callable<Result>() {
                        public Result call() throws Exception {
                            return executeInternal(execution);
                        }
                    });
    }

    private Result executeInternal(final Execution execution) {
        execution.setContextMap();
        final Statement statement = execution.getMondrianStatement();
        // Cleanup any previous executions still running
        synchronized (statement) {
            final Execution previousExecution =
                statement.getCurrentExecution();
            if (previousExecution != null) {
                statement.end(previousExecution);
            }
        }
        final Query query = statement.getQuery();
        final MemoryMonitor.Listener listener = new MemoryMonitor.Listener() {
            public void memoryUsageNotification(long used, long max) {
                execution.setOutOfMemory(
                    "OutOfMemory used="
                    + used
                    + ", max="
                    + max
                    + " for connection: "
                    + getConnectString());
            }
        };
        MemoryMonitor mm = MemoryMonitorFactory.getMemoryMonitor();
        final long currId = execution.getId();
        try {
            mm.addListener(listener);
            // Check to see if we must punt
            execution.checkCancelOrTimeout();

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(Util.unparse(query));
            }

            if (RolapUtil.MDX_LOGGER.isDebugEnabled()) {
                RolapUtil.MDX_LOGGER.debug(currId + ": " + Util.unparse(query));
            }

            final Locus locus = new Locus(execution, null, "Loading cells");
            Locus.push(locus);
            Result result;
            try {
                statement.start(execution);
                ((RolapCube) query.getCube()).clearCachedAggregations(true);
                result = new RolapResult(execution, true);
                int i = 0;
                for (QueryAxis axis : query.getAxes()) {
                    if (axis.isNonEmpty()) {
                        result = new NonEmptyResult(result, execution, i);
                    }
                    ++i;
                }
            } finally {
                Locus.pop(locus);
                ((RolapCube) query.getCube()).clearCachedAggregations(true);
            }
            statement.end(execution);
            return result;
        } catch (ResultLimitExceededException e) {
            // query has been punted
            throw e;
        } catch (Exception e) {
            try {
                if (!execution.isCancelOrTimeout()) {
                    statement.end(execution);
                }
            } catch (Exception e1) {
                // We can safely ignore that cleanup exception.
                // If an error is encountered here, it means that
                // one was already encountered at statement.start()
                // above and the exception we will throw after the
                // cleanup is the same as the original one.
            }
            String queryString;
            try {
                queryString = Util.unparse(query);
            } catch (Exception e1) {
                queryString = "?";
            }
            throw Util.newError(
                e,
                "Error while executing query [" + queryString + "]");
        } finally {
            mm.removeListener(listener);
            if (RolapUtil.MDX_LOGGER.isDebugEnabled()) {
                final long elapsed = execution.getElapsedMillis();
                RolapUtil.MDX_LOGGER.debug(
                    currId + ": exec: " + elapsed + " ms");
            }
        }
    }

    public void setRole(Role role) {
        assert role != null;

        this.role = role;
        this.schemaReader = new RolapSchemaReader(role, schema);
    }

    public Role getRole() {
        Util.assertPostcondition(role != null, "role != null");

        return role;
    }

    public void setScenario(Scenario scenario) {
        this.scenario = scenario;
    }

    public Scenario getScenario() {
        return scenario;
    }

    /**
     * Returns the server (mondrian instance) that this connection belongs to.
     * Usually there is only one server instance in a given JVM.
     *
     * @return Server instance; never null
     */
    public MondrianServer getServer() {
        return server;
    }

    public QueryPart parseStatement(String query) {
        Statement statement = createInternalStatement(false);
        final Locus locus =
            new Locus(
                new Execution(statement, 0),
                "Parse/validate MDX statement",
                null);
        Locus.push(locus);
        try {
            QueryPart queryPart =
                parseStatement(statement, query, null, false);
            if (queryPart instanceof Query) {
                ((Query) queryPart).setOwnStatement(true);
                statement = null;
            }
            return queryPart;
        } finally {
            Locus.pop(locus);
            if (statement != null) {
                statement.close();
            }
        }
    }

    public Exp parseExpression(String expr) {
        boolean debug = false;
        if (getLogger().isDebugEnabled()) {
            //debug = true;
            getLogger().debug(
                Util.nl
                + expr);
        }
        final Statement statement = getInternalStatement();
        try {
            MdxParserValidator parser = createParser();
            final FunTable funTable = getSchema().getFunTable();
            return parser.parseExpression(statement, expr, debug, funTable);
        } catch (Throwable exception) {
            throw MondrianResource.instance().FailedToParseQuery.ex(
                expr,
                exception);
        }
    }

    public Statement getInternalStatement() {
        if (internalStatement == null) {
            return schema.getInternalConnection().getInternalStatement();
        } else {
            return internalStatement;
        }
    }

    private Statement createInternalStatement(boolean reentrant) {
        final Statement statement =
            reentrant
                ? new ReentrantInternalStatement()
                : new InternalStatement();
        server.addStatement(statement);
        return statement;
    }

    /**
     * Implementation of {@link DataSource} which calls the good ol'
     * {@link java.sql.DriverManager}.
     *
     * <p>Overrides {@link #hashCode()} and {@link #equals(Object)} so that
     * {@link Dialect} objects can be cached more effectively.
     */
    private static class DriverManagerDataSource implements DataSource {
        private final String jdbcConnectString;
        private PrintWriter logWriter;
        private int loginTimeout;
        private Properties jdbcProperties;

        public DriverManagerDataSource(
            String jdbcConnectString,
            Properties properties)
        {
            this.jdbcConnectString = jdbcConnectString;
            this.jdbcProperties = properties;
        }

        @Override
        public int hashCode() {
            int h = loginTimeout;
            h = Util.hash(h, jdbcConnectString);
            h = Util.hash(h, jdbcProperties);
            return h;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof DriverManagerDataSource) {
                DriverManagerDataSource
                    that = (DriverManagerDataSource) obj;
                return this.loginTimeout == that.loginTimeout
                    && this.jdbcConnectString.equals(that.jdbcConnectString)
                    && this.jdbcProperties.equals(that.jdbcProperties);
            }
            return false;
        }

        public Connection getConnection() throws SQLException {
            return new org.apache.commons.dbcp.DelegatingConnection(
                java.sql.DriverManager.getConnection(
                    jdbcConnectString, jdbcProperties));
        }

        public Connection getConnection(String username, String password)
            throws SQLException
        {
            if (jdbcProperties == null) {
                return java.sql.DriverManager.getConnection(
                    jdbcConnectString, username, password);
            } else {
                Properties temp = (Properties)jdbcProperties.clone();
                temp.put("user", username);
                temp.put("password", password);
                return java.sql.DriverManager.getConnection(
                    jdbcConnectString, temp);
            }
        }

        public PrintWriter getLogWriter() throws SQLException {
            return logWriter;
        }

        public void setLogWriter(PrintWriter out) throws SQLException {
            logWriter = out;
        }

        public void setLoginTimeout(int seconds) throws SQLException {
            loginTimeout = seconds;
        }

        public int getLoginTimeout() throws SQLException {
            return loginTimeout;
        }

        public java.util.logging.Logger getParentLogger() {
            return java.util.logging.Logger.getLogger("");
        }

        public <T> T unwrap(Class<T> iface) throws SQLException {
            throw new SQLException("not a wrapper");
        }

        public boolean isWrapperFor(Class<?> iface) throws SQLException {
            return false;
        }
    }

    public DataSource getDataSource() {
        return dataSource;
    }

    /**
     * Helper method to allow olap4j wrappers to implement
     * {@link org.olap4j.OlapConnection#createScenario()}.
     *
     * @return new Scenario
     */
    public ScenarioImpl createScenario() {
        final ScenarioImpl scenario = new ScenarioImpl();
        scenario.register(schema);
        return scenario;
    }

    /**
     * A <code>NonEmptyResult</code> filters a result by removing empty rows
     * on a particular axis.
     */
    static class NonEmptyResult extends ResultBase {

        final Result underlying;
        private final int axis;
        private final Map<Integer, Integer> map;
        /** workspace. Synchronized access only. */
        private final int[] pos;

        /**
         * Creates a NonEmptyResult.
         *
         * @param result Result set
         * @param execution Execution context
         * @param axis Which axis to make non-empty
         */
        NonEmptyResult(Result result, Execution execution, int axis) {
            super(execution, result.getAxes().clone());

            this.underlying = result;
            this.axis = axis;
            this.map = new HashMap<Integer, Integer>();
            int axisCount = underlying.getAxes().length;
            this.pos = new int[axisCount];
            this.slicerAxis = underlying.getSlicerAxis();
            TupleList tupleList =
                ((RolapAxis) underlying.getAxes()[axis]).getTupleList();

            final TupleList filteredTupleList;
            if (!tupleList.isEmpty()
                    && tupleList.get(0).get(0).getDimension()
                        .isHighCardinality())
            {
                LOGGER.warn(
                    MondrianResource.instance()
                        .HighCardinalityInDimension.str(
                            tupleList.get(0).get(0).getDimension()
                                .getUniqueName()));
                filteredTupleList =
                    new DelegatingTupleList(
                        tupleList.getArity(),
                        new FilteredIterableList<List<Member>>(
                            tupleList,
                            new FilteredIterableList.Filter<List<Member>>() {
                                public boolean accept(final List<Member> p) {
                                    return p.get(0) != null;
                                }
                            }
                        ));
            } else {
                filteredTupleList =
                    TupleCollections.createList(tupleList.getArity());
                int i = -1;
                TupleCursor tupleCursor = tupleList.tupleCursor();
                while (tupleCursor.forward()) {
                    ++i;
                    if (! isEmpty(i, axis)) {
                        map.put(filteredTupleList.size(), i);
                        filteredTupleList.addCurrent(tupleCursor);
                    }
                }
            }
            this.axes[axis] = new RolapAxis(filteredTupleList);
        }

        protected Logger getLogger() {
            return LOGGER;
        }

        /**
         * Returns true if all cells at a given offset on a given axis are
         * empty. For example, in a 2x2x2 dataset, <code>isEmpty(1,0)</code>
         * returns true if cells <code>{(1,0,0), (1,0,1), (1,1,0),
         * (1,1,1)}</code> are all empty. As you can see, we hold the 0th
         * coordinate fixed at 1, and vary all other coordinates over all
         * possible values.
         */
        private boolean isEmpty(int offset, int fixedAxis) {
            int axisCount = getAxes().length;
            pos[fixedAxis] = offset;
            return isEmptyRecurse(fixedAxis, axisCount - 1);
        }

        private boolean isEmptyRecurse(int fixedAxis, int axis) {
            if (axis < 0) {
                RolapCell cell = (RolapCell) underlying.getCell(pos);
                return cell.isNull();
            } else if (axis == fixedAxis) {
                return isEmptyRecurse(fixedAxis, axis - 1);
            } else {
                List<Position> positions = getAxes()[axis].getPositions();
                final int positionCount = positions.size();
                for (int i = 0; i < positionCount; i++) {
                    pos[axis] = i;
                    if (!isEmptyRecurse(fixedAxis, axis - 1)) {
                        return false;
                    }
                }
                return true;
            }
        }

        // synchronized because we use 'pos'
        public synchronized Cell getCell(int[] externalPos) {
            try {
                System.arraycopy(
                    externalPos, 0, this.pos, 0, externalPos.length);
                int offset = externalPos[axis];
                int mappedOffset = mapOffsetToUnderlying(offset);
                this.pos[axis] = mappedOffset;
                return underlying.getCell(this.pos);
            } catch (NullPointerException npe) {
                return underlying.getCell(externalPos);
            }
        }

        private int mapOffsetToUnderlying(int offset) {
            return map.get(offset);
        }

        public void close() {
            underlying.close();
        }
    }

    /**
     * Data source that delegates all methods to an underlying data source.
     */
    private static abstract class DelegatingDataSource implements DataSource {
        protected final DataSource dataSource;

        public DelegatingDataSource(DataSource dataSource) {
            this.dataSource = dataSource;
        }

        public Connection getConnection() throws SQLException {
            return dataSource.getConnection();
        }

        public Connection getConnection(
            String username,
            String password)
            throws SQLException
        {
            return dataSource.getConnection(username, password);
        }

        public PrintWriter getLogWriter() throws SQLException {
            return dataSource.getLogWriter();
        }

        public void setLogWriter(PrintWriter out) throws SQLException {
            dataSource.setLogWriter(out);
        }

        public void setLoginTimeout(int seconds) throws SQLException {
            dataSource.setLoginTimeout(seconds);
        }

        public int getLoginTimeout() throws SQLException {
            return dataSource.getLoginTimeout();
        }

        // JDBC 4.0 support (JDK 1.6 and higher)
        public <T> T unwrap(Class<T> iface) throws SQLException {
            if (Util.JdbcVersion >= 0x0400) {
                // Do
                //              return dataSource.unwrap(iface);
                // via reflection.
                try {
                    Method method =
                        DataSource.class.getMethod("unwrap", Class.class);
                    return iface.cast(method.invoke(dataSource, iface));
                } catch (IllegalAccessException e) {
                    throw Util.newInternal(e, "While invoking unwrap");
                } catch (InvocationTargetException e) {
                    throw Util.newInternal(e, "While invoking unwrap");
                } catch (NoSuchMethodException e) {
                    throw Util.newInternal(e, "While invoking unwrap");
                }
            } else {
                if (iface.isInstance(dataSource)) {
                    return iface.cast(dataSource);
                } else {
                    return null;
                }
            }
        }

        // JDBC 4.0 support (JDK 1.6 and higher)
        public boolean isWrapperFor(Class<?> iface) throws SQLException {
            if (Util.JdbcVersion >= 0x0400) {
                // Do
                //              return dataSource.isWrapperFor(iface);
                // via reflection.
                try {
                    Method method =
                        DataSource.class.getMethod(
                            "isWrapperFor", boolean.class);
                    return (Boolean) method.invoke(dataSource, iface);
                } catch (IllegalAccessException e) {
                    throw Util.newInternal(e, "While invoking isWrapperFor");
                } catch (InvocationTargetException e) {
                    throw Util.newInternal(e, "While invoking isWrapperFor");
                } catch (NoSuchMethodException e) {
                    throw Util.newInternal(e, "While invoking isWrapperFor");
                }
            } else {
                return iface.isInstance(dataSource);
            }
        }

        // JDBC 4.1 support (JDK 1.7 and higher)
        public java.util.logging.Logger getParentLogger() {
            if (Util.JdbcVersion >= 0x0401) {
                // Do
                //              return dataSource.getParentLogger();
                // via reflection.
                try {
                    Method method =
                        DataSource.class.getMethod("getParentLogger");
                    return (java.util.logging.Logger) method.invoke(dataSource);
                } catch (IllegalAccessException e) {
                    throw Util.newInternal(e, "While invoking getParentLogger");
                } catch (InvocationTargetException e) {
                    throw Util.newInternal(e, "While invoking getParentLogger");
                } catch (NoSuchMethodException e) {
                    throw Util.newInternal(e, "While invoking getParentLogger");
                }
            } else {
                // Can't throw SQLFeatureNotSupportedException... it doesn't
                // exist before JDBC 4.1.
                throw new UnsupportedOperationException();
            }
        }
    }

    /**
     * Data source that gets connections from an underlying data source but
     * with different user name and password.
     */
    private static class UserPasswordDataSource extends DelegatingDataSource {
        private final String jdbcUser;
        private final String jdbcPassword;

        /**
         * Creates a UserPasswordDataSource
         *
         * @param dataSource Underlying data source
         * @param jdbcUser User name
         * @param jdbcPassword Password
         */
        public UserPasswordDataSource(
            DataSource dataSource,
            String jdbcUser,
            String jdbcPassword)
        {
            super(dataSource);
            this.jdbcUser = jdbcUser;
            this.jdbcPassword = jdbcPassword;
        }

        public Connection getConnection() throws SQLException {
            return dataSource.getConnection(jdbcUser, jdbcPassword);
        }
    }

    /**
     * <p>Implementation of {@link Statement} for use when you don't have an
     * olap4j connection.</p>
     */
    private class InternalStatement extends StatementImpl {
        private boolean closed = false;

        public void close() {
            if (!closed) {
                closed = true;
                server.removeStatement(this);
            }
        }

        public RolapConnection getMondrianConnection() {
            return RolapConnection.this;
        }
    }

    /**
     * <p>A statement that can be used for all of the various internal
     * operations, such as resolving MDX identifiers, that require a
     * {@link Statement} and an {@link Execution}.
     *
     * <p>The statement needs to be reentrant because there are many such
     * operations; several of these operations might be active at one time. We
     * don't want to create a new statement for each, but just one internal
     * statement for each connection. The statement shouldn't have a unique
     * execution. For this reason, we don't use the inherited {@link #execution}
     * field.</p>
     *
     * <p>But there is a drawback. If we can't find the unique execution, the
     * statement cannot be canceled or time out. If you want that behavior
     * from an internal statement, use the base class: create a new
     * {@link InternalStatement} for each operation.</p>
     */
    private class ReentrantInternalStatement extends InternalStatement {
        @Override
        public void start(Execution execution) {
            // Unlike StatementImpl, there is not a unique execution. An
            // internal statement can execute several at the same time. So,
            // we don't set this.execution.
            execution.start();
        }

        @Override
        public void end(Execution execution) {
            execution.end();
        }

        @Override
        public void close() {
            // do not close
        }
    }
}

// End RolapConnection.java
