001: package jimm.datavision.source;
002:
003: import jimm.datavision.*;
004: import jimm.datavision.field.*;
005: import jimm.util.XMLWriter;
006: import java.util.*;
007:
008: /**
009: * A data source query.
010: *
011: * @author Jim Menard, <a href="mailto:jimm@io.com">jimm@io.com</a>
012: */
013: public class Query implements Writeable {
014:
015: public static final int SORT_UNDEFINED = -1;
016: public static final int SORT_DESCENDING = 0;
017: public static final int SORT_ASCENDING = 1;
018:
019: protected Report report;
020: protected ArrayList joins;
021: protected String whereClause;
022: protected ArrayList sortSelectables;
023: protected ArrayList sortOrders;
024: protected ArrayList selectables; // Can't be a Set; we need selectable indices
025:
026: /**
027: * Constructor.
028: *
029: * @param r the report for which this query will retrieve data
030: */
031: public Query(Report r) {
032: report = r;
033: joins = new ArrayList();
034: whereClause = null;
035: sortSelectables = new ArrayList();
036: sortOrders = new ArrayList();
037: selectables = new ArrayList();
038: }
039:
040: /**
041: * Returns <code>true</code> if the specified parameter exists within this
042: * query's where clause.
043: *
044: * @param p a parameter
045: * @return <code>true</code> if the specified parameter exists within
046: * the where clause
047: */
048: public boolean containsReferenceTo(Parameter p) {
049: if (whereClause == null || whereClause.indexOf("{") == -1)
050: return false;
051:
052: int pos, endPos;
053: for (pos = 0, endPos = -1; (pos = whereClause.indexOf("{",
054: endPos + 1)) >= 0; pos = endPos + 1) {
055: endPos = whereClause.indexOf("}", pos);
056: if (endPos == -1)
057: return false;
058:
059: switch (whereClause.charAt(pos + 1)) {
060: case '@': // Formula
061: String idAsString = whereClause.substring(pos + 2,
062: endPos);
063: Formula f = report.findFormula(idAsString);
064: if (f.refersTo(p))
065: return true;
066: break;
067: case '?': // Parameter
068: idAsString = whereClause.substring(pos + 2, endPos);
069: if (p.getId().toString().equals(idAsString))
070: return true;
071: break;
072: }
073: pos = endPos + 1;
074: }
075:
076: return false;
077: }
078:
079: /**
080: * Adds a join to the list of joins used by this query.
081: *
082: * @param join a join
083: */
084: public void addJoin(Join join) {
085: joins.add(join);
086: }
087:
088: /**
089: * Adds all joins in the collection to our list.
090: *
091: * @param coll a collection of joins
092: */
093: public void addAllJoins(Collection coll) {
094: joins.addAll(coll);
095: }
096:
097: /**
098: * Removes a join from the list of joins used by this query.
099: *
100: * @param join a join
101: */
102: public void removeJoin(Join join) {
103: joins.remove(join);
104: }
105:
106: /**
107: * Removes all joins in the collection from our list.
108: */
109: public void clearJoins() {
110: joins.clear();
111: }
112:
113: /**
114: * Returns an iterator over all the joins used by this query.
115: *
116: * @return an iterator over all the joins
117: */
118: public Iterator joins() {
119: return joins.iterator();
120: }
121:
122: /**
123: * Returns the where clause fit for human consumption. This mainly means
124: * that we substitute formula, parameter, and user column id numbers with
125: * names. Called from a where clause editor. This code assumes that curly
126: * braces are never nested.
127: *
128: * @return the eval string with formula, parameter, and user column id
129: * numbers replaced with names
130: * @see jimm.datavision.gui.sql.WhereClauseWin
131: */
132: public String getEditableWhereClause() {
133: return Expression.expressionToDisplay(report, whereClause);
134: }
135:
136: /**
137: * Returns the raw where clause string; may be <code>null</code>.
138: *
139: * @return the where clause string; may be <code>null</code>
140: */
141: public String getWhereClause() {
142: return whereClause;
143: }
144:
145: /**
146: * Sets the where clause (may be <code>null</code>). The string passed
147: * to us contains parameter and formula display strings, not their
148: * "real" <code>formulaString</code> representations. Translate the latter
149: * into the former before saving this string.
150: *
151: * @param newWhereClause a where clause string; may be <code>null</code>
152: */
153: public void setEditableWhereClause(String newWhereClause) {
154: setWhereClause(Expression.displayToExpression(report,
155: newWhereClause));
156: }
157:
158: /**
159: * Sets the where clause (may be <code>null</code>).
160: *
161: * @param newWhereClause a where clause string; may be <code>null</code>
162: */
163: public void setWhereClause(String newWhereClause) {
164: whereClause = newWhereClause;
165: }
166:
167: /**
168: * Adds a sort order for the specified selectable. The first character
169: * of the <i>order</i> string is inspected. If it is a 'd' or 'D',
170: * then it's taken to mean "descending". Anything else will result
171: * in an "ascending" sort order.
172: *
173: * @param sel a selectable
174: * @param order either <code>SORT_DESCENDING</code> or
175: * <code>SORT_ASCENDING</code>
176: */
177: public void addSort(Selectable sel, int order) {
178: sortSelectables.add(sel);
179: sortOrders.add(new Integer(order));
180: }
181:
182: /**
183: * Removes a sorting from the list.
184: *
185: * @param sel a selectable
186: */
187: public void removeSort(Selectable sel) {
188: // I used to use a map, but then sorts would not keep their order,
189: // which is important. That's why this code isn't just a map lookup
190: // any more.
191: for (int i = 0; i < sortSelectables.size(); ++i) {
192: if (sortSelectables.get(i) == sel) {
193: sortSelectables.remove(i);
194: sortOrders.remove(i);
195: return;
196: }
197: }
198: }
199:
200: /**
201: * Removes all sorts from our list.
202: */
203: public void clearSorts() {
204: sortSelectables = new ArrayList();
205: sortOrders = new ArrayList();
206: }
207:
208: /**
209: * Returns an iterator over all selectables.
210: *
211: * @return an iterator over all selectables
212: */
213: public Iterator selectables() {
214: return selectables.iterator();
215: }
216:
217: /**
218: * Returns an iterator over all the sorted selectables used by this query.
219: *
220: * @return an iterator
221: */
222: public Iterator sortedSelectables() {
223: return sortSelectables.iterator();
224: }
225:
226: /**
227: * Returns the sort order (<code>SORT_DESCENDING</code>,
228: * <code>SORT_ASCENDING</code>, or <code>SORT_UNDEFINED</code>) of the
229: * specified selectable.
230: *
231: * @param sel a database selectable
232: * @return the sort order (<code>SORT_DESCENDING</code>,
233: * <code>SORT_ASCENDING</code>, or <code>SORT_UNDEFINED</code>) of the
234: * specified selectable.
235: */
236: public int sortOrderOf(Selectable sel) {
237: // I used to use a map, but then sorts would not keep their order,
238: // which is important. That's why this code isn't just a map lookup
239: // any more.
240: for (int i = 0; i < sortSelectables.size(); ++i) {
241: if (sortSelectables.get(i) == sel)
242: return ((Integer) sortOrders.get(i)).intValue();
243: }
244: return SORT_UNDEFINED;
245: }
246:
247: /**
248: * Returns the index of the specified selectable.
249: *
250: * @param selectable a database selectable
251: */
252: public int indexOfSelectable(Selectable selectable) {
253: return selectables.indexOf(selectable);
254: }
255:
256: /**
257: * Builds collections of the selectables actually used in the report.
258: */
259: public void findSelectablesUsed() {
260: // It would be nice if the selectables collection was a set, so we
261: // could avoid the contains() calls below. However, we need it to be
262: // indexable.
263:
264: selectables.clear();
265: report.withFieldsDo(new FieldWalker() {
266: public void step(Field f) {
267: if (f instanceof ColumnField) {
268: Column col = ((ColumnField) f).getColumn();
269: if (!selectables.contains(col))
270: selectables.add(col);
271: } else if (f instanceof FormulaField) {
272: FormulaField ff = (FormulaField) f;
273: for (Iterator iter = ff.columnsUsed().iterator(); iter
274: .hasNext();) {
275: Column col = (Column) iter.next();
276: if (!selectables.contains(col))
277: selectables.add(col);
278: }
279: for (Iterator iter = ff.userColumnsUsed()
280: .iterator(); iter.hasNext();) {
281: UserColumn uc = (UserColumn) iter.next();
282: if (!selectables.contains(uc))
283: selectables.add(uc);
284: }
285: } else if (f instanceof UserColumnField) {
286: UserColumn uc = ((UserColumnField) f)
287: .getUserColumn();
288: if (!selectables.contains(uc))
289: selectables.add(uc);
290: }
291: }
292: });
293:
294: // Add groups' selectables, which may or may not be used in any
295: // report field
296: for (Iterator iter = report.groups(); iter.hasNext();) {
297: Group g = (Group) iter.next();
298: Selectable s = g.getSelectable();
299: if (!selectables.contains(s))
300: selectables.add(s);
301: }
302:
303: // Add all selectables used in sorts.
304: for (Iterator iter = sortedSelectables(); iter.hasNext();) {
305: Selectable s = (Selectable) iter.next();
306: if (!selectables.contains(s))
307: selectables.add(s);
308: }
309:
310: // Add all columns used by subreports' joins. Though only a report
311: // that uses a SQL data source can have subreports right now, that may
312: // not be true in the future. There is no harm in implementing this
313: // here (rather than in SQLQuery).
314: for (Iterator iter = report.subreports(); iter.hasNext();) {
315: Subreport sub = (Subreport) iter.next();
316: for (Iterator subIter = sub.parentColumns(); subIter
317: .hasNext();) {
318: Column col = (Column) subIter.next();
319: if (!selectables.contains(col))
320: selectables.add(col);
321: }
322: }
323: }
324:
325: /**
326: * Returns the number of selectables in the query. Does not recalculate the
327: * selectables used; we assume this is being called after the qurey has been
328: * run or {@link #findSelectablesUsed} has been called.
329: *
330: * @return the number of selectables (database and user columns) in the query
331: */
332: public int getNumSelectables() {
333: return selectables.size();
334: }
335:
336: /**
337: * Called from <code>DataSource.reloadColumns</code>, this method gives the
338: * query source a chance to tell its ancillary objects (such as joins and
339: * the sort) to reload selectable objects.
340: * <p>
341: * This is necessary, for example, after a SQL database data source has
342: * reloaded all of its table and column information. The old column
343: * objects no longer exist. New ones (with the same ids, we assume) have
344: * taken their place.
345: *
346: * @param dataSource the data source
347: */
348: public void reloadColumns(DataSource dataSource) {
349: // Joins
350: for (Iterator iter = joins(); iter.hasNext();) {
351: Join j = (Join) iter.next();
352: j.setFrom(dataSource.findColumn(j.getFrom().getId()));
353: j.setTo(dataSource.findColumn(j.getTo().getId()));
354: }
355:
356: // Selectables
357: ArrayList newSelectables = new ArrayList();
358: for (Iterator iter = selectables.iterator(); iter.hasNext();) {
359: Selectable g = (Selectable) iter.next();
360: newSelectables.add(g.reloadInstance(dataSource));
361: }
362: selectables = newSelectables;
363:
364: // Sort selectables
365: ArrayList newSortCols = new ArrayList();
366: for (Iterator iter = sortSelectables.iterator(); iter.hasNext();) {
367: Selectable s = (Selectable) iter.next();
368: newSortCols.add(s.reloadInstance(dataSource));
369: }
370: sortSelectables = newSortCols;
371: }
372:
373: /**
374: * Writes this query as an XML tag. Writes all joins, where clauses,
375: * and sorts as well.
376: *
377: * @param out a writer that knows how to write XML
378: */
379: public void writeXML(XMLWriter out) {
380: out.startElement("query");
381:
382: ListWriter.writeList(out, joins);
383:
384: if (whereClause != null && whereClause.length() > 0)
385: out.cdataElement("where", whereClause);
386:
387: for (int i = 0; i < sortSelectables.size(); ++i) {
388: int sortOrder = ((Integer) sortOrders.get(i)).intValue();
389: Selectable selectable = (Selectable) sortSelectables.get(i);
390: out.startElement("sort");
391: out
392: .attr("order",
393: sortOrder == Query.SORT_DESCENDING ? "desc"
394: : "asc");
395: out.attr("groupable-id", selectable.getId());
396: out.attr("groupable-type", selectable.fieldTypeString());
397: out.endElement();
398: }
399: writeExtras(out);
400:
401: out.endElement();
402: }
403:
404: /** This method exists so subclasses can write out extra information. */
405: protected void writeExtras(XMLWriter out) {
406: }
407:
408: }
|