Reports: Implement grouping.
[online-glom:gwt-glom.git] / src / main / java / org / glom / web / server / ReportGenerator.java
1 /*
2  * Copyright (C) 2012 Openismus GmbH
3  *
4  * This file is part of GWT-Glom.
5  *
6  * GWT-Glom is free software: you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published by the
8  * Free Software Foundation, either version 3 of the License, or (at your
9  * option) any later version.
10  *
11  * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
14  * for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with GWT-Glom.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package org.glom.web.server;
21
22 import java.io.ByteArrayOutputStream;
23 import java.sql.Connection;
24 import java.util.HashMap;
25
26 import net.sf.jasperreports.engine.JRException;
27 import net.sf.jasperreports.engine.JasperCompileManager;
28 import net.sf.jasperreports.engine.JasperFillManager;
29 import net.sf.jasperreports.engine.JasperPrint;
30 import net.sf.jasperreports.engine.JasperReport;
31 import net.sf.jasperreports.engine.design.JRDesignBand;
32 import net.sf.jasperreports.engine.design.JRDesignExpression;
33 import net.sf.jasperreports.engine.design.JRDesignField;
34 import net.sf.jasperreports.engine.design.JRDesignGroup;
35 import net.sf.jasperreports.engine.design.JRDesignQuery;
36 import net.sf.jasperreports.engine.design.JRDesignSection;
37 import net.sf.jasperreports.engine.design.JRDesignStaticText;
38 import net.sf.jasperreports.engine.design.JRDesignTextField;
39 import net.sf.jasperreports.engine.design.JasperDesign;
40 import net.sf.jasperreports.engine.export.JRHtmlExporterParameter;
41 import net.sf.jasperreports.engine.export.JRXhtmlExporter;
42
43 import org.apache.commons.lang.StringUtils;
44 import org.glom.libglom.Document;
45 import org.glom.libglom.Glom;
46 import org.glom.libglom.LayoutFieldVector;
47 import org.glom.libglom.LayoutGroup;
48 import org.glom.libglom.LayoutItemVector;
49 import org.glom.libglom.LayoutItem_Field;
50 import org.glom.libglom.LayoutItem_GroupBy;
51 import org.glom.libglom.Relationship;
52 import org.glom.libglom.SortClause;
53 import org.glom.libglom.SortFieldPair;
54 import org.glom.libglom.SqlBuilder;
55 import org.glom.libglom.SqlExpr;
56 import org.glom.libglom.Value;
57
58 /**
59  * @author Murray Cumming <murrayc@openimus.com>
60  * 
61  */
62 public class ReportGenerator {
63
64         final int height = 30;
65         LayoutFieldVector fieldsToGet = new LayoutFieldVector();
66         SortClause sortClause = new SortClause();
67
68         /**
69          * @param tableName
70          * @param reportName
71          * @param configuredDoc
72          * @param layout_group
73          * @return
74          */
75         public String generateReport(final Document document, final String tableName, final String reportName,
76                         final Connection connection, final org.glom.libglom.LayoutGroup layout_group) {
77
78                 final JasperDesign design = new JasperDesign();
79                 design.setName(reportName); // TODO: Actually, we want the title.
80
81                 final JRDesignBand titleBand = new JRDesignBand();
82                 titleBand.setHeight(height);
83                 final JRDesignStaticText staticTitle = new JRDesignStaticText();
84                 staticTitle.setText("debug: test report title text");
85                 titleBand.addElement(staticTitle);
86                 design.setTitle(titleBand);
87
88                 final JRDesignBand detailBand = new JRDesignBand();
89                 detailBand.setHeight(height + 20);
90
91                 fieldsToGet = new LayoutFieldVector();
92                 final int x = 0;
93                 addToReport(layout_group, design, detailBand, x);
94
95                 ((JRDesignSection) design.getDetailSection()).addBand(detailBand);
96
97                 // Later versions of libglom actually return an empty SqlExpr when quickFindValue is empty,
98                 // but let's be sure:
99                 final String quickFind = ""; // TODO
100                 SqlExpr whereClause;
101                 if (StringUtils.isEmpty(quickFind)) {
102                         whereClause = new SqlExpr();
103                 } else {
104                         final Value quickFindValue = new Value(quickFind);
105                         whereClause = Glom.get_find_where_clause_quick(document, tableName, quickFindValue);
106                 }
107
108                 final Relationship extraJoin = new Relationship(); // Ignored.
109                 final SqlBuilder builder = Glom.build_sql_select_with_where_clause(tableName, fieldsToGet, whereClause,
110                                 extraJoin, sortClause);
111                 final String sqlQuery = Glom.sqlbuilder_get_full_query(builder);
112
113                 final JRDesignQuery query = new JRDesignQuery();
114                 query.setText(sqlQuery); // TODO: Extra sort clause to sort the rows within the groups.
115                 design.setQuery(query);
116
117                 JasperReport report;
118                 try {
119                         report = JasperCompileManager.compileReport(design);
120                 } catch (final JRException ex) {
121                         ex.printStackTrace();
122                         return "Failed to Generate HTML: compileReport() failed.";
123                 }
124
125                 JasperPrint print;
126                 try {
127                         final HashMap<String, Object> parameters = new HashMap<String, Object>();
128                         parameters.put("ReportTitle", reportName); // TODO: Use the title, not the name.
129                         print = JasperFillManager.fillReport(report, parameters, connection);
130                 } catch (final JRException ex) {
131                         ex.printStackTrace();
132                         return "Failed to Generate HTML: fillReport() failed.";
133                 }
134
135                 final ByteArrayOutputStream output = new ByteArrayOutputStream();
136
137                 // We use this because there is no JasperExportManager.exportReportToHtmlStream() method.
138                 // JasperExportManager.exportReportToXmlStream(print, output);
139                 try {
140                         final JRXhtmlExporter exporter = new JRXhtmlExporter();
141                         exporter.setParameter(JRHtmlExporterParameter.JASPER_PRINT, print);
142                         exporter.setParameter(JRHtmlExporterParameter.OUTPUT_STREAM, output);
143
144                         // Use points instead of pixels for sizes, because pixels are silly
145                         // in HTML:
146                         exporter.setParameter(JRHtmlExporterParameter.SIZE_UNIT, "pt");
147
148                         exporter.exportReport();
149                 } catch (final JRException ex) {
150                         ex.printStackTrace();
151                         return "Failed to Generate HTML: exportReport() failed.";
152                 }
153
154                 // System.out.print(output.toString() + "\n");
155                 return output.toString();
156         }
157
158         /**
159          * @param layout_group
160          * @param design
161          * @param height
162          * @param parentBand
163          * @param x
164          */
165         private int addToReport(final org.glom.libglom.LayoutGroup layout_group, final JasperDesign design,
166                         final JRDesignBand parentBand, int x) {
167                 final LayoutItemVector layoutItemsVec = layout_group.get_items();
168                 final int numItems = Utils.safeLongToInt(layoutItemsVec.size());
169                 for (int i = 0; i < numItems; i++) {
170                         final org.glom.libglom.LayoutItem libglomLayoutItem = layoutItemsVec.get(i);
171
172                         final LayoutGroup libglomLayoutGroup = LayoutGroup.cast_dynamic(libglomLayoutItem);
173                         final LayoutItem_Field libglomLayoutItemField = LayoutItem_Field.cast_dynamic(libglomLayoutItem);
174                         if (libglomLayoutItemField != null) {
175                                 x = addFieldToBand(design, parentBand, x, libglomLayoutItemField);
176                         } else if (libglomLayoutGroup != null) {
177                                 final LayoutItem_GroupBy libglomGroupBy = LayoutItem_GroupBy.cast_dynamic(libglomLayoutGroup);
178                                 if (libglomGroupBy != null) {
179                                         final LayoutItem_Field fieldGroupBy = libglomGroupBy.get_field_group_by();
180                                         if (fieldGroupBy == null)
181                                                 continue;
182
183                                         final String fieldName = addField(design, fieldGroupBy);
184
185                                         // We must sort by the group field,
186                                         // so that JasperReports can start a new group when its value changes.
187                                         // Note that this is not like a SQL GROUP BY.
188                                         final SortFieldPair pair = new SortFieldPair();
189                                         pair.setFirst(fieldGroupBy);
190                                         pair.setSecond(true); // Ascending.
191                                         sortClause.add(pair);
192
193                                         final JRDesignGroup group = new JRDesignGroup();
194                                         group.setName(fieldName);
195
196                                         final JRDesignExpression expression = new JRDesignExpression();
197                                         expression.setText("$F{" + fieldName + "}");
198                                         group.setExpression(expression);
199
200                                         final JRDesignBand headerBand = new JRDesignBand();
201                                         headerBand.setHeight(height);
202                                         ((JRDesignSection) group.getGroupHeaderSection()).addBand(headerBand);
203
204                                         final JRDesignBand footerBand = new JRDesignBand();
205                                         footerBand.setHeight(height);
206                                         ((JRDesignSection) group.getGroupFooterSection()).addBand(footerBand);
207
208                                         try {
209                                                 design.addGroup(group);
210                                         } catch (final JRException e) {
211                                                 // TODO Auto-generated catch block
212                                                 e.printStackTrace();
213                                         }
214
215                                         // Show the group-by field:
216                                         final int groupX = addFieldToBand(design, headerBand, x, fieldGroupBy);
217
218                                         // Show the secondary fields:
219                                         final LayoutGroup groupSecondaries = libglomGroupBy.get_group_secondary_fields();
220                                         if (groupSecondaries != null)
221                                                 addToReport(groupSecondaries, design, headerBand, groupX);
222                                 }
223
224                                 // Recurse into sub-groups:
225                                 x = addToReport(libglomLayoutGroup, design, parentBand, x);
226                         }
227                 }
228
229                 return x;
230         }
231
232         /**
233          * @param design
234          * @param parentBand
235          * @param x
236          * @param libglomLayoutItemField
237          * @return
238          */
239         private int addFieldToBand(final JasperDesign design, final JRDesignBand parentBand, int x,
240                         final LayoutItem_Field libglomLayoutItemField) {
241                 final String fieldName = addField(design, libglomLayoutItemField);
242
243                 // Tell the JasperDesign to show an instance of the field:
244                 final JRDesignTextField textField = new JRDesignTextField();
245
246                 // Make sure this field starts at the right of the previous field,
247                 // because JasperReports uses absolute positioning.
248                 textField.setY(0);
249                 textField.setX(x);
250
251                 // An arbitrary width, because we must specify _some_ width:
252                 final int width = 100; // Points, as specified later.
253                 textField.setWidth(width); // No data will be shown without this.
254                 x += width;
255
256                 // This only stretches vertically, but that is better than
257                 // nothing.
258                 textField.setStretchWithOverflow(true);
259                 textField.setHeight(height); // We must specify _some_ height.
260
261                 // TODO: Where is this format documented?
262                 final JRDesignExpression expression = new JRDesignExpression();
263                 expression.setText("$F{" + fieldName + "}");
264
265                 textField.setExpression(expression);
266                 parentBand.addElement(textField);
267                 return x;
268         }
269
270         /**
271          * @param design
272          * @param libglomLayoutItemField
273          * @return
274          */
275         private String addField(final JasperDesign design, final LayoutItem_Field libglomLayoutItemField) {
276                 fieldsToGet.add(libglomLayoutItemField);
277
278                 final String fieldName = libglomLayoutItemField.get_name();
279                 // System.out.print("fieldName=" + fieldName + "\n");
280
281                 // Tell the JasperDesign about the database field that will be in the SQL query,
282                 // specified later:
283                 final JRDesignField field = new JRDesignField();
284                 field.setName(fieldName); // TODO: Related fields.
285                 field.setValueClass(getClassTypeForGlomType(libglomLayoutItemField));
286
287                 try {
288                         design.addField(field);
289                 } catch (final JRException e2) {
290                         // TODO Auto-generated catch block
291                         e2.printStackTrace();
292                 }
293                 return fieldName;
294         }
295
296         /**
297          * @param libglomLayoutItemField
298          * @return
299          */
300         private Class<?> getClassTypeForGlomType(final LayoutItem_Field libglomLayoutItemField) {
301                 // Choose a suitable java class type for the SQL field:
302                 Class<?> klass = null;
303                 switch (libglomLayoutItemField.get_glom_type()) {
304                 case TYPE_TEXT:
305                         klass = java.lang.String.class;
306                         break;
307                 case TYPE_BOOLEAN:
308                         klass = java.lang.Boolean.class;
309                         break;
310                 case TYPE_NUMERIC:
311                         klass = java.lang.Double.class;
312                         break;
313                 case TYPE_DATE:
314                         klass = java.util.Date.class;
315                         break;
316                 case TYPE_TIME:
317                         klass = java.sql.Time.class;
318                         break;
319                 case TYPE_IMAGE:
320                         klass = java.sql.Blob.class; // TODO: This does not work.
321                         break;
322                 }
323                 return klass;
324         }
325 }