1 package org.apache.torque.util;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import java.io.IOException;
23 import java.io.Writer;
24 import java.sql.Connection;
25 import java.sql.ResultSet;
26 import java.sql.SQLException;
27 import java.sql.Statement;
28 import java.util.ArrayList;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Vector;
32
33 import org.apache.commons.collections.OrderedMapIterator;
34 import org.apache.commons.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36 import org.apache.torque.Column;
37 import org.apache.torque.ColumnImpl;
38 import org.apache.torque.TorqueException;
39 import org.apache.torque.criteria.SqlEnum;
40 import org.apache.torque.om.mapper.ObjectListMapper;
41 import org.apache.torque.om.mapper.RecordMapper;
42 import org.apache.torque.sql.SqlBuilder;
43 import org.apache.torque.util.functions.SQLFunction;
44
45 /**
46 * <p>A utility to help produce aggregate summary information about a table.
47 * The default assumes that the underlying DB supports the SQL 99 Standard
48 * Aggregate functions, e.g. COUNT, SUM, AVG, MAX, & MIN. However, some
49 * non-standard functions (like MySQL's older LEAST instead of MIN can be
50 * handled programatically if needed (@see Aggregate class)</p>
51 *
52 * <P>Here is a simple example to generate the results of a query like:</P>
53 *
54 * <pre>
55 * SELECT EMPLOYEE, SUM(HOURS), MIN(HOURS), MAX(HOURS)
56 * FROM TIMESHEET WHERE TYPE = 1 GROUP BY EMPLOYEE ORDER BY EMPLOYEE ASC
57 * </pre>
58 * <p>Use the following code</p>
59 * <pre>
60 * SummaryHelper sHelp = new SummaryHelper();
61 * Criteria c = new Criteria();
62 * c.add(TimeSheetPeer.TYPE, 1);
63 * c.addAscendingOrderBy(TimeSheetPeer.EMPLOYEE);
64 * sHelper.addGroupBy(TimeSheetPeer.EMPLOYEE);
65 * sHelper.addAggregate(FunctionFactory.Sum(TimeSheetPeer.HOURS),"Hours");
66 * sHelper.addAggregate(FunctionFactory.Min(TimeSheetPeer.HOURS),"Min_Hrs");
67 * sHelper.addAggregate(FunctionFactory.Max(TimeSheetPeer.HOURS),"Max_Hrs");
68 * List results = sHelper.summarize( c );
69 * </pre>
70 * <p>The results list will be an OrderedMap with a key of either the group by
71 * column name or the name specified for the aggregate function (e.g. EMPLOYEE
72 * or Hours). The value will be a Village Value Class. Below is a simple
73 * way to do this. See the dumpResults* method code for a more complex example.
74 * </p>
75 * <pre>
76 * String emp = results.get("EMPLOYEE").asString();
77 * int hours = results.get("Hours").asInt();
78 * </pre>
79 * <p>
80 * Notes:</p>
81 * <p>
82 * If there are no group by columns specified, the aggregate is over the
83 * whole table. The from table is defined either via the Criteria.addAlias(...)
84 * method or by the first table prefix in an aggregate function.</p>
85 * <p>
86 * This will also work with joined tables if the criteria is creates as
87 * to create valid SQL.</p>
88 *
89 * @author <a href="mailto:greg.monroe@dukece.com">Greg Monroe</a>
90 * @version $Id: SummaryHelper.java 1388656 2012-09-21 19:59:16Z tfischer $
91 */
92 public class SummaryHelper
93 {
94 /** The class log. */
95 private static Log logger = LogFactory.getLog(SummaryHelper.class);
96
97 /** A list of the group by columns. */
98 private List<Column> groupByColumns;
99 /** A ListOrderMapCI<String, Aggregate.Function> with the aggregate functions
100 * to use in generating results. */
101 private ListOrderedMapCI aggregates;
102 /** Flag for excluding unnamed columns. */
103 private boolean excludeExprColumns = false;
104
105 /**
106 * Return a list of ListOrderedMapCI objects with the results of the summary
107 * query. The ListOrderedMapCI objects have a key of the column name or
108 * function alias and are in the order generated by the query.
109 * The class of the return values are decided by the database driver,
110 * which makes this method not database independent.
111 *
112 * @param crit The base criteria to build on.
113 *
114 * @return Results as a OrderMap<String, List<Object>> object.
115 *
116 * @throws TorqueException if a database error occurs.
117 *
118 * @deprecated please use
119 * summarize(org.apache.torque.criteria.Criteria)
120 * instead.
121 * This method will be removed in a future version of Torque.
122 */
123 @Deprecated
124 public List<ListOrderedMapCI> summarize(Criteria crit)
125 throws TorqueException
126 {
127 return summarize(crit, (List<Class<?>>) null);
128 }
129
130 /**
131 * Return a list of ListOrderedMapCI objects with the results of the summary
132 * query. The ListOrderedMapCI objects have a key of the column name or
133 * function alias and are in the order generated by the query.
134 * The class of the return values are decided by the database driver,
135 * which makes this method not database independent.
136 *
137 * @param crit The base criteria to build on.
138 *
139 * @return Results as a OrderMap<String, List<Object>> object.
140 *
141 * @throws TorqueException if a database error occurs.
142 */
143 public List<ListOrderedMapCI> summarize(
144 org.apache.torque.criteria.Criteria crit)
145 throws TorqueException
146 {
147 return summarize(crit, (List<Class<?>>) null);
148 }
149
150 /**
151 * Return a list of ListOrderedMapCI objects with the results of the summary
152 * query. The ListOrderedMapCI objects have a key of the column name or
153 * function alias and are in the order generated by the query.
154 *
155 * @param crit The base criteria to build on.
156 * @param resultTypes the classes to which the return values of the query
157 * should be cast, or null to let the database driver decide.
158 * See org.apache.torque.om.mapper.ObjectListMapper�for the supported
159 * classes.
160 *
161 * @return Results as a ListOrderMapCI<String, List<Object>> object.
162 *
163 * @throws TorqueException if a database error occurs.
164 *
165 * @deprecated Please use
166 * summarize(org.apache.torque.criteria.Criteria, List<Class<?>>)
167 * instead.
168 * This method will be removed in a future version of Torque.
169 */
170 @Deprecated
171 public List<ListOrderedMapCI> summarize(
172 Criteria crit,
173 List<Class<?>> resultTypes)
174 throws TorqueException
175 {
176 Connection connection = null;
177 try
178 {
179 connection = Transaction.begin(crit.getDbName());
180 List<ListOrderedMapCI> result = summarize(crit, resultTypes, connection);
181 Transaction.commit(connection);
182 connection = null;
183 return result;
184 }
185 finally
186 {
187 if (connection != null)
188 {
189 Transaction.safeRollback(connection);
190 }
191 }
192 }
193
194 /**
195 * Return a list of ListOrderedMapCI objects with the results of the summary
196 * query. The ListOrderedMapCI objects have a key of the column name or
197 * function alias and are in the order generated by the query.
198 *
199 * @param crit The base criteria to build on.
200 * @param resultTypes the classes to which the return values of the query
201 * should be cast, or null to let the database driver decide.
202 * See org.apache.torque.om.mapper.ObjectListMapper�for the supported
203 * classes.
204 *
205 * @return Results as a ListOrderMapCI<String, List<Object>> object.
206 *
207 * @throws TorqueException if a database error occurs.
208 */
209 public List<ListOrderedMapCI> summarize(
210 org.apache.torque.criteria.Criteria crit,
211 List<Class<?>> resultTypes)
212 throws TorqueException
213 {
214 Connection connection = null;
215 try
216 {
217 connection = Transaction.begin(crit.getDbName());
218 List<ListOrderedMapCI> result = summarize(crit, resultTypes, connection);
219 Transaction.commit(connection);
220 connection = null;
221 return result;
222 }
223 finally
224 {
225 if (connection != null)
226 {
227 Transaction.safeRollback(connection);
228 }
229 }
230 }
231
232 /**
233 * Return a list of OrderedMap objects with the results of the summary
234 * query. The OrderedMap objects have a key of the column name or
235 * function alias and are in the order generated by the query.
236 * The class of the return values are decided by the database driver,
237 * which makes this method not database independent.
238 *
239 * @param crit The base criteria to build on.
240 * @param conn The DB Connection to use.
241 *
242 * @return Results as a OrderMap<String, List<Object>> object.
243 *
244 * @throws TorqueException if a database error occurs.
245 *
246 * @deprecated please use
247 * summarize(org.apache.torque.criteria.Criteria, Connection)
248 * instead.
249 * This method will be removed in a future version of Torque.
250 */
251 @Deprecated
252 public List<ListOrderedMapCI> summarize(Criteria crit, Connection conn)
253 throws TorqueException
254 {
255 return summarize(crit, null, conn);
256 }
257
258 /**
259 * Return a list of OrderedMap objects with the results of the summary
260 * query. The OrderedMap objects have a key of the column name or
261 * function alias and are in the order generated by the query.
262 * The class of the return values are decided by the database driver,
263 * which makes this method not database independent.
264 *
265 * @param crit The base criteria to build on.
266 * @param conn The DB Connection to use.
267 *
268 * @return Results as a OrderMap<String, List<Object>> object.
269 *
270 * @throws TorqueException if a database error occurs.
271 */
272 public List<ListOrderedMapCI> summarize(
273 org.apache.torque.criteria.Criteria crit,
274 Connection conn)
275 throws TorqueException
276 {
277 return summarize(crit, null, conn);
278 }
279
280 /**
281 * Return a list of ListOrderedMapCI objects with the results of the summary
282 * query. The ListOrderedMapCI objects have a key of the column name or
283 * function alias and are in the order generated by the query.
284 *
285 * @param crit The base criteria to build on.
286 * @param resultTypes the classes to which the return values of the query
287 * should be cast, or null to let the database driver decide.
288 * See org.apache.torque.om.mapper.ObjectListMapper�for the supported
289 * classes.
290 * @param conn The DB Connection to use.
291 *
292 * @return Results as a ListOrderedMapCI<String,Values> object.
293 *
294 * @throws TorqueException if a database error occurs.
295 *
296 * @deprecated please use
297 * summarize(org.apache.torque.criteria.Criteria, List<Class<?>>, Connection)
298 * instead.
299 * This method will be removed in a future version of Torque.
300 */
301 @Deprecated
302 public List<ListOrderedMapCI> summarize(
303 Criteria crit,
304 List<Class<?>> resultTypes,
305 Connection conn)
306 throws TorqueException
307 {
308 Criteria c = buildCriteria(crit);
309 String query = SqlBuilder.buildQuery(c).toString();
310 RecordMapper<List<Object>> mapper = new ObjectListMapper(resultTypes);
311
312 Statement statement = null;
313 ResultSet resultSet = null;
314 List<List<Object>> rows = new ArrayList<List<Object>>();
315 try
316 {
317 statement = conn.createStatement();
318 long startTime = System.currentTimeMillis();
319 logger.debug("Executing query " + query);
320
321 resultSet = statement.executeQuery(query.toString());
322 long queryEndTime = System.currentTimeMillis();
323 logger.trace("query took " + (queryEndTime - startTime)
324 + " milliseconds");
325
326 while (resultSet.next())
327 {
328 List<Object> rowResult = mapper.processRow(resultSet, 0);
329 rows.add(rowResult);
330 }
331 long mappingEndTime = System.currentTimeMillis();
332 logger.trace("mapping took " + (mappingEndTime - queryEndTime)
333 + " milliseconds");
334 }
335 catch (SQLException e)
336 {
337 throw new TorqueException(e);
338 }
339 finally
340 {
341 if (resultSet != null)
342 {
343 try
344 {
345 resultSet.close();
346 }
347 catch (SQLException e)
348 {
349 logger.warn("error closing resultSet", e);
350 }
351 }
352 if (statement != null)
353 {
354 try
355 {
356 statement.close();
357 }
358 catch (SQLException e)
359 {
360 logger.warn("error closing statement", e);
361 }
362 }
363 }
364
365 List<ListOrderedMapCI> resultsList = new Vector<ListOrderedMapCI>(rows.size());
366 List<String> columnNames = new ArrayList<String>();
367 for (Column column : c.getSelectColumns())
368 {
369 columnNames.add(column.getColumnName());
370 }
371 columnNames.addAll(c.getAsColumns().keySet());
372 for (List<Object> row : rows)
373 {
374 ListOrderedMapCI recordMap = new ListOrderedMapCI();
375 for (int i = 0; i < row.size(); i++)
376 {
377 Object value = row.get(i);
378 String cName = columnNames.get(i);
379 if (cName == null || cName.equals(""))
380 {
381 if (excludeExprColumns())
382 {
383 continue;
384 }
385 cName = "Expr" + i;
386 }
387 recordMap.put(cName, value);
388 }
389 resultsList.add(recordMap);
390 }
391 return resultsList;
392 }
393
394 /**
395 * Return a list of ListOrderedMapCI objects with the results of the summary
396 * query. The ListOrderedMapCI objects have a key of the column name or
397 * function alias and are in the order generated by the query.
398 *
399 * @param crit The base criteria to build on.
400 * @param resultTypes the classes to which the return values of the query
401 * should be cast, or null to let the database driver decide.
402 * See org.apache.torque.om.mapper.ObjectListMapper�for the supported
403 * classes.
404 * @param conn The DB Connection to use.
405 *
406 * @return Results as a ListOrderedMapCI<String,Values> object.
407 *
408 * @throws TorqueException if a database error occurs.
409 */
410 public List<ListOrderedMapCI> summarize(
411 org.apache.torque.criteria.Criteria crit,
412 List<Class<?>> resultTypes,
413 Connection conn)
414 throws TorqueException
415 {
416 org.apache.torque.criteria.Criteria c = buildCriteria(crit);
417 String query = SqlBuilder.buildQuery(c).toString();
418 RecordMapper<List<Object>> mapper = new ObjectListMapper(resultTypes);
419
420 Statement statement = null;
421 ResultSet resultSet = null;
422 List<List<Object>> rows = new ArrayList<List<Object>>();
423 try
424 {
425 statement = conn.createStatement();
426 long startTime = System.currentTimeMillis();
427 logger.debug("Executing query " + query);
428
429 resultSet = statement.executeQuery(query.toString());
430 long queryEndTime = System.currentTimeMillis();
431 logger.trace("query took " + (queryEndTime - startTime)
432 + " milliseconds");
433
434 while (resultSet.next())
435 {
436 List<Object> rowResult = mapper.processRow(resultSet, 0);
437 rows.add(rowResult);
438 }
439 long mappingEndTime = System.currentTimeMillis();
440 logger.trace("mapping took " + (mappingEndTime - queryEndTime)
441 + " milliseconds");
442 }
443 catch (SQLException e)
444 {
445 throw new TorqueException(e);
446 }
447 finally
448 {
449 if (resultSet != null)
450 {
451 try
452 {
453 resultSet.close();
454 }
455 catch (SQLException e)
456 {
457 logger.warn("error closing resultSet", e);
458 }
459 }
460 if (statement != null)
461 {
462 try
463 {
464 statement.close();
465 }
466 catch (SQLException e)
467 {
468 logger.warn("error closing statement", e);
469 }
470 }
471 }
472
473 List<ListOrderedMapCI> resultsList = new Vector<ListOrderedMapCI>(rows.size());
474 List<String> columnNames = new ArrayList<String>();
475 for (Column column : c.getSelectColumns())
476 {
477 columnNames.add(column.getColumnName());
478 }
479 columnNames.addAll(c.getAsColumns().keySet());
480 for (List<Object> row : rows)
481 {
482 ListOrderedMapCI recordMap = new ListOrderedMapCI();
483 for (int i = 0; i < row.size(); i++)
484 {
485 Object value = row.get(i);
486 String cName = columnNames.get(i);
487 if (cName == null || cName.equals(""))
488 {
489 if (excludeExprColumns())
490 {
491 continue;
492 }
493 cName = "Expr" + i;
494 }
495 recordMap.put(cName, value);
496 }
497 resultsList.add(recordMap);
498 }
499 return resultsList;
500 }
501
502 /**
503 * Builds the criteria to use in summarizing the information. Note that
504 * the criteria passed in will be modified.
505 *
506 * @param c The base criteria to build the summary criteria from.
507 * @return A criteria to use in summarizing the information.
508 * @throws TorqueException
509 *
510 * @deprecated please use
511 * buildCriteria(org.apache.torque.criteria.Criteria)
512 * instead.
513 * This method will be removed in a future version of Torque.
514 */
515 @Deprecated
516 public Criteria buildCriteria(Criteria c) throws TorqueException
517 {
518 c.getSelectColumns().clear();
519 c.getGroupByColumns().clear();
520
521 UniqueList<String> criteriaSelectModifiers;
522 criteriaSelectModifiers = c.getSelectModifiers();
523
524 if (criteriaSelectModifiers != null
525 && criteriaSelectModifiers.size() > 0
526 && criteriaSelectModifiers.contains(SqlEnum.DISTINCT.toString()))
527 {
528 criteriaSelectModifiers.remove(SqlEnum.DISTINCT.toString());
529 }
530 c.setIgnoreCase(false);
531
532 List<Column> cols = getGroupByColumns();
533 boolean haveFromTable = !cols.isEmpty(); // Group By cols define src table.
534 for (Column col : cols)
535 {
536 c.addGroupByColumn(col);
537 c.addSelectColumn(col);
538 }
539 if (haveFromTable)
540 {
541 logger.debug("From table defined by Group By Cols");
542 }
543
544 // Check if the from table is set via a where clause.
545 if (!haveFromTable && !c.isEmpty())
546 {
547 haveFromTable = true;
548 logger.debug("From table defined by a where clause");
549 }
550
551 ListOrderedMapCI cMap = getAggregates();
552 OrderedMapIterator iMap = cMap.orderedMapIterator();
553 while (iMap.hasNext())
554 {
555 String key = (String) iMap.next();
556 SQLFunction f = (SQLFunction) iMap.getValue();
557 Column col = f.getColumn();
558 c.addAsColumn(key, new ColumnImpl(
559 null,
560 col.getTableName(),
561 col.getColumnName(),
562 f.getSqlExpression()));
563 if (!haveFromTable) // Last chance. Get it from the func.
564 {
565 {
566 // Kludgy Where table.col = table.col clause to force
567 // from table identification.
568 c.add(col,
569 (col.getColumnName()
570 + "=" + col.getColumnName()),
571 SqlEnum.CUSTOM);
572 haveFromTable = true;
573
574 String table = col.getTableName();
575 logger.debug("From table, '" + table
576 + "', defined from aggregate column");
577 }
578 }
579 }
580 if (!haveFromTable)
581 {
582 throw new TorqueException(
583 "No FROM table defined by the GroupBy set, "
584 + "criteria.setAlias, or specified function column!");
585 }
586 return c;
587 }
588
589 /**
590 * Builds the criteria to use in summarizing the information. Note that
591 * the criteria passed in will be modified.
592 *
593 * @param c The base criteria to build the summary criteria from.
594 * @return A criteria to use in summarizing the information.
595 * @throws TorqueException
596 */
597 public org.apache.torque.criteria.Criteria buildCriteria(
598 org.apache.torque.criteria.Criteria c) throws TorqueException
599 {
600 c.getSelectColumns().clear();
601 c.getGroupByColumns().clear();
602
603 UniqueList<String> criteriaSelectModifiers;
604 criteriaSelectModifiers = c.getSelectModifiers();
605
606 if (criteriaSelectModifiers != null
607 && criteriaSelectModifiers.size() > 0
608 && criteriaSelectModifiers.contains(SqlEnum.DISTINCT.toString()))
609 {
610 criteriaSelectModifiers.remove(SqlEnum.DISTINCT.toString());
611 }
612 c.setIgnoreCase(false);
613
614 List<Column> cols = getGroupByColumns();
615 boolean haveFromTable = !cols.isEmpty(); // Group By cols define src table.
616 for (Column col : cols)
617 {
618 c.addGroupByColumn(col);
619 c.addSelectColumn(col);
620 }
621 if (haveFromTable)
622 {
623 logger.debug("From table defined by Group By Cols");
624 }
625
626 // Check if the from table is set via a where clause.
627 if (!haveFromTable && c.getTopLevelCriterion() != null)
628 {
629 haveFromTable = true;
630 logger.debug("From table defined by a where clause");
631 }
632
633 ListOrderedMapCI cMap = getAggregates();
634 OrderedMapIterator iMap = cMap.orderedMapIterator();
635 while (iMap.hasNext())
636 {
637 String key = (String) iMap.next();
638 SQLFunction f = (SQLFunction) iMap.getValue();
639 Column col = f.getColumn();
640 c.addAsColumn(key, new ColumnImpl(
641 null,
642 col.getTableName(),
643 col.getColumnName(),
644 f.getSqlExpression()));
645 if (!haveFromTable) // Last chance. Get it from the func.
646 {
647 {
648 // Kludgy Where table.col = table.col clause to force
649 // from table identification.
650 c.and(col,
651 (col.getColumnName()
652 + "=" + col.getColumnName()),
653 SqlEnum.CUSTOM);
654 haveFromTable = true;
655
656 String table = col.getTableName();
657 logger.debug("From table, '" + table
658 + "', defined from aggregate column");
659 }
660 }
661 }
662 if (!haveFromTable)
663 {
664 throw new TorqueException(
665 "No FROM table defined by the GroupBy set, "
666 + "criteria.setAlias, or specified function column!");
667 }
668 return c;
669 }
670
671 /**
672 * <p>
673 * Add a column that will be used to group the aggregate results by.
674 * This is a first added / first listed on SQL method. E.g.,
675 * </p>
676 * <pre>
677 * add(TablePeer.COL1);
678 * add(TablePeer.COL2);
679 * </pre>
680 *
681 * <p>Generates SQL like: SELECT .... GROUP BY Table.COL1, TABLE.COL2</p>
682 *
683 * @param column
684 */
685 public void addGroupBy(Column column)
686 {
687 getGroupByColumns().add(column);
688 }
689
690 /**
691 * Add in an Aggregate function to the summary information.
692 *
693 * @param alias A valid SQL99 column identifier ([_A-Z0-9] no spaces and
694 * no key words, e.g. function names.
695 * @param function One of the inner classes from the Aggregate class.
696 */
697 public void addAggregate(String alias, SQLFunction function)
698 {
699 getAggregates().put(alias, function);
700 }
701
702 /**
703 * Resets the class internal variables to their initial states so
704 * the class can be re-used like a new class.
705 */
706 public void clear()
707 {
708 getGroupByColumns().clear();
709 getAggregates().clear();
710 setExcludeExprColumns(false);
711 }
712
713 public List<Column> getGroupByColumns()
714 {
715 if (groupByColumns == null)
716 {
717 groupByColumns = new Vector<Column>();
718 }
719 return groupByColumns;
720 }
721
722 /**
723 * Get the order map list of aggregate functions to use in
724 * summarizing this table's informations. The key is used
725 * as the result column alias.
726 *
727 * @return the avgColumns. Will always return a ListOrderedMap object.
728 */
729 public ListOrderedMapCI getAggregates()
730 {
731 if (aggregates == null)
732 {
733 aggregates = new ListOrderedMapCI();
734 }
735 return aggregates;
736 }
737
738 /**
739 * Convenience method to dump a summary results list to an output writer
740 * in a semi-CSV format. E.g., there is no handling of embedded
741 * quotes/special characters.
742 *
743 * @param out
744 * @param results
745 * @param includeHeader
746 * @throws IOException
747 */
748 public void dumpResults(Writer out, List<?> results, boolean includeHeader)
749 throws IOException
750 {
751 Iterator<?> i = results.iterator();
752 boolean first = includeHeader;
753
754 while (i.hasNext())
755 {
756 ListOrderedMapCI rec = (ListOrderedMapCI) i.next();
757 OrderedMapIterator rI = rec.orderedMapIterator();
758 StringBuilder heading = new StringBuilder();
759 StringBuilder recString = new StringBuilder();
760 while (rI.hasNext())
761 {
762 String colId = (String) rI.next();
763 if (first)
764 {
765 heading.append("\"").append(colId).append("\"");
766 if (rI.hasNext())
767 {
768 heading.append(", ");
769 }
770 }
771 Object v = rI.getValue();
772 recString.append(v.toString());
773 if (rI.hasNext())
774 {
775 recString.append(", ");
776 }
777 }
778 if (first)
779 {
780 first = false;
781 out.write(heading.toString());
782 out.write("\n");
783 }
784 out.write(recString.toString());
785 out.write("\n");
786 }
787 }
788
789 /**
790 * Should the results include unnamed columns, e.g. EXPR{index#}.
791 *
792 * @return the excludeExprColumns
793 */
794 public boolean excludeExprColumns()
795 {
796 return excludeExprColumns;
797 }
798
799 /**
800 * <p>Define if unnamed output columns which get labeled as EXPR{index#})
801 * should be included in the the output set.</p>
802 * <p>
803 * Note these are generally added by the criteria
804 * processing to handle special cases such as case insensitive ordering.
805 * </p>
806 *
807 * @param excludeExprColumns if True, these columns won't be included.
808 */
809 public void setExcludeExprColumns(boolean excludeExprColumns)
810 {
811 this.excludeExprColumns = excludeExprColumns;
812 }
813 }