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