001    /*
002    // $Id: Query.java 482 2012-01-05 23:27:27Z jhyde $
003    //
004    // Licensed to Julian Hyde under one or more contributor license
005    // agreements. See the NOTICE file distributed with this work for
006    // additional information regarding copyright ownership.
007    //
008    // Julian Hyde licenses this file to you under the Apache License,
009    // Version 2.0 (the "License"); you may not use this file except in
010    // compliance with the License. You may obtain a copy of the License at:
011    //
012    // http://www.apache.org/licenses/LICENSE-2.0
013    //
014    // Unless required by applicable law or agreed to in writing, software
015    // distributed under the License is distributed on an "AS IS" BASIS,
016    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017    // See the License for the specific language governing permissions and
018    // limitations under the License.
019    */
020    package org.olap4j.query;
021    
022    import org.olap4j.*;
023    import org.olap4j.mdx.SelectNode;
024    import org.olap4j.metadata.*;
025    
026    import java.sql.SQLException;
027    import java.util.*;
028    import java.util.Map.Entry;
029    
030    /**
031     * Base query model object.
032     *
033     * @author jhyde, jdixon, Luc Boudreau
034     * @version $Id: Query.java 482 2012-01-05 23:27:27Z jhyde $
035     * @since May 29, 2007
036     */
037    public class Query extends QueryNodeImpl {
038    
039        protected final String name;
040        protected Map<Axis, QueryAxis> axes = new HashMap<Axis, QueryAxis>();
041        protected QueryAxis across;
042        protected QueryAxis down;
043        protected QueryAxis filter;
044        protected QueryAxis unused;
045        protected final Cube cube;
046        protected Map<String, QueryDimension> dimensionMap =
047            new HashMap<String, QueryDimension>();
048        /**
049         * Whether or not to select the default hierarchy and default
050         * member on a dimension if no explicit selections were performed.
051         */
052        protected boolean selectDefaultMembers = true;
053        private final OlapConnection connection;
054        private final SelectionFactory selectionFactory = new SelectionFactory();
055    
056        /**
057         * Constructs a Query object.
058         * @param name Any arbitrary name to give to this query.
059         * @param cube A Cube object against which to build a query.
060         * @throws SQLException If an error occurs while accessing the
061         * cube's underlying connection.
062         */
063        public Query(String name, Cube cube) throws SQLException {
064            super();
065            this.name = name;
066            this.cube = cube;
067            final Catalog catalog = cube.getSchema().getCatalog();
068            this.connection =
069                catalog.getMetaData().getConnection().unwrap(OlapConnection.class);
070            this.connection.setCatalog(catalog.getName());
071            this.unused = new QueryAxis(this, null);
072            for (Dimension dimension : cube.getDimensions()) {
073                QueryDimension queryDimension = new QueryDimension(
074                    this, dimension);
075                unused.getDimensions().add(queryDimension);
076                dimensionMap.put(queryDimension.getName(), queryDimension);
077            }
078            across = new QueryAxis(this, Axis.COLUMNS);
079            down = new QueryAxis(this, Axis.ROWS);
080            filter = new QueryAxis(this, Axis.FILTER);
081            axes.put(null, unused);
082            axes.put(Axis.COLUMNS, across);
083            axes.put(Axis.ROWS, down);
084            axes.put(Axis.FILTER, filter);
085        }
086    
087        /**
088         * Returns the MDX parse tree behind this Query. The returned object is
089         * generated for each call to this function. Altering the returned
090         * SelectNode object won't affect the query itself.
091         * @return A SelectNode object representing the current query structure.
092         */
093        public SelectNode getSelect() {
094            return Olap4jNodeConverter.toOlap4j(this);
095        }
096    
097        /**
098         * Returns the underlying cube object that is used to query against.
099         * @return The Olap4j's Cube object.
100         */
101        public Cube getCube() {
102            return cube;
103        }
104    
105        /**
106         * Returns the Olap4j's Dimension object according to the name
107         * given as a parameter. If no dimension of the given name is found,
108         * a null value will be returned.
109         * @param name The name of the dimension you want the object for.
110         * @return The dimension object, null if no dimension of that
111         * name can be found.
112         */
113        public QueryDimension getDimension(String name) {
114            return dimensionMap.get(name);
115        }
116    
117        /**
118         * Swaps rows and columns axes. Only applicable if there are two axes.
119         */
120        public void swapAxes() {
121            // Only applicable if there are two axes - plus filter and unused.
122            if (axes.size() != 4) {
123                throw new IllegalArgumentException();
124            }
125            List<QueryDimension> tmpAcross = new ArrayList<QueryDimension>();
126            tmpAcross.addAll(across.getDimensions());
127    
128            List<QueryDimension> tmpDown = new ArrayList<QueryDimension>();
129            tmpDown.addAll(down.getDimensions());
130    
131            across.getDimensions().clear();
132            Map<Integer, QueryNode> acrossChildList =
133                new HashMap<Integer, QueryNode>();
134            for (int cpt = 0; cpt < tmpAcross.size();cpt++) {
135                acrossChildList.put(Integer.valueOf(cpt), tmpAcross.get(cpt));
136            }
137            across.notifyRemove(acrossChildList);
138    
139            down.getDimensions().clear();
140            Map<Integer, QueryNode> downChildList =
141                new HashMap<Integer, QueryNode>();
142            for (int cpt = 0; cpt < tmpDown.size();cpt++) {
143                downChildList.put(Integer.valueOf(cpt), tmpDown.get(cpt));
144            }
145            down.notifyRemove(downChildList);
146    
147            across.getDimensions().addAll(tmpDown);
148            across.notifyAdd(downChildList);
149    
150            down.getDimensions().addAll(tmpAcross);
151            down.notifyAdd(acrossChildList);
152        }
153    
154        /**
155         * Returns the query axis for a given axis type.
156         *
157         * <p>If you pass axis=null, returns a special axis that is used to hold
158         * all unused hierarchies. (We may change this behavior in future.)
159         *
160         * @param axis Axis type
161         * @return Query axis
162         */
163        public QueryAxis getAxis(Axis axis) {
164            return this.axes.get(axis);
165        }
166    
167        /**
168         * Returns a map of the current query's axis.
169         * <p>Be aware that modifications to this list might
170         * have unpredictable consequences.</p>
171         * @return A standard Map object that represents the
172         * current query's axis.
173         */
174        public Map<Axis, QueryAxis> getAxes() {
175            return axes;
176        }
177    
178        /**
179         * Returns the fictional axis into which all unused dimensions are stored.
180         * All dimensions included in this axis will not be part of the query.
181         * @return The QueryAxis representing dimensions that are currently not
182         * used inside the query.
183         */
184        public QueryAxis getUnusedAxis() {
185            return unused;
186        }
187    
188        /**
189         * Safely disposes of all underlying objects of this
190         * query.
191         * @param closeConnection Whether or not to call the
192         * {@link OlapConnection#close()} method of the underlying
193         * connection.
194         */
195        public void tearDown(boolean closeConnection) {
196            for (Entry<Axis, QueryAxis> entry : this.axes.entrySet()) {
197                entry.getValue().tearDown();
198            }
199            this.axes.clear();
200            this.clearListeners();
201            if (closeConnection) {
202                try {
203                    this.connection.close();
204                } catch (SQLException e) {
205                    e.printStackTrace();
206                }
207            }
208        }
209    
210        /**
211         * Safely disposes of all underlying objects of this
212         * query and closes the underlying {@link OlapConnection}.
213         * <p>Equivalent of calling Query.tearDown(true).
214         */
215        public void tearDown() {
216            this.tearDown(true);
217        }
218    
219        /**
220         * Validates the current query structure. If a dimension axis has
221         * been placed on an axis but no selections were performed on it,
222         * the default hierarchy and default member will be selected. This
223         * can be turned off by invoking the
224         * {@link Query#setSelectDefaultMembers(boolean)} method.
225         * @throws OlapException If the query is not valid, an exception
226         * will be thrown and it's message will describe exactly what to fix.
227         */
228        public void validate() throws OlapException {
229            try {
230                // First, perform default selections if needed.
231                if (this.selectDefaultMembers) {
232                    // Perform default selection on the dimensions on the rows axis.
233                    for (QueryDimension dimension : this.getAxis(Axis.ROWS)
234                        .getDimensions())
235                    {
236                        if (dimension.getInclusions().size() == 0) {
237                            Member defaultMember = dimension.getDimension()
238                                .getDefaultHierarchy().getDefaultMember();
239                            dimension.include(defaultMember);
240                        }
241                    }
242                    // Perform default selection on the
243                    // dimensions on the columns axis.
244                    for (QueryDimension dimension : this.getAxis(Axis.COLUMNS)
245                        .getDimensions())
246                    {
247                        if (dimension.getInclusions().size() == 0) {
248                            Member defaultMember = dimension.getDimension()
249                                .getDefaultHierarchy().getDefaultMember();
250                            dimension.include(defaultMember);
251                        }
252                    }
253                    // Perform default selection on the dimensions
254                    // on the filter axis.
255                    for (QueryDimension dimension : this.getAxis(Axis.FILTER)
256                        .getDimensions())
257                    {
258                        if (dimension.getInclusions().size() == 0) {
259                            Member defaultMember = dimension.getDimension()
260                                .getDefaultHierarchy().getDefaultMember();
261                            dimension.include(defaultMember);
262                        }
263                    }
264                }
265    
266                // We at least need a dimension on the rows and on the columns axis.
267                if (this.getAxis(Axis.ROWS).getDimensions().size() == 0) {
268                    throw new OlapException(
269                        "A valid Query requires at least one dimension on the rows axis.");
270                }
271                if (this.getAxis(Axis.COLUMNS).getDimensions().size() == 0) {
272                    throw new OlapException(
273                        "A valid Query requires at least one dimension on the columns axis.");
274                }
275    
276                // Try to build a select tree.
277                this.getSelect();
278            } catch (Exception e) {
279                throw new OlapException("Query validation failed.", e);
280            }
281        }
282    
283        /**
284         * Executes the query against the current OlapConnection and returns
285         * a CellSet object representation of the data.
286         *
287         * @return A proper CellSet object that represents the query execution
288         *     results.
289         * @throws OlapException If something goes sour, an OlapException will
290         *     be thrown to the caller. It could be caused by many things, like
291         *     a stale connection. Look at the root cause for more details.
292         */
293        public CellSet execute() throws OlapException {
294            SelectNode mdx = getSelect();
295            final Catalog catalog = cube.getSchema().getCatalog();
296            try {
297                this.connection.setCatalog(catalog.getName());
298            } catch (SQLException e) {
299                throw new OlapException("Error while executing query", e);
300            }
301            OlapStatement olapStatement = connection.createStatement();
302            return olapStatement.executeOlapQuery(mdx);
303        }
304    
305        /**
306         * Returns this query's name. There is no guarantee that it is unique
307         * and is set at object instanciation.
308         * @return This query's name.
309         */
310        public String getName() {
311            return name;
312        }
313    
314        /**
315         * Returns the current locale with which this query is expressed.
316         * @return A standard Locale object.
317         */
318        public Locale getLocale() {
319            // REVIEW Do queries really support locales?
320            return Locale.getDefault();
321        }
322    
323        /**
324         * Package restricted method to access this query's selection factory.
325         * Usually used by query dimensions who wants to perform selections.
326         * @return The underlying SelectionFactory implementation.
327         */
328        SelectionFactory getSelectionFactory() {
329            return selectionFactory;
330        }
331    
332        /**
333         * Behavior setter for a query. By default, if a dimension is placed on
334         * an axis but no selections are made, the default hierarchy and
335         * the default member will be selected when validating the query.
336         * This behavior can be turned off by this setter.
337         * @param selectDefaultMembers Enables or disables the default
338         * member and hierarchy selection upon validation.
339         */
340        public void setSelectDefaultMembers(boolean selectDefaultMembers) {
341            this.selectDefaultMembers = selectDefaultMembers;
342        }
343    }
344    
345    // End Query.java