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