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