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