View Javadoc

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 }