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