2 * Copyright (C) 2012 Openismus GmbH
4 * This file is part of GWT-Glom.
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.
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
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/>.
20 package org.glom.web.server;
22 import java.io.ByteArrayOutputStream;
23 import java.sql.Connection;
24 import java.text.DateFormat;
25 import java.text.DecimalFormat;
26 import java.text.SimpleDateFormat;
27 import java.util.ArrayList;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Locale;
32 import net.sf.jasperreports.engine.JRException;
33 import net.sf.jasperreports.engine.JRExporterParameter;
34 import net.sf.jasperreports.engine.JasperCompileManager;
35 import net.sf.jasperreports.engine.JasperFillManager;
36 import net.sf.jasperreports.engine.JasperPrint;
37 import net.sf.jasperreports.engine.JasperReport;
38 import net.sf.jasperreports.engine.design.JRDesignBand;
39 import net.sf.jasperreports.engine.design.JRDesignExpression;
40 import net.sf.jasperreports.engine.design.JRDesignField;
41 import net.sf.jasperreports.engine.design.JRDesignGroup;
42 import net.sf.jasperreports.engine.design.JRDesignLine;
43 import net.sf.jasperreports.engine.design.JRDesignQuery;
44 import net.sf.jasperreports.engine.design.JRDesignSection;
45 import net.sf.jasperreports.engine.design.JRDesignStaticText;
46 import net.sf.jasperreports.engine.design.JRDesignStyle;
47 import net.sf.jasperreports.engine.design.JRDesignTextField;
48 import net.sf.jasperreports.engine.design.JasperDesign;
49 import net.sf.jasperreports.engine.export.JRHtmlExporterParameter;
50 import net.sf.jasperreports.engine.export.JRXhtmlExporter;
52 import org.apache.commons.lang3.StringUtils;
53 import org.glom.web.shared.TypedDataItem;
54 import org.glom.web.shared.libglom.Document;
55 import org.glom.web.shared.libglom.Field.GlomFieldType;
56 import org.glom.web.shared.libglom.NumericFormat;
57 import org.glom.web.shared.libglom.Report;
58 import org.glom.web.shared.libglom.layout.Formatting;
59 import org.glom.web.shared.libglom.layout.LayoutGroup;
60 import org.glom.web.shared.libglom.layout.LayoutItem;
61 import org.glom.web.shared.libglom.layout.LayoutItemField;
62 import org.glom.web.shared.libglom.layout.SortClause;
63 import org.glom.web.shared.libglom.layout.UsesRelationship;
64 import org.glom.web.shared.libglom.layout.reportparts.LayoutItemGroupBy;
65 import org.glom.web.shared.libglom.layout.reportparts.LayoutItemVerticalGroup;
66 import org.jooq.Condition;
69 * @author Murray Cumming <murrayc@openimus.com>
72 public class ReportGenerator {
74 private class Position {
75 public Position(final int x, final int y) {
80 public Position(final Position pos) {
89 final int height = 30; // Points, as specified later.
90 // An arbitrary width, because we must specify _some_ width:
91 final int width = 100; // Points, as specified later.
93 List<LayoutItemField> fieldsToGet = new ArrayList<LayoutItemField>();
94 SortClause sortClause = new SortClause();
97 final JasperDesign design = new JasperDesign();
98 JRDesignStyle titleStyle = new JRDesignStyle();
99 JRDesignStyle normalStyle = new JRDesignStyle();
100 JRDesignStyle fieldTitleStyle = new JRDesignStyle();
102 ReportGenerator(final String localeID) {
103 this.localeID = StringUtils.defaultString(localeID);
108 public String generateReport(final Document glomDocument, final String tableName, final Report report,
109 final Connection connection, final String quickFind) {
111 final LayoutGroup layout_group = report.get_layout_group();
113 design.setName(report.get_title(localeID)); // TODO: Actually, we want the title.
115 titleStyle.setName("Sans_Title");
116 titleStyle.setFontName("DejaVu Sans");
117 titleStyle.setFontSize(24);
118 normalStyle.setName("Sans_Normal");
119 normalStyle.setDefault(true);
120 normalStyle.setFontName("DejaVu Sans");
121 normalStyle.setFontSize(12);
122 normalStyle.setBlankWhenNull(true); // Avoid "null" appearing in reports.
123 fieldTitleStyle.setName("Sans_Bold");
124 fieldTitleStyle.setFontName("DejaVu Sans");
125 fieldTitleStyle.setFontSize(12);
126 fieldTitleStyle.setBold(true);
127 fieldTitleStyle.setBlankWhenNull(true); // Avoid "null" appearing in reports when this is used for a GroupBy
130 design.addStyle(titleStyle);
131 design.addStyle(normalStyle);
132 design.addStyle(fieldTitleStyle);
133 } catch (final JRException ex) {
134 // TODO Auto-generated catch block
135 ex.printStackTrace();
138 final JRDesignBand titleBand = new JRDesignBand();
139 titleBand.setHeight(height);
140 final JRDesignStaticText staticTitle = new JRDesignStaticText();
141 staticTitle.setText(report.get_title(localeID));
144 staticTitle.setWidth(width * 5); // No data will be shown without this.
145 // staticTitle.setStretchWithOverflow(true);
146 staticTitle.setHeight(height); // We must specify _some_ height.
147 staticTitle.setStyle(titleStyle);
148 titleBand.addElement(staticTitle);
149 design.setTitle(titleBand);
151 final JRDesignBand detailBand = new JRDesignBand();
152 detailBand.setHeight(height + 20);
154 final JRDesignBand headerBand = new JRDesignBand();
155 headerBand.setHeight(height + 20);
157 fieldsToGet = new ArrayList<LayoutItemField>();
159 addGroupToReport(layout_group, detailBand, x, headerBand, 0);
161 design.setColumnHeader(headerBand);
162 ((JRDesignSection) design.getDetailSection()).addBand(detailBand);
164 // Later versions of libglom actually return an empty SqlExpr when quickFindValue is empty,
165 // but let's be sure:
166 Condition whereClause = null;
167 if (!StringUtils.isEmpty(quickFind)) {
168 final TypedDataItem quickFindValue = new TypedDataItem();
169 quickFindValue.setText(quickFind);
170 whereClause = SqlUtils.get_find_where_clause_quick(glomDocument, tableName, quickFindValue);
173 String sqlQuery = "";
174 if (!fieldsToGet.isEmpty()) {
175 sqlQuery = SqlUtils.build_sql_select_with_where_clause(connection, tableName, fieldsToGet, whereClause,
178 Log.info("generateReport(): fieldsToGet is empty.");
181 final JRDesignQuery query = new JRDesignQuery();
182 query.setText(sqlQuery); // TODO: Extra sort clause to sort the rows within the groups.
183 design.setQuery(query);
185 JasperReport jasperreport;
187 jasperreport = JasperCompileManager.compileReport(design);
188 } catch (final JRException ex) {
189 ex.printStackTrace();
190 return "Failed to Generate HTML: compileReport() failed.";
195 final HashMap<String, Object> parameters = new HashMap<String, Object>();
196 parameters.put("ReportTitle", report.get_title(localeID)); // TODO: Use the title, not the name.
197 print = JasperFillManager.fillReport(jasperreport, parameters, connection);
198 } catch (final JRException ex) {
199 ex.printStackTrace();
200 return "Failed to Generate HTML: fillReport() failed.";
203 final ByteArrayOutputStream output = new ByteArrayOutputStream();
205 // We use this because there is no JasperExportManager.exportReportToHtmlStream() method.
206 // JasperExportManager.exportReportToXmlStream(print, output);
208 final JRXhtmlExporter exporter = new JRXhtmlExporter();
209 exporter.setParameter(JRExporterParameter.JASPER_PRINT, print);
210 exporter.setParameter(JRExporterParameter.OUTPUT_STREAM, output);
212 // Use points instead of pixels for sizes, because pixels are silly
214 exporter.setParameter(JRHtmlExporterParameter.SIZE_UNIT, "pt");
216 // Avoid the page breaks, because they do not make sense for HTML:
217 // TODO: These do not seem to preven the page break gap.
218 exporter.setParameter(JRHtmlExporterParameter.IS_REMOVE_EMPTY_SPACE_BETWEEN_ROWS, Boolean.TRUE);
219 exporter.setParameter(JRHtmlExporterParameter.BETWEEN_PAGES_HTML, "");
221 exporter.exportReport();
222 } catch (final JRException ex) {
223 ex.printStackTrace();
224 return "Failed to Generate HTML: exportReport() failed.";
227 // System.out.print(output.toString() + "\n");
228 final String html = output.toString();
230 // This does not work because jasperReports does not put individual rows in separate table rows.
231 // jasperReports just puts the whole thing in one table row.
232 // Remove the arbitrary width and height that JasperReports forced us to specify:
233 // html = html.replaceAll("position:absolute;", "");
234 // html = html.replaceAll("top:(\\d*)pt;", "");
235 // html = html.replaceAll("left:(\\d*)pt;", "");
236 // html = html.replaceAll("height:(\\d*)pt;", "");
237 // html = html.replaceAll("width:(\\d*)pt;", "");
238 // html = html.replaceAll("overflow: hidden;", "");
244 * A vertical group lays the fields out vertically instead of horizontally, with titles to the left.
246 * @param layout_group
249 * @param fieldTitlesY
253 private Position addVerticalGroupToReport(final LayoutItemVerticalGroup layout_group,
254 final JRDesignBand parentBand, final Position pos) {
255 Position pos_result = new Position(pos);
257 final List<LayoutItem> layoutItemsVec = layout_group.get_items();
258 final int numItems = Utils.safeLongToInt(layoutItemsVec.size());
259 for (int i = 0; i < numItems; i++) {
260 final LayoutItem libglomLayoutItem = layoutItemsVec.get(i);
262 if (libglomLayoutItem instanceof LayoutItemField) {
263 final LayoutItemField libglomLayoutItemField = (LayoutItemField) libglomLayoutItem;
264 pos_result = addFieldToDetailBandVertical(parentBand, pos_result, libglomLayoutItemField);
265 pos_result.x = pos.x;
268 // TODO: Handle other item types.
270 // Recurse into sub-groups:
271 // TODO: x = addGroupToReport(libglomLayoutGroup, parentBand, x, fieldTitlesBand, thisFieldTitlesY);
275 pos_result.x += width * 2;
280 * @param layout_group
283 * @param fieldTitlesY
287 private Position addGroupToReport(final LayoutGroup layout_group, final JRDesignBand parentBand, final int x,
288 final JRDesignBand headerBand, final int fieldTitlesY) {
290 Position pos_result = new Position(x, 0);
293 * * If this is a vertical group then we will lay the fields out vertically instead of horizontally.
295 if (layout_group instanceof LayoutItemVerticalGroup) {
296 final LayoutItemVerticalGroup verticalGroup = (LayoutItemVerticalGroup) layout_group;
297 return addVerticalGroupToReport(verticalGroup, parentBand, pos_result);
300 // Where we put the field titles depends on whether we are in a group-by:
301 JRDesignBand fieldTitlesBand = headerBand;
302 int thisFieldTitlesY = fieldTitlesY; // If they are in a group title then they must be lower.
304 final List<LayoutItem> layoutItemsVec = layout_group.get_items();
305 final int numItems = Utils.safeLongToInt(layoutItemsVec.size());
306 for (int i = 0; i < numItems; i++) {
307 final LayoutItem libglomLayoutItem = layoutItemsVec.get(i);
309 if (libglomLayoutItem instanceof LayoutItemField) {
310 final LayoutItemField libglomLayoutItemField = (LayoutItemField) libglomLayoutItem;
311 pos_result = addFieldToDetailBand(parentBand, headerBand, pos_result.x, libglomLayoutItemField,
312 thisFieldTitlesY, pos_result.y);
313 } else if (libglomLayoutItem instanceof LayoutGroup) {
314 final LayoutGroup libglomLayoutGroup = (LayoutGroup) libglomLayoutItem;
316 if (libglomLayoutGroup instanceof LayoutItemGroupBy) {
317 final LayoutItemGroupBy libglomGroupBy = (LayoutItemGroupBy) libglomLayoutGroup;
318 final LayoutItemField fieldGroupBy = libglomGroupBy.get_field_group_by();
319 if (fieldGroupBy == null)
322 final String fieldName = addField(fieldGroupBy);
323 if (StringUtils.isEmpty(fieldName)) {
327 // We must sort by the group field,
328 // so that JasperReports can start a new group when its value changes.
329 // Note that this is not like a SQL GROUP BY.
330 final SortClause.SortField pair = new SortClause.SortField();
331 pair.field = fieldGroupBy;
332 pair.ascending = true;
333 sortClause.add(pair);
335 final JRDesignGroup group = new JRDesignGroup();
336 group.setName(fieldName);
338 // Show the field value:
339 final JRDesignExpression expression = createFieldExpression(fieldGroupBy);
340 group.setExpression(expression);
343 design.addGroup(group);
344 } catch (final JRException e) {
345 // TODO Auto-generated catch block
349 // Show the group-by field:
350 final JRDesignBand groupBand = new JRDesignBand();
352 // TODO: Use height instead of height*2 if there are no child fields,
353 // for instance if the only child is a sub group-by.
354 groupBand.setHeight(height * 2); // Enough height for the title and the field titles.
355 ((JRDesignSection) group.getGroupHeaderSection()).addBand(groupBand);
357 // Put the field titles inside the group-by instead of just at the top of the page.
358 // (or instead of just in the parent group-by):
359 fieldTitlesBand = groupBand;
360 thisFieldTitlesY = height;
363 * final JRDesignBand footerBand = new JRDesignBand(); footerBand.setHeight(height);
364 * ((JRDesignSection) group.getGroupFooterSection()).addBand(footerBand);
367 int groupX = addFieldToGroupBand(groupBand, x, fieldGroupBy);
369 // Show the secondary fields:
370 final LayoutGroup groupSecondaries = libglomGroupBy.get_secondary_fields();
371 if (groupSecondaries != null)
372 groupX = addSecondaryFieldsToGroupBand(groupSecondaries, groupBand, groupX);
374 final JRDesignLine line = new JRDesignLine();
375 final int lineheight = 1;
378 // TODO: Automatically place it below the text, though that needs us to know how much height the
379 // text really needs.
380 line.setY(height - 15);
382 // TODO: Make it as wide as needed by the details band.
383 line.setWidth(groupX);
384 line.setHeight(lineheight);
385 groupBand.addElement(line);
388 // Recurse into sub-groups:
389 pos_result = addGroupToReport(libglomLayoutGroup, parentBand, pos_result.x, fieldTitlesBand,
397 private int addSecondaryFieldsToGroupBand(final LayoutGroup layout_group, final JRDesignBand groupBand, int x) {
398 final List<LayoutItem> layoutItemsVec = layout_group.get_items();
399 final int numItems = Utils.safeLongToInt(layoutItemsVec.size());
400 for (int i = 0; i < numItems; i++) {
401 final LayoutItem libglomLayoutItem = layoutItemsVec.get(i);
403 if (libglomLayoutItem instanceof LayoutItemField) {
404 final LayoutItemField libglomLayoutItemField = (LayoutItemField) libglomLayoutItem;
405 x = addFieldToGroupBand(groupBand, x, libglomLayoutItemField);
406 } else if (libglomLayoutItem instanceof LayoutGroup) {
407 final LayoutGroup libglomLayoutGroup = (LayoutGroup) libglomLayoutItem;
409 // We do not expect LayoutItem_GroupBy in the secondary fields:
410 // final LayoutItem_GroupBy libglomGroupBy = LayoutItem_GroupBy.cast_dynamic(libglomLayoutGroup);
412 // Recurse into sub-groups:
413 x = addSecondaryFieldsToGroupBand(libglomLayoutGroup, groupBand, x);
421 * @param libglomLayoutItemField
424 private JRDesignExpression createFieldExpression(final LayoutItemField libglomLayoutItemField) {
425 final JRDesignExpression expression = new JRDesignExpression();
427 final String fieldName = libglomLayoutItemField.get_name(); // TODO: Is this enough for related fields?
429 // TODO: Where is this format documented?
430 expression.setText("$F{" + fieldName + "}");
437 * @param libglomLayoutItemField
438 * @param fieldTitlesY
442 private Position addFieldToDetailBand(final JRDesignBand parentBand, final JRDesignBand headerBand, final int x,
443 final LayoutItemField libglomLayoutItemField, final int fieldTitlesY, final int fieldY) {
444 addField(libglomLayoutItemField);
446 // Show the field title:
447 final JRDesignStaticText textFieldColumn = createFieldTitleElement(new Position(x, fieldTitlesY),
448 libglomLayoutItemField, false);
449 textFieldColumn.setStyle(fieldTitleStyle);
450 headerBand.addElement(textFieldColumn);
452 // Show an instance of the field (the field value):
453 final JRDesignTextField textField = createFieldValueElement(new Position(x, 0), libglomLayoutItemField);
454 textField.setStyle(normalStyle);
455 parentBand.addElement(textField);
457 return new Position(x + width, 0);
463 * @param libglomLayoutItemField
464 * @param fieldTitlesY
468 private Position addFieldToDetailBandVertical(final JRDesignBand parentBand, final Position pos,
469 final LayoutItemField libglomLayoutItemField) {
470 addField(libglomLayoutItemField);
472 final Position pos_result = new Position(pos);
474 // Make the band high enough if necessary:
475 if (parentBand.getHeight() < (pos_result.y + height))
476 parentBand.setHeight(pos_result.y + height + 20);
478 // Show the field title:
479 final JRDesignStaticText textFieldColumn = createFieldTitleElement(pos_result, libglomLayoutItemField, true);
480 textFieldColumn.setStyle(fieldTitleStyle);
481 parentBand.addElement(textFieldColumn);
482 pos_result.x += width;
484 // Show an instance of the field (the field value):
485 final JRDesignTextField textField = createFieldValueElement(pos_result, libglomLayoutItemField);
486 textField.setStyle(normalStyle);
487 parentBand.addElement(textField);
489 pos_result.x += width;
491 pos_result.y += height;
496 private int addFieldToGroupBand(final JRDesignBand parentBand, final int x,
497 final LayoutItemField libglomLayoutItemField) {
498 addField(libglomLayoutItemField);
500 final Position pos = new Position(x, 0);
502 // Show the field title:
503 final JRDesignStaticText textFieldColumn = createFieldTitleElement(pos, libglomLayoutItemField, true);
505 // Instead, the field value will be bold, because here it is like a title.
506 textFieldColumn.setStyle(normalStyle);
508 parentBand.addElement(textFieldColumn);
511 // Show an instance of the field (the field value):
512 final JRDesignTextField textField = createFieldValueElement(pos, libglomLayoutItemField);
513 parentBand.addElement(textField);
514 textField.setStyle(fieldTitleStyle);
522 * @param libglomLayoutItemField
525 private JRDesignTextField createFieldValueElement(final Position pos, final LayoutItemField libglomLayoutItemField) {
526 final JRDesignTextField textField = new JRDesignTextField();
528 // Make sure this field starts at the right of the previous field,
529 // because JasperReports uses absolute positioning.
530 textField.setY(pos.y);
531 textField.setX(pos.x);
532 textField.setWidth(width); // No data will be shown without this.
534 // This only stretches vertically, but that is better than
536 textField.setStretchWithOverflow(true);
537 textField.setHeight(height); // We must specify _some_ height.
539 final JRDesignExpression expression = createFieldExpression(libglomLayoutItemField);
540 textField.setExpression(expression);
542 if (libglomLayoutItemField.get_glom_type() == GlomFieldType.TYPE_NUMERIC) {
543 // Numeric formatting:
544 final Formatting formatting = libglomLayoutItemField.get_formatting_used();
545 final NumericFormat numericFormat = formatting.getNumericFormat();
547 final DecimalFormat format = new DecimalFormat();
548 format.setMaximumFractionDigits(numericFormat.getDecimalPlaces());
549 format.setGroupingUsed(numericFormat.getUseThousandsSeparator());
551 // TODO: Use numericFormat.get_currency_symbol(), possibly via format.setCurrency().
552 textField.setPattern(format.toPattern());
553 } else if (libglomLayoutItemField.get_glom_type() == GlomFieldType.TYPE_DATE) {
555 // TODO: Use a 4-digit-year short form, somehow.
556 try // We use a try block because getDateInstance() is not guaranteed to return a SimpleDateFormat.
558 final SimpleDateFormat format = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT,
561 textField.setPattern(format.toPattern());
562 } catch (final Exception ex) {
563 Log.info("ReportGenerator: The cast of SimpleDateFormat failed.");
565 } else if (libglomLayoutItemField.get_glom_type() == GlomFieldType.TYPE_TIME) {
567 try // We use a try block because getDateInstance() is not guaranteed to return a SimpleDateFormat.
569 final SimpleDateFormat format = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.SHORT,
572 textField.setPattern(format.toPattern());
573 } catch (final Exception ex) {
574 Log.info("ReportGenerator: The cast of SimpleDateFormat failed.");
585 * @param libglomLayoutItemField
588 private JRDesignStaticText createFieldTitleElement(final Position pos,
589 final LayoutItemField libglomLayoutItemField, final boolean withColon) {
590 final JRDesignStaticText textFieldColumn = new JRDesignStaticText();
592 String title = StringUtils.defaultString(libglomLayoutItemField.get_title(this.localeID));
594 // If the title is at the left, instead of above, we need a : to show that it's a title.
598 textFieldColumn.setText(title);
599 textFieldColumn.setY(pos.y);
600 textFieldColumn.setX(pos.x);
601 textFieldColumn.setWidth(width); // No data will be shown without this.
602 // textFieldColumn.setStretchWithOverflow(true);
603 textFieldColumn.setHeight(height); // We must specify _some_ height.
604 return textFieldColumn;
608 * @param libglomLayoutItemField
611 private String addField(final LayoutItemField libglomLayoutItemField) {
613 final String fieldName = libglomLayoutItemField.get_name(); // TODO: Is this enough for related fields?
615 // Avoid an unnamed field:
616 if (StringUtils.isEmpty(fieldName)) {
617 Log.info("addField(): Ignoring LayoutItemField with no field name");
621 // Avoid adding duplicate fields,
622 // because JasperDesign.addField() throws a "Duplicate declaration of field" exception.
623 for (int i = 0; i < fieldsToGet.size(); ++i) {
624 final UsesRelationship thisField = fieldsToGet.get(i);
625 if (thisField.equals(libglomLayoutItemField))
629 fieldsToGet.add(libglomLayoutItemField);
631 // System.out.print("fieldName=" + fieldName + "\n");
633 // Tell the JasperDesign about the database field that will be in the SQL query,
635 final JRDesignField field = new JRDesignField();
636 field.setName(fieldName); // TODO: Related fields.
638 final Class<?> klass = getClassTypeForGlomType(libglomLayoutItemField);
640 field.setValueClass(klass);
642 Log.info("getClassTypeForGlomType() returned null");
646 design.addField(field);
647 } catch (final JRException e2) {
648 // TODO Auto-generated catch block
649 e2.printStackTrace();
656 * @param libglomLayoutItemField
659 private Class<?> getClassTypeForGlomType(final LayoutItemField libglomLayoutItemField) {
660 // Choose a suitable java class type for the SQL field:
661 Class<?> klass = null;
663 final GlomFieldType glom_type = libglomLayoutItemField.get_glom_type();
666 klass = java.lang.String.class;
669 klass = java.lang.Boolean.class;
672 klass = java.lang.Double.class;
675 klass = java.util.Date.class;
678 klass = java.sql.Time.class;
681 klass = java.sql.Blob.class; // TODO: This does not work.
684 Log.info("getClassTypeForGlomType() returning null for TYPE_INVALID glom type. Field name="
685 + libglomLayoutItemField.get_layout_display_name());
687 Log.info("getClassTypeForGlomType() returning null for glom type: " + glom_type + ". Field name="
688 + libglomLayoutItemField.get_layout_display_name());