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.util.HashMap;
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;
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;
62 * @author Murray Cumming <murrayc@openimus.com>
65 public class ReportGenerator {
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.
71 LayoutFieldVector fieldsToGet = new LayoutFieldVector();
72 SortClause sortClause = new SortClause();
75 final JasperDesign design = new JasperDesign();
76 JRDesignStyle titleStyle = new JRDesignStyle();
77 JRDesignStyle normalStyle = new JRDesignStyle();
78 JRDesignStyle boldStyle = new JRDesignStyle();
80 ReportGenerator(final String localeID) {
81 this.localeID = StringUtils.defaultString(localeID);
86 public String generateReport(final Document document, final String tableName, final Report report,
87 final Connection connection) {
89 final org.glom.libglom.LayoutGroup layout_group = report.get_layout_group();
91 design.setName(report.get_title(localeID)); // TODO: Actually, we want the title.
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);
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();
114 final JRDesignBand titleBand = new JRDesignBand();
115 titleBand.setHeight(height);
116 final JRDesignStaticText staticTitle = new JRDesignStaticText();
117 staticTitle.setText(report.get_title(localeID));
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);
127 final JRDesignBand detailBand = new JRDesignBand();
128 detailBand.setHeight(height + 20);
130 final JRDesignBand headerBand = new JRDesignBand();
131 headerBand.setHeight(height + 20);
133 fieldsToGet = new LayoutFieldVector();
135 addToReport(layout_group, detailBand, x, headerBand, 0);
137 design.setColumnHeader(headerBand);
138 ((JRDesignSection) design.getDetailSection()).addBand(detailBand);
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
144 if (StringUtils.isEmpty(quickFind)) {
145 whereClause = new SqlExpr();
147 final Value quickFindValue = new Value(quickFind);
148 whereClause = Glom.get_find_where_clause_quick(document, tableName, quickFindValue);
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);
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);
160 JasperReport jasperreport;
162 jasperreport = JasperCompileManager.compileReport(design);
163 } catch (final JRException ex) {
164 ex.printStackTrace();
165 return "Failed to Generate HTML: compileReport() failed.";
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.";
178 final ByteArrayOutputStream output = new ByteArrayOutputStream();
180 // We use this because there is no JasperExportManager.exportReportToHtmlStream() method.
181 // JasperExportManager.exportReportToXmlStream(print, output);
183 final JRXhtmlExporter exporter = new JRXhtmlExporter();
184 exporter.setParameter(JRHtmlExporterParameter.JASPER_PRINT, print);
185 exporter.setParameter(JRHtmlExporterParameter.OUTPUT_STREAM, output);
187 // Use points instead of pixels for sizes, because pixels are silly
189 exporter.setParameter(JRHtmlExporterParameter.SIZE_UNIT, "pt");
191 exporter.exportReport();
192 } catch (final JRException ex) {
193 ex.printStackTrace();
194 return "Failed to Generate HTML: exportReport() failed.";
197 // System.out.print(output.toString() + "\n");
198 return output.toString();
202 * @param layout_group
205 * @param fieldTitlesY
209 private int addToReport(final org.glom.libglom.LayoutGroup layout_group, final JRDesignBand parentBand, int x,
210 final JRDesignBand headerBand, final int fieldTitlesY) {
213 * If this is a vertical group then we will layout the fields out vertically instead of horizontally.
216 * TODO: final org.glom.libglom.LayoutItem_VerticalGroup verticalGroup = LayoutItem_VerticalGroup
217 * .cast_dynamic(layout_group); final boolean isVertical = (verticalGroup != null);
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.
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);
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)
240 final String fieldName = addField(fieldGroupBy);
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);
250 final JRDesignGroup group = new JRDesignGroup();
251 group.setName(fieldName);
253 // Show the field value:
254 final JRDesignExpression expression = createFieldExpression(fieldName);
255 group.setExpression(expression);
258 design.addGroup(group);
259 } catch (final JRException e) {
260 // TODO Auto-generated catch block
264 // Show the group-by field:
265 final JRDesignBand groupBand = new JRDesignBand();
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);
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;
278 * final JRDesignBand footerBand = new JRDesignBand(); footerBand.setHeight(height);
279 * ((JRDesignSection) group.getGroupFooterSection()).addBand(footerBand);
282 int groupX = addFieldToGroupBand(groupBand, x, fieldGroupBy);
284 // Show the secondary fields:
285 final LayoutGroup groupSecondaries = libglomGroupBy.get_secondary_fields();
286 if (groupSecondaries != null)
287 groupX = addSecondaryFieldsToGroupBand(groupSecondaries, groupBand, groupX);
289 final JRDesignLine line = new JRDesignLine();
290 final int lineheight = 1;
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);
297 // TODO: Make it as wide as needed by the details band.
298 line.setWidth(groupX);
299 line.setHeight(lineheight);
300 groupBand.addElement(line);
303 // Recurse into sub-groups:
304 x = addToReport(libglomLayoutGroup, parentBand, x, fieldTitlesBand, thisFieldTitlesY);
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);
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);
326 // Recurse into sub-groups:
327 x = addSecondaryFieldsToGroupBand(libglomLayoutGroup, groupBand, x);
338 private JRDesignExpression createFieldExpression(final String fieldName) {
339 final JRDesignExpression expression = new JRDesignExpression();
341 // TODO: Where is this format documented?
342 expression.setText("$F{" + fieldName + "}");
349 * @param libglomLayoutItemField
350 * @param fieldTitlesY
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);
358 // Show the field title:
359 final JRDesignStaticText textFieldColumn = createFieldTitleElement(x, fieldTitlesY, libglomLayoutItemField,
361 textFieldColumn.setStyle(boldStyle);
362 headerBand.addElement(textFieldColumn);
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);
373 private int addFieldToGroupBand(final JRDesignBand parentBand, int x, final LayoutItem_Field libglomLayoutItemField) {
374 final String fieldName = addField(libglomLayoutItemField);
376 // Show the field title:
377 final JRDesignStaticText textFieldColumn = createFieldTitleElement(x, 0, libglomLayoutItemField, true);
379 // Instead, the field value will be bold, because here it is like a title.
380 textFieldColumn.setStyle(normalStyle);
382 parentBand.addElement(textFieldColumn);
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);
399 private JRDesignTextField createFieldValueElement(final int x, final String fieldName) {
400 final JRDesignTextField textField = new JRDesignTextField();
402 // Make sure this field starts at the right of the previous field,
403 // because JasperReports uses absolute positioning.
406 textField.setWidth(width); // No data will be shown without this.
408 // This only stretches vertically, but that is better than
410 textField.setStretchWithOverflow(true);
411 textField.setHeight(height); // We must specify _some_ height.
413 final JRDesignExpression expression = createFieldExpression(fieldName);
415 textField.setExpression(expression);
423 * @param libglomLayoutItemField
426 private JRDesignStaticText createFieldTitleElement(final int x, final int y,
427 final LayoutItem_Field libglomLayoutItemField, final boolean withColon) {
428 final JRDesignStaticText textFieldColumn = new JRDesignStaticText();
430 String title = StringUtils.defaultString(libglomLayoutItemField.get_title(this.localeID));
432 // If the title is at the left, instead of above, we need a : to show that it's a title.
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;
446 * @param libglomLayoutItemField
449 private String addField(final LayoutItem_Field libglomLayoutItemField) {
450 fieldsToGet.add(libglomLayoutItemField);
452 final String fieldName = libglomLayoutItemField.get_name();
453 // System.out.print("fieldName=" + fieldName + "\n");
455 // Tell the JasperDesign about the database field that will be in the SQL query,
457 final JRDesignField field = new JRDesignField();
458 field.setName(fieldName); // TODO: Related fields.
459 field.setValueClass(getClassTypeForGlomType(libglomLayoutItemField));
462 design.addField(field);
463 } catch (final JRException e2) {
464 // TODO Auto-generated catch block
465 e2.printStackTrace();
471 * @param libglomLayoutItemField
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()) {
479 klass = java.lang.String.class;
482 klass = java.lang.Boolean.class;
485 klass = java.lang.Double.class;
488 klass = java.util.Date.class;
491 klass = java.sql.Time.class;
494 klass = java.sql.Blob.class; // TODO: This does not work.