001    /*
002    // $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/RolapConnection.java#4 $
003    // This software is subject to the terms of the Common Public License
004    // Agreement, available at the following URL:
005    // http://www.opensource.org/licenses/cpl.html.
006    // Copyright (C) 2001-2002 Kana Software, Inc.
007    // Copyright (C) 2001-2007 Julian Hyde and others
008    // All Rights Reserved.
009    // You must accept the terms of that agreement to use this software.
010    //
011    // jhyde, 2 October, 2002
012    */
013    package mondrian.rolap;
014    
015    import java.io.PrintWriter;
016    import java.sql.Connection;
017    import java.sql.SQLException;
018    import java.sql.Statement;
019    import java.sql.ResultSet;
020    import java.util.*;
021    import java.lang.reflect.Method;
022    import java.lang.reflect.InvocationTargetException;
023    
024    import javax.naming.InitialContext;
025    import javax.naming.NamingException;
026    import javax.sql.DataSource;
027    
028    import mondrian.olap.CacheControl;
029    import mondrian.olap.Cell;
030    import mondrian.olap.ConnectionBase;
031    import mondrian.olap.Cube;
032    import mondrian.olap.DriverManager;
033    import mondrian.olap.MondrianProperties;
034    import mondrian.olap.Position;
035    import mondrian.olap.Query;
036    import mondrian.olap.QueryAxis;
037    import mondrian.olap.QueryCanceledException;
038    import mondrian.olap.QueryTimeoutException;
039    import mondrian.olap.ResourceLimitExceededException;
040    import mondrian.olap.Result;
041    import mondrian.olap.ResultBase;
042    import mondrian.olap.ResultLimitExceededException;
043    import mondrian.olap.Role;
044    import mondrian.olap.RoleImpl;
045    import mondrian.olap.Schema;
046    import mondrian.olap.SchemaReader;
047    import mondrian.olap.Util;
048    import mondrian.rolap.agg.AggregationManager;
049    import mondrian.rolap.sql.SqlQuery;
050    import mondrian.util.MemoryMonitor;
051    import mondrian.util.MemoryMonitorFactory;
052    import mondrian.util.Pair;
053    import mondrian.rolap.agg.AggregationManager;
054    import org.apache.commons.dbcp.ConnectionFactory;
055    import org.apache.commons.dbcp.DataSourceConnectionFactory;
056    import org.apache.commons.dbcp.DriverManagerConnectionFactory;
057    
058    import org.apache.log4j.Logger;
059    import javax.naming.InitialContext;
060    import javax.naming.NamingException;
061    import javax.sql.DataSource;
062    import java.io.PrintWriter;
063    import java.io.StringWriter;
064    import java.sql.Connection;
065    import java.sql.SQLException;
066    import java.util.*;
067    
068    /**
069     * A <code>RolapConnection</code> is a connection to a Mondrian OLAP Server.
070     *
071     * <p>Typically, you create a connection via
072     * {@link DriverManager#getConnection(String, mondrian.spi.CatalogLocator)}.
073     * {@link RolapConnectionProperties} describes allowable keywords.</p>
074     *
075     * @see RolapSchema
076     * @see DriverManager
077     * @author jhyde
078     * @since 2 October, 2002
079     * @version $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/RolapConnection.java#4 $
080     */
081    public class RolapConnection extends ConnectionBase {
082        private static final Logger LOGGER = Logger.getLogger(RolapConnection.class);
083        
084        private final Util.PropertyList connectInfo;
085    
086        // used for MDX logging, allows for a MDX Statement UID 
087        private static long executeCount = 0;
088        
089        /**
090         * Factory for JDBC connections to talk to the RDBMS. This factory will
091         * usually use a connection pool.
092         */
093        private final DataSource dataSource;
094        private final String catalogUrl;
095        private final RolapSchema schema;
096        private SchemaReader schemaReader;
097        protected Role role;
098        private Locale locale = Locale.US;
099    
100        /**
101         * Creates a connection.
102         *
103         * @param connectInfo Connection properties; keywords are described in
104         *   {@link RolapConnectionProperties}.
105         */
106        public RolapConnection(Util.PropertyList connectInfo) {
107            this(connectInfo, null, null);
108        }
109    
110        /**
111         * Creates a connection.
112         *
113         * @param connectInfo Connection properties; keywords are described in
114         *   {@link RolapConnectionProperties}.
115         */
116        public RolapConnection(Util.PropertyList connectInfo, DataSource dataSource) {
117            this(connectInfo, null, dataSource);
118        }
119    
120        /**
121         * Creates a RolapConnection.
122         *
123         * <p>Only {@link mondrian.rolap.RolapSchema.Pool#get} calls this with schema != null (to
124         * create a schema's internal connection). Other uses retrieve a schema
125         * from the cache based upon the <code>Catalog</code> property.
126         *
127         * @param connectInfo Connection properties; keywords are described in
128         *   {@link RolapConnectionProperties}.
129         * @param schema Schema for the connection. Must be null unless this is to
130         *   be an internal connection.
131         * @pre connectInfo != null
132         */
133        RolapConnection(Util.PropertyList connectInfo, RolapSchema schema) {
134            this(connectInfo, schema, null);
135        }
136    
137        /**
138         * Creates a RolapConnection.
139         *
140         * <p>Only {@link mondrian.rolap.RolapSchema.Pool#get} calls this with
141         * schema != null (to create a schema's internal connection).
142         * Other uses retrieve a schema from the cache based upon
143         * the <code>Catalog</code> property.
144         *
145         * @param connectInfo Connection properties; keywords are described in
146         *   {@link RolapConnectionProperties}.
147         * @param schema Schema for the connection. Must be null unless this is to
148         *   be an internal connection.
149         * @param dataSource If not null an external DataSource to be used
150         *        by Mondrian
151         * @pre connectInfo != null
152         */
153        RolapConnection(
154            Util.PropertyList connectInfo,
155            RolapSchema schema,
156            DataSource dataSource)
157        {
158            super();
159    
160            String provider = connectInfo.get(
161                RolapConnectionProperties.Provider.name(), "mondrian");
162            Util.assertTrue(provider.equalsIgnoreCase("mondrian"));
163            this.connectInfo = connectInfo;
164            this.catalogUrl =
165                connectInfo.get(RolapConnectionProperties.Catalog.name());
166            final String jdbcUser =
167                connectInfo.get(RolapConnectionProperties.JdbcUser.name());
168            final String jdbcConnectString =
169                connectInfo.get(RolapConnectionProperties.Jdbc.name());
170            final String strDataSource =
171                connectInfo.get(RolapConnectionProperties.DataSource.name());
172            StringBuilder buf = new StringBuilder();
173            this.dataSource =
174                createDataSource(dataSource, connectInfo, buf);
175            Role role = null;
176            if (schema == null) {
177                // If RolapSchema.Pool.get were to call this with schema == null,
178                // we would loop.
179                if (dataSource == null) {
180                    // If there is no external data source is passed in,
181                    // we expect the properties Jdbc, JdbcUser, DataSource to be set,
182                    // as they are used to generate the schema cache key.
183                    final String connectionKey = jdbcConnectString +
184                        getJdbcProperties(connectInfo).toString();
185    
186                    schema = RolapSchema.Pool.instance().get(
187                        catalogUrl,
188                        connectionKey,
189                        jdbcUser,
190                        strDataSource,
191                        connectInfo);
192                } else {
193                    schema = RolapSchema.Pool.instance().get(
194                        catalogUrl,
195                        dataSource,
196                        connectInfo);
197                }
198                String roleNameList =
199                    connectInfo.get(RolapConnectionProperties.Role.name());
200                if (roleNameList != null) {
201                    List<String> roleNames = Util.parseCommaList(roleNameList);
202                    List<Role> roleList = new ArrayList<Role>();
203                    for (String roleName : roleNames) {
204                        Role role1 = schema.lookupRole(roleName);
205                        if (role1 == null) {
206                            throw Util.newError("Role '" + roleName + "' not found");
207                        }
208                        roleList.add(role1);
209                    }
210                    switch (roleList.size()) {
211                    case 0:
212                        // If they specify 'Role=;', the list of names will be
213                        // empty, and the effect will be as if they did specify
214                        // Role at all.
215                        role = null;
216                        break;
217                    case 1:
218                        role = roleList.get(0);
219                        break;
220                    default:
221                        role = RoleImpl.union(roleList);
222                        break;
223                    }
224                }
225            } else {
226                // We are creating an internal connection. Now is a great time to
227                // make sure that the JDBC credentials are valid, for this
228                // connection and for external connections built on top of this.
229                Connection conn = null;
230                Statement statement = null;
231                try {
232                    conn = this.dataSource.getConnection();
233                    SqlQuery.Dialect dialect =
234                        SqlQuery.Dialect.create(conn.getMetaData());
235                    if (dialect.isDerby()) {
236                        // Derby requires a little extra prodding to do the
237                        // validation to detect an error.
238                        statement = conn.createStatement();
239                        statement.executeQuery("select * from bogustable");
240                    }
241                } catch (SQLException e) {
242                    if (e.getMessage().equals("Table/View 'BOGUSTABLE' does not exist.")) {
243                        // Ignore. This exception comes from Derby when the
244                        // connection is valid. If the connection were invalid, we
245                        // would receive an error such as "Schema 'BOGUSUSER' does
246                        // not exist"
247                    } else {
248                        final String message =
249                            "Error while creating SQL connection: " + buf.toString();
250                        throw Util.newError(e, message);
251                    }
252                } finally {
253                    try {
254                        if (statement != null) {
255                            statement.close();
256                        }
257                        if (conn != null) {
258                            conn.close();
259                        }
260                    } catch (SQLException e) {
261                        // ignore
262                    }
263                }
264            }
265    
266            if (role == null) {
267                role = schema.getDefaultRole();
268            }
269    
270            // Set the locale.
271            String localeString =
272                connectInfo.get(RolapConnectionProperties.Locale.name());
273            if (localeString != null) {
274                String[] strings = localeString.split("_");
275                switch (strings.length) {
276                case 1:
277                    this.locale = new Locale(strings[0]);
278                    break;
279                case 2:
280                    this.locale = new Locale(strings[0], strings[1]);
281                    break;
282                case 3:
283                    this.locale = new Locale(strings[0], strings[1], strings[2]);
284                    break;
285                default:
286                    throw Util.newInternal("bad locale string '" + localeString + "'");
287                }
288            }
289    
290            this.schema = schema;
291            setRole(role);
292        }
293    
294        protected Logger getLogger() {
295            return LOGGER;
296        }
297    
298        /**
299         * Creates a JDBC data source from the JDBC credentials contained within a
300         * set of mondrian connection properties.
301         *
302         * <p>This method is package-level so that it can be called from the
303         * RolapConnectionTest unit test.
304         *
305         * @param dataSource Anonymous data source from user, or null
306         * @param connectInfo Mondrian connection properties
307         * @param buf Into which method writes a description of the JDBC credentials
308         * @return Data source
309         */
310        static DataSource createDataSource(
311            DataSource dataSource,
312            Util.PropertyList connectInfo,
313            StringBuilder buf)
314        {
315            assert buf != null;
316            final String jdbcConnectString =
317                connectInfo.get(RolapConnectionProperties.Jdbc.name());
318            final String jdbcUser =
319                connectInfo.get(RolapConnectionProperties.JdbcUser.name());
320            final String jdbcPassword =
321                connectInfo.get(RolapConnectionProperties.JdbcPassword.name());
322            final String dataSourceName =
323                connectInfo.get(RolapConnectionProperties.DataSource.name());
324    
325            if (dataSource != null) {
326                appendKeyValue(buf, "Anonymous data source", dataSource);
327                appendKeyValue(
328                    buf, RolapConnectionProperties.JdbcUser.name(), jdbcUser);
329                appendKeyValue(
330                    buf, RolapConnectionProperties.JdbcPassword.name(), jdbcPassword);
331                if (jdbcUser != null || jdbcPassword != null) {
332                    dataSource =
333                        new UserPasswordDataSource(
334                            dataSource, jdbcUser, jdbcPassword);
335                }
336                return dataSource;
337    
338            } else if (jdbcConnectString != null) {
339                // Get connection through own pooling datasource
340                appendKeyValue(
341                    buf, RolapConnectionProperties.Jdbc.name(), jdbcConnectString);
342                appendKeyValue(
343                    buf, RolapConnectionProperties.JdbcUser.name(), jdbcUser);
344                appendKeyValue(
345                    buf, RolapConnectionProperties.JdbcPassword.name(), jdbcPassword);
346                String jdbcDrivers =
347                    connectInfo.get(RolapConnectionProperties.JdbcDrivers.name());
348                if (jdbcDrivers != null) {
349                    RolapUtil.loadDrivers(jdbcDrivers);
350                }
351                final String jdbcDriversProp =
352                        MondrianProperties.instance().JdbcDrivers.get();
353                RolapUtil.loadDrivers(jdbcDriversProp);
354    
355                Properties jdbcProperties = getJdbcProperties(connectInfo);
356                for (Map.Entry<Object, Object> entry : jdbcProperties.entrySet()) {
357                    // FIXME ordering is non-deterministic
358                    appendKeyValue(buf, (String) entry.getKey(), entry.getValue());
359                }
360                String propertyString = jdbcProperties.toString();
361                if (jdbcUser != null) {
362                    jdbcProperties.put("user", jdbcUser);
363                }
364                if (jdbcPassword != null) {
365                    jdbcProperties.put("password", jdbcPassword);
366                }
367    
368                // JDBC connections are dumb beasts, so we assume they're not
369                // pooled. Therefore the default is true.
370                final boolean poolNeeded =
371                    connectInfo.get(
372                        RolapConnectionProperties.PoolNeeded.name(),
373                        "true").equalsIgnoreCase("true");
374    
375                if (!poolNeeded) {
376                    // Connection is already pooled; don't pool it again.
377                    return new DriverManagerDataSource(
378                        jdbcConnectString,
379                        jdbcProperties);
380                }
381    
382                if (jdbcConnectString.toLowerCase().indexOf("mysql") > -1) {
383                    // mysql driver needs this autoReconnect parameter
384                    jdbcProperties.setProperty("autoReconnect", "true");
385                }
386                // use the DriverManagerConnectionFactory to create connections
387                ConnectionFactory connectionFactory =
388                    new DriverManagerConnectionFactory(
389                        jdbcConnectString,
390                        jdbcProperties);
391                try {
392                    return RolapConnectionPool.instance().getPoolingDataSource(
393                        jdbcConnectString + propertyString,
394                        connectionFactory);
395                } catch (Throwable e) {
396                    throw Util.newInternal(
397                        e,
398                        "Error while creating connection pool (with URI " +
399                            jdbcConnectString + ")");
400                }
401    
402            } else if (dataSourceName != null) {
403                appendKeyValue(
404                    buf, RolapConnectionProperties.DataSource.name(), dataSourceName);
405                appendKeyValue(
406                    buf, RolapConnectionProperties.JdbcUser.name(), jdbcUser);
407                appendKeyValue(
408                    buf, RolapConnectionProperties.JdbcPassword.name(), jdbcPassword);
409    
410                // Data sources are fairly smart, so we assume they look after
411                // their own pooling. Therefore the default is false.
412                final boolean poolNeeded =
413                    connectInfo.get(
414                        RolapConnectionProperties.PoolNeeded.name(),
415                        "false").equalsIgnoreCase("true");
416    
417                // Get connection from datasource.
418                try {
419                    dataSource =
420                        (DataSource) new InitialContext().lookup(dataSourceName);
421                } catch (NamingException e) {
422                    throw Util.newInternal(
423                        e,
424                        "Error while looking up data source (" +
425                            dataSourceName + ")");
426                }
427                if (poolNeeded) {
428                    ConnectionFactory connectionFactory;
429                    if (jdbcUser != null || jdbcPassword != null) {
430                        connectionFactory =
431                            new DataSourceConnectionFactory(
432                                dataSource, jdbcUser, jdbcPassword);
433                    } else {
434                        connectionFactory =
435                            new DataSourceConnectionFactory(dataSource);
436                    }
437                    try {
438                        dataSource =
439                            RolapConnectionPool.instance().getPoolingDataSource(
440                                dataSourceName,
441                                connectionFactory);
442                    } catch (Exception e) {
443                        throw Util.newInternal(
444                            e,
445                            "Error while creating connection pool (with URI " +
446                                dataSourceName + ")");
447                    }
448                } else {
449                    if (jdbcUser != null || jdbcPassword != null) {
450                        dataSource =
451                            new UserPasswordDataSource(
452                                dataSource, jdbcUser, jdbcPassword);
453                    }
454                }
455                return dataSource;
456            } else {
457                throw Util.newInternal(
458                    "Connect string '" + connectInfo.toString() +
459                        "' must contain either '" + RolapConnectionProperties.Jdbc +
460                        "' or '" + RolapConnectionProperties.DataSource + "'");
461            }
462        }
463    
464        /**
465         * Appends "key=value" to a buffer, if value is not null.
466         *
467         * @param buf Buffer
468         * @param key Key
469         * @param value Value
470         */
471        private static void appendKeyValue(
472            StringBuilder buf,
473            String key,
474            Object value)
475        {
476            if (value != null) {
477                if (buf.length() > 0) {
478                    buf.append("; ");
479                }
480                buf.append(key).append('=').append(value);
481            }
482        }
483    
484        /**
485         * Creates a {@link Properties} object containing all of the JDBC
486         * connection properties present in the
487         * {@link mondrian.olap.Util.PropertyList connectInfo}.
488         *
489         * @param connectInfo Connection properties
490         * @return The JDBC connection properties.
491         */
492        private static Properties getJdbcProperties(Util.PropertyList connectInfo) {
493            Properties jdbcProperties = new Properties();
494            for (Pair<String,String> entry : connectInfo) {
495                if (entry.left.startsWith(
496                    RolapConnectionProperties.JdbcPropertyPrefix))
497                {
498                    jdbcProperties.put(
499                        entry.left.substring(
500                            RolapConnectionProperties.JdbcPropertyPrefix.length()),
501                        entry.right);
502                }
503            }
504            return jdbcProperties;
505        }
506    
507        public Util.PropertyList getConnectInfo() {
508            return connectInfo;
509        }
510    
511        public void close() {
512        }
513    
514        public Schema getSchema() {
515            return schema;
516        }
517    
518        public String getConnectString() {
519            return connectInfo.toString();
520        }
521    
522        public String getCatalogName() {
523            return catalogUrl;
524        }
525    
526        public Locale getLocale() {
527            return locale;
528        }
529    
530        public void setLocale(Locale locale) {
531            this.locale = locale;
532        }
533    
534        public SchemaReader getSchemaReader() {
535            return schemaReader;
536        }
537    
538        public Object getProperty(String name) {
539            // Mask out the values of certain properties.
540            if (name.equals(RolapConnectionProperties.JdbcPassword.name()) ||
541                name.equals(RolapConnectionProperties.CatalogContent.name())) {
542                return "";
543            }
544            return connectInfo.get(name);
545        }
546    
547        public CacheControl getCacheControl(PrintWriter pw) {
548            return AggregationManager.instance().getCacheControl(pw);
549        }
550    
551        /**
552         * Executes a Query.
553         *
554         * @throws ResourceLimitExceededException if some resource limit specified in the
555         * property file was exceeded
556         * @throws QueryCanceledException if query was canceled during execution
557         * @throws QueryTimeoutException if query exceeded timeout specified in
558         * the property file
559         */
560        public Result execute(Query query) {
561            class Listener implements MemoryMonitor.Listener {
562                private final Query query;
563                Listener(final Query query) {
564                    this.query = query;
565                }
566                public void memoryUsageNotification(long used, long max) {
567                    StringBuilder buf = new StringBuilder(200);
568                    buf.append("OutOfMemory used=");
569                    buf.append(used);
570                    buf.append(", max=");
571                    buf.append(max);
572                    buf.append(" for connection: ");
573                    buf.append(getConnectString());
574                    // Call ConnectionBase method which has access to
575                    // Query methods.
576                    RolapConnection.memoryUsageNotification(query, buf.toString());
577                }
578            }
579            Listener listener = new Listener(query);
580            MemoryMonitor mm = MemoryMonitorFactory.getMemoryMonitor();
581            long currId = -1;
582            try {
583                mm.addListener(listener);
584                // Check to see if we must punt
585                query.checkCancelOrTimeout();
586    
587                if (LOGGER.isDebugEnabled()) {
588                    LOGGER.debug(Util.unparse(query));
589                }
590    
591                if (RolapUtil.MDX_LOGGER.isDebugEnabled()) {
592                    currId = executeCount++;
593                    RolapUtil.MDX_LOGGER.debug( currId + ": " + Util.unparse(query));
594                }
595                
596                query.setQueryStartTime();
597                Result result = new RolapResult(query, true);
598                for (int i = 0; i < query.axes.length; i++) {
599                    QueryAxis axis = query.axes[i];
600                    if (axis.isNonEmpty()) {
601                        result = new NonEmptyResult(result, query, i);
602                    }
603                }
604                if (LOGGER.isDebugEnabled()) {
605                    StringWriter sw = new StringWriter();
606                    PrintWriter pw = new PrintWriter(sw);
607                    result.print(pw);
608                    pw.flush();
609                    LOGGER.debug(sw.toString());
610                }
611                query.setQueryEndExecution();
612                return result;
613    
614            } catch (ResultLimitExceededException e) {
615                // query has been punted
616                throw e;
617            } catch (Exception e) {
618                String queryString;
619                query.setQueryEndExecution();
620                try {
621                    queryString = Util.unparse(query);
622                } catch (Exception e1) {
623                    queryString = "?";
624                }
625                throw Util.newError(e, "Error while executing query [" +
626                        queryString + "]");
627            } finally {
628                mm.removeListener(listener);
629                if (RolapUtil.MDX_LOGGER.isDebugEnabled()) {
630                    RolapUtil.MDX_LOGGER.debug( currId + ": exec: " + 
631                        (System.currentTimeMillis() - query.getQueryStartTime()) +
632                        " ms");
633                }
634            }
635        }
636    
637        public void setRole(Role role) {
638            assert role != null;
639    
640            this.role = role;
641            this.schemaReader = new RolapSchemaReader(role, schema) {
642                public Cube getCube() {
643                    throw new UnsupportedOperationException();
644                }
645            };
646        }
647    
648        public Role getRole() {
649            Util.assertPostcondition(role != null, "role != null");
650    
651            return role;
652        }
653    
654        /**
655         * Implementation of {@link DataSource} which calls the good ol'
656         * {@link java.sql.DriverManager}.
657         */
658        private static class DriverManagerDataSource implements DataSource {
659            private final String jdbcConnectString;
660            private PrintWriter logWriter;
661            private int loginTimeout;
662            private Properties jdbcProperties;
663    
664            public DriverManagerDataSource(
665                String jdbcConnectString,
666                Properties properties)
667            {
668                this.jdbcConnectString = jdbcConnectString;
669                this.jdbcProperties = properties;
670            }
671    
672            public Connection getConnection() throws SQLException {
673                return new org.apache.commons.dbcp.DelegatingConnection(
674                    java.sql.DriverManager.getConnection(
675                        jdbcConnectString, jdbcProperties));
676            }
677    
678            public Connection getConnection(String username, String password)
679                    throws SQLException {
680                if (jdbcProperties == null) {
681                    return java.sql.DriverManager.getConnection(jdbcConnectString,
682                            username, password);
683                } else {
684                    Properties temp = (Properties)jdbcProperties.clone();
685                    temp.put("user", username);
686                    temp.put("password", password);
687                    return java.sql.DriverManager.getConnection(jdbcConnectString, temp);
688                }
689            }
690    
691            public PrintWriter getLogWriter() throws SQLException {
692                return logWriter;
693            }
694    
695            public void setLogWriter(PrintWriter out) throws SQLException {
696                logWriter = out;
697            }
698    
699            public void setLoginTimeout(int seconds) throws SQLException {
700                loginTimeout = seconds;
701            }
702    
703            public int getLoginTimeout() throws SQLException {
704                return loginTimeout;
705            }
706    
707            public <T> T unwrap(Class<T> iface) throws SQLException {
708                throw new SQLException("not a wrapper");
709            }
710    
711            public boolean isWrapperFor(Class<?> iface) throws SQLException {
712                return false;
713            }
714        }
715    
716        public DataSource getDataSource() {
717            return dataSource;
718        }
719    
720        /**
721         * A <code>NonEmptyResult</code> filters a result by removing empty rows
722         * on a particular axis.
723         */
724        static class NonEmptyResult extends ResultBase {
725    
726            final Result underlying;
727            private final int axis;
728            private final Map<Integer, Integer> map;
729            /** workspace. Synchronized access only. */
730            private final int[] pos;
731    
732            NonEmptyResult(Result result, Query query, int axis) {
733                super(query, result.getAxes().clone());
734    
735                this.underlying = result;
736                this.axis = axis;
737                this.map = new HashMap<Integer, Integer>();
738                int axisCount = underlying.getAxes().length;
739                this.pos = new int[axisCount];
740                this.slicerAxis = underlying.getSlicerAxis();
741                List<Position> positions = underlying.getAxes()[axis].getPositions();
742    
743                List<Position> positionsList = new ArrayList<Position>();
744                int i = 0;
745                for (Position position: positions) {
746                    if (! isEmpty(i, axis)) {
747                        map.put(positionsList.size(), i);
748                        positionsList.add(position);
749                    }
750                    i++;
751                }
752                this.axes[axis] = new RolapAxis.PositionList(positionsList);
753            }
754    
755            protected Logger getLogger() {
756                return LOGGER;
757            }
758    
759            /**
760             * Returns true if all cells at a given offset on a given axis are
761             * empty. For example, in a 2x2x2 dataset, <code>isEmpty(1,0)</code>
762             * returns true if cells <code>{(1,0,0), (1,0,1), (1,1,0),
763             * (1,1,1)}</code> are all empty. As you can see, we hold the 0th
764             * coordinate fixed at 1, and vary all other coordinates over all
765             * possible values.
766             */
767            private boolean isEmpty(int offset, int fixedAxis) {
768                int axisCount = getAxes().length;
769                pos[fixedAxis] = offset;
770                return isEmptyRecurse(fixedAxis, axisCount - 1);
771            }
772    
773            private boolean isEmptyRecurse(int fixedAxis, int axis) {
774                if (axis < 0) {
775                    RolapCell cell = (RolapCell) underlying.getCell(pos);
776                    return cell.isNull();
777                } else if (axis == fixedAxis) {
778                    return isEmptyRecurse(fixedAxis, axis - 1);
779                } else {
780                    List<Position> positions = getAxes()[axis].getPositions();
781                    int i = 0;
782                    for (Position position: positions) {
783                        pos[axis] = i;
784                        if (!isEmptyRecurse(fixedAxis, axis - 1)) {
785                            return false;
786                        }
787                        i++;
788                    }
789                    return true;
790                }
791            }
792    
793            // synchronized because we use 'pos'
794            public synchronized Cell getCell(int[] externalPos) {
795                System.arraycopy(externalPos, 0, this.pos, 0, externalPos.length);
796                int offset = externalPos[axis];
797                int mappedOffset = mapOffsetToUnderlying(offset);
798                this.pos[axis] = mappedOffset;
799                return underlying.getCell(this.pos);
800            }
801    
802            private int mapOffsetToUnderlying(int offset) {
803                return map.get(offset);
804            }
805    
806            public void close() {
807                underlying.close();
808            }
809        }
810    
811        /**
812         * Data source that delegates all methods to an underlying data source.
813         */
814        private static abstract class DelegatingDataSource implements DataSource {
815            protected final DataSource dataSource;
816    
817            public DelegatingDataSource(DataSource dataSource) {
818                this.dataSource = dataSource;
819            }
820    
821            public Connection getConnection() throws SQLException {
822                return dataSource.getConnection();
823            }
824    
825            public Connection getConnection(String username, String password) throws SQLException {
826                return dataSource.getConnection(username, password);
827            }
828    
829            public PrintWriter getLogWriter() throws SQLException {
830                return dataSource.getLogWriter();
831            }
832    
833            public void setLogWriter(PrintWriter out) throws SQLException {
834                dataSource.setLogWriter(out);
835            }
836    
837            public void setLoginTimeout(int seconds) throws SQLException {
838                dataSource.setLoginTimeout(seconds);
839            }
840    
841            public int getLoginTimeout() throws SQLException {
842                return dataSource.getLoginTimeout();
843            }
844    
845            public <T> T unwrap(Class<T> iface) throws SQLException {
846                if (Util.JdbcVersion >= 4) {
847                    // Do
848                    //              return dataSource.unwrap(iface);
849                    // via reflection.
850                    try {
851                        Method method =
852                            DataSource.class.getMethod("unwrap", Class.class);
853                        return iface.cast(method.invoke(dataSource, iface));
854                    } catch (IllegalAccessException e) {
855                        throw Util.newInternal(e, "While invokin unwrap");
856                    } catch (InvocationTargetException e) {
857                        throw Util.newInternal(e, "While invokin unwrap");
858                    } catch (NoSuchMethodException e) {
859                        throw Util.newInternal(e, "While invokin unwrap");
860                    }
861                } else {
862                    if (iface.isInstance(dataSource)) {
863                        return iface.cast(dataSource);
864                    } else {
865                        return null;
866                    }
867                }
868            }
869    
870            public boolean isWrapperFor(Class<?> iface) throws SQLException {
871                if (Util.JdbcVersion >= 4) {
872                    // Do
873                    //              return dataSource.isWrapperFor(iface);
874                    // via reflection.
875                    try {
876                        Method method =
877                            DataSource.class.getMethod("isWrapperFor", boolean.class);
878                        return (Boolean) method.invoke(dataSource, iface);
879                    } catch (IllegalAccessException e) {
880                        throw Util.newInternal(e, "While invoking isWrapperFor");
881                    } catch (InvocationTargetException e) {
882                        throw Util.newInternal(e, "While invoking isWrapperFor");
883                    } catch (NoSuchMethodException e) {
884                        throw Util.newInternal(e, "While invoking isWrapperFor");
885                    }
886                } else {
887                    return iface.isInstance(dataSource);
888                }
889            }
890        }
891    
892        /**
893         * Data source that gets connections from an underlying data source but
894         * with different user name and password.
895         */
896        private static class UserPasswordDataSource extends DelegatingDataSource {
897            private final String jdbcUser;
898            private final String jdbcPassword;
899    
900            /**
901             * Creates a UserPasswordDataSource
902             *
903             * @param dataSource Underlying data source
904             * @param jdbcUser User name
905             * @param jdbcPassword Password
906             */
907            public UserPasswordDataSource(
908                DataSource dataSource,
909                String jdbcUser,
910                String jdbcPassword)
911            {
912                super(dataSource);
913                this.jdbcUser = jdbcUser;
914                this.jdbcPassword = jdbcPassword;
915            }
916    
917            public Connection getConnection() throws SQLException {
918                return dataSource.getConnection(jdbcUser, jdbcPassword);
919            }
920        }
921    }
922    
923    // End RolapConnection.java