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