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*/
020package org.olap4j.query;
021
022import org.olap4j.*;
023import org.olap4j.mdx.SelectNode;
024import org.olap4j.metadata.*;
025
026import java.sql.SQLException;
027import java.util.*;
028import 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 */
037public 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