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