ReportGenerator: Take the Report itself instead of the name and group.
[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.JRDesignLine;
36 import net.sf.jasperreports.engine.design.JRDesignQuery;
37 import net.sf.jasperreports.engine.design.JRDesignSection;
38 import net.sf.jasperreports.engine.design.JRDesignStaticText;
39 import net.sf.jasperreports.engine.design.JRDesignStyle;
40 import net.sf.jasperreports.engine.design.JRDesignTextField;
41 import net.sf.jasperreports.engine.design.JasperDesign;
42 import net.sf.jasperreports.engine.export.JRHtmlExporterParameter;
43 import net.sf.jasperreports.engine.export.JRXhtmlExporter;
44
45 import org.apache.commons.lang.StringUtils;
46 import org.glom.libglom.Document;
47 import org.glom.libglom.Glom;
48 import org.glom.libglom.LayoutFieldVector;
49 import org.glom.libglom.LayoutGroup;
50 import org.glom.libglom.LayoutItemVector;
51 import org.glom.libglom.LayoutItem_Field;
52 import org.glom.libglom.LayoutItem_GroupBy;
53 import org.glom.libglom.Relationship;
54 import org.glom.libglom.Report;
55 import org.glom.libglom.SortClause;
56 import org.glom.libglom.SortFieldPair;
57 import org.glom.libglom.SqlBuilder;
58 import org.glom.libglom.SqlExpr;
59 import org.glom.libglom.Value;
60
61 /**
62  * @author Murray Cumming <murrayc@openimus.com>
63  * 
64  */
65 public class ReportGenerator {
66
67         final int height = 30; // Points, as specified later.
68         // An arbitrary width, because we must specify _some_ width:
69         final int width = 100; // Points, as specified later.
70
71         LayoutFieldVector fieldsToGet = new LayoutFieldVector();
72         SortClause sortClause = new SortClause();
73         String localeID;
74
75         final JasperDesign design = new JasperDesign();
76         JRDesignStyle normalStyle = new JRDesignStyle();
77         JRDesignStyle boldStyle = new JRDesignStyle();
78
79         ReportGenerator(final String localeID) {
80                 this.localeID = StringUtils.defaultString(localeID);
81         }
82
83         /**
84          */
85         public String generateReport(final Document document, final String tableName, final Report report,
86                         final Connection connection) {
87
88                 final org.glom.libglom.LayoutGroup layout_group = report.get_layout_group();
89
90                 design.setName(report.get_title(localeID)); // TODO: Actually, we want the title.
91
92                 normalStyle.setName("Sans_Normal");
93                 normalStyle.setDefault(true);
94                 normalStyle.setFontName("DejaVu Sans");
95                 normalStyle.setFontSize(12);
96                 boldStyle.setName("Sans_Bold");
97                 boldStyle.setFontName("DejaVu Sans");
98                 boldStyle.setFontSize(12);
99                 boldStyle.setBold(true);
100                 try {
101                         design.addStyle(normalStyle);
102                         design.addStyle(boldStyle);
103                 } catch (final JRException ex) {
104                         // TODO Auto-generated catch block
105                         ex.printStackTrace();
106                 }
107
108                 final JRDesignBand titleBand = new JRDesignBand();
109                 titleBand.setHeight(height);
110                 final JRDesignStaticText staticTitle = new JRDesignStaticText();
111                 staticTitle.setText(report.get_title(localeID));
112                 staticTitle.setY(0);
113                 staticTitle.setX(0);
114                 staticTitle.setWidth(width * 5); // No data will be shown without this.
115                 // staticTitle.setStretchWithOverflow(true);
116                 staticTitle.setHeight(height); // We must specify _some_ height.
117                 staticTitle.setStyle(boldStyle);
118                 titleBand.addElement(staticTitle);
119                 design.setTitle(titleBand);
120
121                 final JRDesignBand detailBand = new JRDesignBand();
122                 detailBand.setHeight(height + 20);
123
124                 final JRDesignBand headerBand = new JRDesignBand();
125                 headerBand.setHeight(height + 20);
126
127                 fieldsToGet = new LayoutFieldVector();
128                 final int x = 0;
129                 addToReport(layout_group, detailBand, headerBand, x);
130
131                 design.setColumnHeader(headerBand);
132                 ((JRDesignSection) design.getDetailSection()).addBand(detailBand);
133
134                 // Later versions of libglom actually return an empty SqlExpr when quickFindValue is empty,
135                 // but let's be sure:
136                 final String quickFind = ""; // TODO
137                 SqlExpr whereClause;
138                 if (StringUtils.isEmpty(quickFind)) {
139                         whereClause = new SqlExpr();
140                 } else {
141                         final Value quickFindValue = new Value(quickFind);
142                         whereClause = Glom.get_find_where_clause_quick(document, tableName, quickFindValue);
143                 }
144
145                 final Relationship extraJoin = new Relationship(); // Ignored.
146                 final SqlBuilder builder = Glom.build_sql_select_with_where_clause(tableName, fieldsToGet, whereClause,
147                                 extraJoin, sortClause);
148                 final String sqlQuery = Glom.sqlbuilder_get_full_query(builder);
149
150                 final JRDesignQuery query = new JRDesignQuery();
151                 query.setText(sqlQuery); // TODO: Extra sort clause to sort the rows within the groups.
152                 design.setQuery(query);
153
154                 JasperReport jasperreport;
155                 try {
156                         jasperreport = JasperCompileManager.compileReport(design);
157                 } catch (final JRException ex) {
158                         ex.printStackTrace();
159                         return "Failed to Generate HTML: compileReport() failed.";
160                 }
161
162                 JasperPrint print;
163                 try {
164                         final HashMap<String, Object> parameters = new HashMap<String, Object>();
165                         parameters.put("ReportTitle", report.get_title(localeID)); // TODO: Use the title, not the name.
166                         print = JasperFillManager.fillReport(jasperreport, parameters, connection);
167                 } catch (final JRException ex) {
168                         ex.printStackTrace();
169                         return "Failed to Generate HTML: fillReport() failed.";
170                 }
171
172                 final ByteArrayOutputStream output = new ByteArrayOutputStream();
173
174                 // We use this because there is no JasperExportManager.exportReportToHtmlStream() method.
175                 // JasperExportManager.exportReportToXmlStream(print, output);
176                 try {
177                         final JRXhtmlExporter exporter = new JRXhtmlExporter();
178                         exporter.setParameter(JRHtmlExporterParameter.JASPER_PRINT, print);
179                         exporter.setParameter(JRHtmlExporterParameter.OUTPUT_STREAM, output);
180
181                         // Use points instead of pixels for sizes, because pixels are silly
182                         // in HTML:
183                         exporter.setParameter(JRHtmlExporterParameter.SIZE_UNIT, "pt");
184
185                         exporter.exportReport();
186                 } catch (final JRException ex) {
187                         ex.printStackTrace();
188                         return "Failed to Generate HTML: exportReport() failed.";
189                 }
190
191                 // System.out.print(output.toString() + "\n");
192                 return output.toString();
193         }
194
195         /**
196          * @param layout_group
197          * @param parentBand
198          * @param x
199          * @param height
200          */
201         private int addToReport(final org.glom.libglom.LayoutGroup layout_group, final JRDesignBand parentBand,
202                         final JRDesignBand headerBand, int x) {
203
204                 /**
205                  * If this is a vertical group then we will layout the fields out vertically instead of horizontally.
206                  */
207                 /*
208                  * TODO: final org.glom.libglom.LayoutItem_VerticalGroup verticalGroup = LayoutItem_VerticalGroup
209                  * .cast_dynamic(layout_group); final boolean isVertical = (verticalGroup != null);
210                  */
211
212                 final LayoutItemVector layoutItemsVec = layout_group.get_items();
213                 final int numItems = Utils.safeLongToInt(layoutItemsVec.size());
214                 for (int i = 0; i < numItems; i++) {
215                         final org.glom.libglom.LayoutItem libglomLayoutItem = layoutItemsVec.get(i);
216
217                         final LayoutGroup libglomLayoutGroup = LayoutGroup.cast_dynamic(libglomLayoutItem);
218                         final LayoutItem_Field libglomLayoutItemField = LayoutItem_Field.cast_dynamic(libglomLayoutItem);
219                         if (libglomLayoutItemField != null) {
220                                 x = addFieldToDetailBand(parentBand, headerBand, x, libglomLayoutItemField);
221                         } else if (libglomLayoutGroup != null) {
222                                 final LayoutItem_GroupBy libglomGroupBy = LayoutItem_GroupBy.cast_dynamic(libglomLayoutGroup);
223                                 if (libglomGroupBy != null) {
224                                         final LayoutItem_Field fieldGroupBy = libglomGroupBy.get_field_group_by();
225                                         if (fieldGroupBy == null)
226                                                 continue;
227
228                                         final String fieldName = addField(fieldGroupBy);
229
230                                         // We must sort by the group field,
231                                         // so that JasperReports can start a new group when its value changes.
232                                         // Note that this is not like a SQL GROUP BY.
233                                         final SortFieldPair pair = new SortFieldPair();
234                                         pair.setFirst(fieldGroupBy);
235                                         pair.setSecond(true); // Ascending.
236                                         sortClause.add(pair);
237
238                                         final JRDesignGroup group = new JRDesignGroup();
239                                         group.setName(fieldName);
240
241                                         // Show the field value:
242                                         final JRDesignExpression expression = createFieldExpression(fieldName);
243                                         group.setExpression(expression);
244
245                                         try {
246                                                 design.addGroup(group);
247                                         } catch (final JRException e) {
248                                                 // TODO Auto-generated catch block
249                                                 e.printStackTrace();
250                                         }
251
252                                         // Show the group-by field:
253                                         final JRDesignBand groupBand = new JRDesignBand();
254                                         groupBand.setHeight(height);
255                                         ((JRDesignSection) group.getGroupHeaderSection()).addBand(groupBand);
256
257                                         /*
258                                          * final JRDesignBand footerBand = new JRDesignBand(); footerBand.setHeight(height);
259                                          * ((JRDesignSection) group.getGroupFooterSection()).addBand(footerBand);
260                                          */
261
262                                         int groupX = addFieldToGroupBand(groupBand, x, fieldGroupBy);
263
264                                         // Show the secondary fields:
265                                         final LayoutGroup groupSecondaries = libglomGroupBy.get_group_secondary_fields();
266                                         if (groupSecondaries != null)
267                                                 groupX = addSecondaryFieldsToGroupBand(groupSecondaries, groupBand, groupX);
268
269                                         final JRDesignLine line = new JRDesignLine();
270                                         final int lineheight = 1;
271                                         line.setX(0);
272
273                                         // TODO: Automatically place it below the text, though that needs us to know how much height the
274                                         // text really needs.
275                                         line.setY(height - lineheight - 10);
276
277                                         // TODO: Make it as wide as needed by the details band.
278                                         line.setWidth(groupX);
279                                         line.setHeight(lineheight);
280                                         groupBand.addElement(line);
281                                 }
282
283                                 // Recurse into sub-groups:
284                                 x = addToReport(libglomLayoutGroup, parentBand, headerBand, x);
285                         }
286                 }
287
288                 return x;
289         }
290
291         private int addSecondaryFieldsToGroupBand(final org.glom.libglom.LayoutGroup layout_group,
292                         final JRDesignBand groupBand, int x) {
293                 final LayoutItemVector layoutItemsVec = layout_group.get_items();
294                 final int numItems = Utils.safeLongToInt(layoutItemsVec.size());
295                 for (int i = 0; i < numItems; i++) {
296                         final org.glom.libglom.LayoutItem libglomLayoutItem = layoutItemsVec.get(i);
297
298                         final LayoutGroup libglomLayoutGroup = LayoutGroup.cast_dynamic(libglomLayoutItem);
299                         final LayoutItem_Field libglomLayoutItemField = LayoutItem_Field.cast_dynamic(libglomLayoutItem);
300                         if (libglomLayoutItemField != null) {
301                                 x = addFieldToGroupBand(groupBand, x, libglomLayoutItemField);
302                         } else if (libglomLayoutGroup != null) {
303                                 // We do not expect LayoutItem_GroupBy in the secondary fields:
304                                 // final LayoutItem_GroupBy libglomGroupBy = LayoutItem_GroupBy.cast_dynamic(libglomLayoutGroup);
305
306                                 // Recurse into sub-groups:
307                                 x = addSecondaryFieldsToGroupBand(libglomLayoutGroup, groupBand, x);
308                         }
309                 }
310
311                 return x;
312         }
313
314         /**
315          * @param fieldName
316          * @return
317          */
318         private JRDesignExpression createFieldExpression(final String fieldName) {
319                 final JRDesignExpression expression = new JRDesignExpression();
320
321                 // TODO: Where is this format documented?
322                 expression.setText("$F{" + fieldName + "}");
323                 return expression;
324         }
325
326         /**
327          * @param parentBand
328          * @param x
329          * @param libglomLayoutItemField
330          * @return
331          */
332         private int addFieldToDetailBand(final JRDesignBand parentBand, final JRDesignBand headerBand, int x,
333                         final LayoutItem_Field libglomLayoutItemField) {
334                 final String fieldName = addField(libglomLayoutItemField);
335
336                 // Show the field title:
337                 final JRDesignStaticText textFieldColumn = createFieldTitleElement(x, libglomLayoutItemField, false);
338                 textFieldColumn.setStyle(boldStyle);
339                 headerBand.addElement(textFieldColumn);
340
341                 // Show an instance of the field (the field value):
342                 final JRDesignTextField textField = createFieldValueElement(x, fieldName);
343                 textField.setStyle(normalStyle);
344                 parentBand.addElement(textField);
345
346                 x += width;
347                 return x;
348         }
349
350         private int addFieldToGroupBand(final JRDesignBand parentBand, int x, final LayoutItem_Field libglomLayoutItemField) {
351                 final String fieldName = addField(libglomLayoutItemField);
352
353                 // Show the field title:
354                 final JRDesignStaticText textFieldColumn = createFieldTitleElement(x, libglomLayoutItemField, true);
355
356                 // Instead, the field value will be bold, because here it is like a title.
357                 textFieldColumn.setStyle(normalStyle);
358
359                 parentBand.addElement(textFieldColumn);
360                 x += width;
361
362                 // Show an instance of the field (the field value):
363                 final JRDesignTextField textField = createFieldValueElement(x, fieldName);
364                 parentBand.addElement(textField);
365                 textField.setStyle(boldStyle);
366
367                 x += width;
368                 return x;
369         }
370
371         /**
372          * @param x
373          * @param fieldName
374          * @return
375          */
376         private JRDesignTextField createFieldValueElement(final int x, final String fieldName) {
377                 final JRDesignTextField textField = new JRDesignTextField();
378
379                 // Make sure this field starts at the right of the previous field,
380                 // because JasperReports uses absolute positioning.
381                 textField.setY(0);
382                 textField.setX(x);
383                 textField.setWidth(width); // No data will be shown without this.
384
385                 // This only stretches vertically, but that is better than
386                 // nothing.
387                 textField.setStretchWithOverflow(true);
388                 textField.setHeight(height); // We must specify _some_ height.
389
390                 final JRDesignExpression expression = createFieldExpression(fieldName);
391
392                 textField.setExpression(expression);
393                 return textField;
394         }
395
396         /**
397          * @param x
398          * @param libglomLayoutItemField
399          * @return
400          */
401         private JRDesignStaticText createFieldTitleElement(final int x, final LayoutItem_Field libglomLayoutItemField,
402                         final boolean withColon) {
403                 final JRDesignStaticText textFieldColumn = new JRDesignStaticText();
404
405                 String title = StringUtils.defaultString(libglomLayoutItemField.get_title(this.localeID));
406
407                 // If the title is at the left, instead of above, we need a : to show that it's a title.
408                 if (withColon)
409                         title += ":";
410
411                 textFieldColumn.setText(title);
412                 textFieldColumn.setY(0);
413                 textFieldColumn.setX(x);
414                 textFieldColumn.setWidth(width); // No data will be shown without this.
415                 // textFieldColumn.setStretchWithOverflow(true);
416                 textFieldColumn.setHeight(height); // We must specify _some_ height.
417                 return textFieldColumn;
418         }
419
420         /**
421          * @param libglomLayoutItemField
422          * @return
423          */
424         private String addField(final LayoutItem_Field libglomLayoutItemField) {
425                 fieldsToGet.add(libglomLayoutItemField);
426
427                 final String fieldName = libglomLayoutItemField.get_name();
428                 // System.out.print("fieldName=" + fieldName + "\n");
429
430                 // Tell the JasperDesign about the database field that will be in the SQL query,
431                 // specified later:
432                 final JRDesignField field = new JRDesignField();
433                 field.setName(fieldName); // TODO: Related fields.
434                 field.setValueClass(getClassTypeForGlomType(libglomLayoutItemField));
435
436                 try {
437                         design.addField(field);
438                 } catch (final JRException e2) {
439                         // TODO Auto-generated catch block
440                         e2.printStackTrace();
441                 }
442                 return fieldName;
443         }
444
445         /**
446          * @param libglomLayoutItemField
447          * @return
448          */
449         private Class<?> getClassTypeForGlomType(final LayoutItem_Field libglomLayoutItemField) {
450                 // Choose a suitable java class type for the SQL field:
451                 Class<?> klass = null;
452                 switch (libglomLayoutItemField.get_glom_type()) {
453                 case TYPE_TEXT:
454                         klass = java.lang.String.class;
455                         break;
456                 case TYPE_BOOLEAN:
457                         klass = java.lang.Boolean.class;
458                         break;
459                 case TYPE_NUMERIC:
460                         klass = java.lang.Double.class;
461                         break;
462                 case TYPE_DATE:
463                         klass = java.util.Date.class;
464                         break;
465                 case TYPE_TIME:
466                         klass = java.sql.Time.class;
467                         break;
468                 case TYPE_IMAGE:
469                         klass = java.sql.Blob.class; // TODO: This does not work.
470                         break;
471                 }
472                 return klass;
473         }
474 }