/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.module.tem.report.service.impl;
import static java.awt.Color.BLACK;
import static net.sf.jasperreports.crosstabs.JRCellContents.POSITION_X_LEFT;
import static net.sf.jasperreports.crosstabs.JRCellContents.POSITION_Y_TOP;
import static net.sf.jasperreports.crosstabs.JRCrosstab.RUN_DIRECTION_LTR;
import static net.sf.jasperreports.engine.JRElement.MODE_OPAQUE;
import static net.sf.jasperreports.engine.JRVariable.CALCULATION_SUM;
import java.awt.Color;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import net.sf.jasperreports.crosstabs.design.JRDesignCellContents;
import net.sf.jasperreports.crosstabs.design.JRDesignCrosstab;
import net.sf.jasperreports.crosstabs.design.JRDesignCrosstabBucket;
import net.sf.jasperreports.crosstabs.design.JRDesignCrosstabCell;
import net.sf.jasperreports.crosstabs.design.JRDesignCrosstabColumnGroup;
import net.sf.jasperreports.crosstabs.design.JRDesignCrosstabDataset;
import net.sf.jasperreports.crosstabs.design.JRDesignCrosstabMeasure;
import net.sf.jasperreports.crosstabs.design.JRDesignCrosstabRowGroup;
import net.sf.jasperreports.engine.JRBand;
import net.sf.jasperreports.engine.JRBoxContainer;
import net.sf.jasperreports.engine.JRChild;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.base.JRBaseBoxPen;
import net.sf.jasperreports.engine.base.JRBaseLineBox;
import net.sf.jasperreports.engine.design.JRDesignBand;
import net.sf.jasperreports.engine.design.JRDesignDataset;
import net.sf.jasperreports.engine.design.JRDesignDatasetRun;
import net.sf.jasperreports.engine.design.JRDesignElement;
import net.sf.jasperreports.engine.design.JRDesignElementGroup;
import net.sf.jasperreports.engine.design.JRDesignExpression;
import net.sf.jasperreports.engine.design.JRDesignField;
import net.sf.jasperreports.engine.design.JRDesignFrame;
import net.sf.jasperreports.engine.design.JRDesignGroup;
import net.sf.jasperreports.engine.design.JRDesignParameter;
import net.sf.jasperreports.engine.design.JRDesignStaticText;
import net.sf.jasperreports.engine.design.JRDesignSubreport;
import net.sf.jasperreports.engine.design.JRDesignTextElement;
import net.sf.jasperreports.engine.design.JRDesignTextField;
import net.sf.jasperreports.engine.design.JasperDesign;
import org.apache.log4j.Logger;
import org.kuali.kfs.module.tem.report.RString;
import org.kuali.kfs.module.tem.report.RTextStyle;
import org.kuali.kfs.module.tem.report.annotations.ColumnFooter;
import org.kuali.kfs.module.tem.report.annotations.ColumnHeader;
import org.kuali.kfs.module.tem.report.annotations.Crosstab;
import org.kuali.kfs.module.tem.report.annotations.DetailSection;
import org.kuali.kfs.module.tem.report.annotations.Group;
import org.kuali.kfs.module.tem.report.annotations.PageFooter;
import org.kuali.kfs.module.tem.report.annotations.PageHeader;
import org.kuali.kfs.module.tem.report.annotations.Parameter;
import org.kuali.kfs.module.tem.report.annotations.SubReport;
import org.kuali.kfs.module.tem.report.annotations.Summary;
import org.kuali.kfs.module.tem.report.service.TravelReportFactoryService;
import org.kuali.kfs.sys.report.ReportInfo;
/**
* Service interface for creating travel reports. Uses annotations from the {@link org.kuali.kfs.module.tem.report.annotations}
* package to build reports on a {@link ReportInfo} instance. Primarily utilizes classes from
* {@link net.sf.jasperreports.engine.design}
*
*/
@SuppressWarnings("deprecation")
public class TravelReportFactoryServiceImpl implements TravelReportFactoryService {
public static Logger LOG = Logger.getLogger(TravelReportFactoryServiceImpl.class);
private static final int MARGIN = 10;
private static final int PAGEHEADER_HEIGHT = 25;
private static final int REPORT_HEIGHT = 842 - MARGIN;
private static final int TITLE_HEIGHT = 842 / 9; // 1/9 of total height
private static final int DETAIL_HEIGHT = (REPORT_HEIGHT - TITLE_HEIGHT);
private static final int SUMMARY_HEIGHT = ((DETAIL_HEIGHT) / 5) + PAGEHEADER_HEIGHT; // 20% of remaining height
private static final int SUBREPORT_HEIGHT = (DETAIL_HEIGHT - SUMMARY_HEIGHT) / 3; // 3 subreports allowed
private static final int COLHEADER_HEIGHT = 20;
private static final int PAGEFOOTER_HEIGHT = 17;
private static final int COLFOOTER_HEIGHT = 16;
private static final int CT_HEADER_WIDTH = 175;
private static final int CELL_WIDTH = 50;
private static final int CELL_HEIGHT = 18;
private static final int GROUP_HEIGHT = (DETAIL_HEIGHT / 3);
protected Map<String,RTextStyle> styles;
/**
* Creates a level 1 header preset
*
* @param str is a {@link String} to use the preset on
* @return {@link RString} instance that is the embodiment of the style
*/
@Override
public RString h1(final String str) {
return applyStyle(str);
}
/**
* Creates a level 2 header preset
*
* @param str is a {@link String} to use the preset on
* @return {@link RString} instance that is the embodiment of the style
*/
@Override
public RString h2(final String str) {
return applyStyle(str);
}
/**
* Creates a level 3 header preset
*
* @param str is a {@link String} to use the preset on
* @return {@link RString} instance that is the embodiment of the style
*/
@Override
public RString h3(final String str) {
return applyStyle(str);
}
/**
* Creates a level 4 header preset
*
* @param str is a {@link String} to use the preset on
* @return {@link RString} instance that is the embodiment of the style
*/
@Override
public RString h4(final String str) {
return applyStyle(str);
}
/**
* Creates a level 5 header preset
*
* @param str is a {@link String} to use the preset on
* @return {@link RString} instance that is the embodiment of the style
*/
@Override
public RString h5(final String str) {
return applyStyle(str);
}
/**
* Creates a Normal type preset
*
* @param str is a {@link String} to use the preset on
* @return {@link RString} instance that is the embodiment of the style
*/
public RString normal(final String str) {
return applyStyle(str);
}
protected RString applyStyle(final String str) {
final String styleName = new Throwable().getStackTrace()[1].getMethodName();
return new RString(str, getStyles().get(styleName));
}
public JRBand createTitle(final ReportInfo report, final String title) throws Exception {
final JRDesignBand retval = new JRDesignBand();
retval.setHeight(93);
final JRDesignTextField headerLine1 = h1("$P{report}.getInstitution()").toTextField();
addDesignElementTo(retval, headerLine1, 0, 0, 356, 30);
final JRDesignStaticText headerLine2 = h5(title + " for # ").toStaticText();
addDesignElementTo(retval, headerLine2, 0, 31, 426, 24);
final JRDesignTextField headerLine3 = h5("$P{report}.getTripId()").toTextField();
addDesignElementTo(retval, headerLine3, 275, 31, 146, 24);
final JRDesignStaticText headerLine4 = h5("Purpose: ").toStaticText();
addDesignElementTo(retval, headerLine4, 0, 52, 100, 20);
final JRDesignStaticText headerLine5 = h5("Dates: ").toStaticText();
addDesignElementTo(retval, headerLine5, 0, 72, 100, 20);
final JRDesignTextField headerLine4Field1 = h5("$P{report}.getPurpose()").toTextField();
addDesignElementTo(retval, headerLine4Field1, 65, 52, 472, 20);
final JRDesignTextField headerLine5Field1 = h5("$P{report}.getBeginDate()").toTextField(java.util.Date.class);
addDesignElementTo(retval, headerLine5Field1, 45, 72, 75, 20);
final JRDesignTextField headerLine5Field2 = h5("$P{report}.getEndDate()").toTextField(java.util.Date.class);
addDesignElementTo(retval, headerLine5Field2, 150, 72, 75, 20);
return retval;
}
/**
* Retrieves the names of crosstabs. Names are determined by the {@link Field} names of the {@link Field}s in
* the {@link ReportInfo} class with the {@link Crosstab} annotation.
*
* @see org.kuali.kfs.module.tem.report.annotations.Crosstab
* @param report instance to get crosstab names for
* @return a {@link Collection} of names for the crosstabs
*/
protected Collection<String> getCrosstabNames(final ReportInfo report) {
final Collection<String> retval = new ArrayList<String>();
for (final Field f : report.getClass().getDeclaredFields()) {
if (f.getAnnotation(Crosstab.class) != null) {
retval.add(f.getName());
}
}
return retval;
}
/**
* Retrieves the names of crosstabs. Names are determined by the {@link Field} names of the {@link Field}s in
* the {@link ReportInfo} class with the {@link Crosstab} annotation. Also, only gets {@link Crosstab}
* {@link Field}s by additional annotations that may describe them.
*
* @see org.kuali.kfs.module.tem.report.annotations.Crosstab
* @param report instance to get crosstab names for
* @param annotations {@link Class} instances of additional annotations that must be present on the {@link Field}
* @return a {@link Collection} of names for the crosstabs
*/
protected Collection<String> getCrosstabNames(final ReportInfo report, final Class ... annotations) {
final Collection<String> retval = new ArrayList<String>();
for (final Field f : report.getClass().getDeclaredFields()) {
boolean valid = false;
valid |= f.getAnnotation(Crosstab.class) != null;
if (annotations != null && annotations.length > 0) {
for (final Class annotation : annotations) {
valid &= f.getAnnotation(annotation) != null;
}
}
if (valid) {
retval.add(f.getName());
}
}
return retval;
}
/**
* Determines if the {@link ReportInfo} instance has any {@link Subreport} defined
*
* @param report to check for {@link Subreport} in
* @return true if there is a {@link Subreport} or false otherwise
*/
protected boolean hasSubreport(final ReportInfo report) {
return hasFieldWithAnnotations(report, SubReport.class);
}
/**
* Determines if the {@link ReportInfo} instance has any {@link Summary} defined
*
* @param report to check for {@link Summary} in
* @return true if there is a {@link Summary} or false otherwise
*/
@Override
public boolean hasSummary(final ReportInfo report) {
return hasFieldWithAnnotations(report, Summary.class);
}
/**
* Determines if the {@link ReportInfo} instance has any {@link DetailSection} defined
*
* @param report to check for {@link DetailSection} in
* @return true if there is a {@link DetailSection} or false otherwise
*/
protected boolean hasDetail(final ReportInfo report) {
return hasFieldWithAnnotations(report, DetailSection.class) || hasGroup(report);
}
/**
* Determines if the {@link ReportInfo} instance has any {@link PageHeader} defined
*
* @param report to check for {@link PageHeader} in
* @return true if there is a {@link PageHeader} or false otherwise
*/
protected boolean hasPageHeader(final ReportInfo report) {
return hasFieldWithAnnotations(report, PageHeader.class);
}
/**
* Determines if the {@link ReportInfo} instance has any {@link PageFooter} defined
*
* @param report to check for {@link PageFooter} in
* @return true if there is a {@link PageFooter} or false otherwise
*/
protected boolean hasPageFooter(final ReportInfo report) {
return hasFieldWithAnnotations(report, PageFooter.class);
}
/**
* Determines if the {@link ReportInfo} instance has any {@link ColumnHeader} defined
*
* @param report to check for {@link ColumnHeader} in
* @return true if there is a {@link ColumnHeader} or false otherwise
*/
protected boolean hasColumnHeader(final ReportInfo report) {
return hasFieldWithAnnotations(report, ColumnHeader.class);
}
/**
* Determines if the {@link ReportInfo} instance has any {@link ColumnFooter} defined
*
* @param report to check for {@link ColumnFooter} in
* @return true if there is a {@link ColumnFooter} or false otherwise
*/
protected boolean hasColumnFooter(final ReportInfo report) {
return hasFieldWithAnnotations(report, ColumnFooter.class);
}
/**
* Check to see if a {@link ReportInfo} instance belongs to a {@link Class} with a given annotation. This
* is a very general method used to find things like {@link ColumnFooter} instances or {@link PageHeader}
* instances
*
* @param report is the {@link ReportInfo} instance
* @param annotation to look for that would be on a field like {@link SubReport} or {@link Summary}
* @return true if the annotation exists, false otherwise
*/
protected boolean hasFieldWithAnnotation(final ReportInfo report, final Class annotation) {
for (final Field field : report.getClass().getDeclaredFields()) {
if (field.getAnnotation(annotation) != null) {
return true;
}
}
return false;
}
/**
* Create a detail section in a {@link JasperReport}. Checks the {@link JasperReport} fields for a
* {@link Detail} annotation and processes that {@link Detail} field.
*/
public JRBand createDetail(final Field subReport) throws Exception {
final JRDesignBand retval = new JRDesignBand();
retval.setHeight(SUBREPORT_HEIGHT - 25);
LOG.debug("Subreport Detail band has height of "+ retval.getHeight());
// In this case, we are creating a subreport and subreports have either a crosstab or not
LOG.debug("Checking if "+ subReport+ " is a crosstab "+ isCrosstab(subReport));
if (isCrosstab(subReport)) {
final JRDesignCrosstab crosstab = createCrosstab();
LOG.debug("Got crosstab of height "+ crosstab.getHeight()+
" and width "+ crosstab.getWidth()+
" adding to design of height "+ retval.getHeight());
retval.addElement(crosstab);
}
return retval;
}
/**
* Create a summary section in a {@link JasperReport}. Checks the {@link JasperReport} fields for a
* {@link Summary} annotation and processes that {@link Summary} field.
*
* @return subreport is the {@link SubReport} {@link Field}
* @return {@link JRBand} of the summary that goes into a subreport
*/
public JRBand createSummary(final Field subReport) throws Exception {
final JRDesignBand retval = new JRDesignBand();
retval.setHeight(SUBREPORT_HEIGHT);
LOG.debug("Summary band has height of "+ retval.getHeight());
// In this case, we are creating a subreport and subreports have either a crosstab or not
LOG.debug("Checking if "+ subReport+ " is a crosstab "+ isCrosstab(subReport));
if (isCrosstab(subReport)) {
final JRDesignCrosstab crosstab = createCrosstab();
LOG.debug("Got crosstab of height "+ crosstab.getHeight()+ " and width "+ crosstab.getWidth()+ " adding to design of height "+ retval.getHeight());
retval.addElement(crosstab);
}
return retval;
}
/**
* Create a detail section in a {@link JasperReport}. Checks the {@link JasperReport} fields for a
* {@link Detail} annotation and processes that {@link Detail} field.
*/
protected JRBand createDetailForSummary(final ReportInfo report) throws Exception {
final JRDesignBand retval = new JRDesignBand();
int maxHeight = DETAIL_HEIGHT;
LOG.debug("Summary: Initial height is "+ DETAIL_HEIGHT);
retval.setHeight(CELL_HEIGHT + 5);
LOG.debug("Summary: Detail band has height of "+ maxHeight);
int y = 0;
LOG.info("Summary: Adding fields for detail");
final Field summaryField = getFieldWithAnnotation(report, Summary.class);
if (isCrosstab(summaryField)) {
// If the summary has a crosstab, then we want to use the Summary section for rendering the crosstab.
final Collection<JRChild> elements = processFields(report, Summary.class, Crosstab.class);
for (final JRChild element : elements) {
final JRDesignCrosstab crosstab = (JRDesignCrosstab) element;
LOG.debug("Adding crosstab to summary "+ crosstab+ " with height "+ crosstab.getHeight());
crosstab.setY(y);
retval.addElement(crosstab);
y += crosstab.getHeight() + PAGEHEADER_HEIGHT;
retval.setHeight(y);
}
}
else {
// No crosstab, so use the detail
final JRDesignTextField nameField = normal("$F{name}").toTextField();
addDesignElementTo(retval, nameField, (CELL_WIDTH * 3 + 5) * 0, 0, CELL_WIDTH * 3, CELL_HEIGHT);
final JRDesignTextField amountField = normal("$F{amount}").toTextField(java.math.BigDecimal.class);
addDesignElementTo(retval, amountField, (CELL_WIDTH * 3 + 5) * 1, 0, CELL_WIDTH * 3, CELL_HEIGHT);
}
return retval;
}
/**
* Create a detail section in a {@link JasperReport}. Checks the {@link JasperReport} fields for a
* {@link Detail} annotation and processes that {@link Detail} field.
*/
public JRBand createDetail(final ReportInfo report, final Integer reportIndex) throws Exception {
final JRDesignBand retval = new JRDesignBand();
if (!(hasDetail(report) || hasSubreport(report))) {
LOG.info("No detail for this report");
LOG.debug("Has detail "+ hasDetail(report));
LOG.debug("Has Subreport "+ hasSubreport(report));
if (reportIndex > 0) {
return null;
}
retval.setHeight(0);
return retval;
}
int maxHeight = DETAIL_HEIGHT;
LOG.debug("Initial height is "+ DETAIL_HEIGHT);
retval.setHeight(0);
LOG.info("Determining maximum detail space size");
if (hasPageHeader(report)) {
maxHeight -= PAGEHEADER_HEIGHT;
}
if (hasPageFooter(report)) {
maxHeight -= PAGEFOOTER_HEIGHT;
}
if (hasColumnHeader(report)) {
maxHeight -= COLHEADER_HEIGHT;
}
if (hasColumnFooter(report)) {
maxHeight -= COLFOOTER_HEIGHT;
}
LOG.debug("Detail band has height of "+ maxHeight);
// Detail includes subreports. This really means besides subreports.
if (hasDetail(report)) {
LOG.info("Adding fields for detail");
final JRDesignTextField nameField = normal("$F{name}").toTextField();
addDesignElementTo(retval, nameField, 0, 0, CELL_WIDTH * 3, CELL_HEIGHT);
final JRDesignTextField dateField = normal("$F{date}").toTextField();
addDesignElementTo(retval, dateField, (CELL_WIDTH * 3 + 5) * 1, 0, CELL_WIDTH * 3, CELL_HEIGHT);
final JRDesignTextField amountField = normal("$F{amount}").toTextField(java.math.BigDecimal.class);
addDesignElementTo(retval, amountField, (CELL_WIDTH * 3 + 5) * 2, 0, CELL_WIDTH * 3, CELL_HEIGHT);
}
int y = 0;
int pageidx = 0;
int upperBound = 19;
final Collection<JRChild> elements = processFields(report, SubReport.class);
LOG.debug("Building report detail starting at position "+ y);
LOG.debug("Adding "+ elements.size()+ " elements to the report");
for (final JRChild obj : elements) {
if (obj != null && obj instanceof JRDesignElement) {
final JRDesignElement element = (JRDesignElement) obj;
// If we exceed the bounds of the report, we have to switch pages.
// This is pretty much what we do until we reach the correctpage
if ((y + element.getHeight()) >= maxHeight) {
pageidx++;
y = 0;
}
// Going to get rid of this. There should never be a crosstab in the detail section unless
// it is in a subreport or a group
if (element instanceof JRDesignCrosstab) {
final JRDesignCrosstab crosstab = (JRDesignCrosstab) element;
final String crosstabName = crosstab.getDataset().getDatasetRun().getDatasetName();
final JRDesignStaticText headerLine4 = h4(crosstabName).toStaticText();
if (pageidx == reportIndex) {
addDesignElementTo(retval, headerLine4, 0, y, 356, 22);
}
y += 25;
}
LOG.debug("Adding element to detail "+ element.getClass()+ " " + element+ " with height "+ element.getHeight()+ " at y = "+ y);
// When we're on the correct page index, we add elements
LOG.debug("pageIdx = "+ pageidx);
LOG.debug("reportIndex = "+ reportIndex);
if (pageidx == reportIndex) {
element.setY(y);
retval.addElement(element);
}
y += element.getHeight() + 30;
upperBound = Math.max(upperBound, y);
LOG.debug("upperbound = "+ upperBound);
}
}
// Only return if elements were added.
if (hasCrosstabs(report) && subReportCount(retval) == 0) {
return null;
}
if (hasGroup(report) && reportIndex > 0) {
return null;
}
LOG.debug("Setting height to "+ upperBound);
retval.setHeight(upperBound);
return retval;
}
/**
* Measure instance used for crosstabs.
*/
protected JRDesignCrosstabMeasure measure(final String name) {
final JRDesignCrosstabMeasure retval = new JRDesignCrosstabMeasure();
final JRDesignExpression expression = new JRDesignExpression();
expression.setValueClass(java.math.BigDecimal.class);
expression.setText("$F{" + name + "}");
retval.setValueExpression(expression);
retval.setName(name + "Measure");
retval.setValueClassName("java.math.BigDecimal");
retval.setCalculation(CALCULATION_SUM);
return retval;
}
/**
* Convenience method for creating buckets ({@link JRDesignCrosstabBucket}) instances for things like groups and crosstabs.
*
* @param text is the {@link String} value of an expression used for the bucket
* @param valueClass is a {@link Class} that the text is eventually translated to
*/
protected JRDesignCrosstabBucket bucket(final String text, final Class valueClass) {
final JRDesignCrosstabBucket retval = new JRDesignCrosstabBucket();
final JRDesignExpression expression = new JRDesignExpression();
expression.setValueClass(valueClass);
expression.setText(text);
retval.setExpression(expression);
return retval;
}
/**
* Convenience method for adding an element to a parent whilst adjusting the size and location
*
* @param band is a {@link JRDesignElementGroup} you want to add to
* @param toAdd is the {@link JRDesignTextElement} you'd like to add to the aforementioned band.
* @param x is the x-location
* @param y is the y-location
* @param width is the width to set to
* @param height is the height to set to
*/
protected void addDesignElementTo(final JRDesignElementGroup band, final JRDesignTextElement toAdd, int x, int y, int width, int height) {
toAdd.setX(x);
toAdd.setY(y);
toAdd.setWidth(width);
toAdd.setHeight(height);
band.addElement(toAdd);
}
/**
* If report title not set
* Guess what the report title should be. Uses the classname of the {@link ReportInfo} instance to
* determine what the title should be. Rather than using camel case, adds spaces.
*
* @param report to create title for
* @return
*/
protected String getReportTitle(final ReportInfo report) {
if(report.getReportTitle() != null){
return report.getReportTitle();
}
final String className = report.getClass().getSimpleName();
return splitByUpperCase(className.substring(0, className.indexOf("Report")));
}
/**
* Capitalizes each word.
*
* @param toCapitalize {@link String} instance to capitalize
* @return new {@link String} instance
*/
protected String initialCaps(final String toCapitalize) {
final char[] str_arr = toCapitalize.toCharArray();
str_arr[0] = Character.toUpperCase(str_arr[0]);
for (int i = 1; i < str_arr.length; i++) {
if (str_arr[i] == ' ' && (i + 1) < str_arr.length) {
str_arr[i + 1] = Character.toUpperCase(str_arr[i + 1]);
}
}
return new String(str_arr);
}
/**
* Adds spaces at upper case characters thus splitting up the camel case of a string like in a class name
* @param toSplit is a {@link String} to split
* @return a {@link String} instance that has added spaces before uppercase characters
*/
protected String splitByUpperCase(final String toSplit) {
final StringBuilder retval = new StringBuilder(toSplit);
final char[] str_arr = toSplit.toCharArray();
int inc = 0;
for (int i = 1; i < str_arr.length; i++) {
if (str_arr[i] >= 'A' && str_arr[i] <= 'Z' ) {
retval.insert(i + inc, ' '); // Add a space after uppercase characters
inc++;
}
}
return retval.toString();
}
/**
* Systematically retrieves groups from a {@link ReportInfo} instances, creates a {@link JRDesignGroup} from the info,
* then adds the {@link JRDesignGroup} instance to a {@link JasperDesign} for each group.
*
* @param report the {@link ReportInfo} instance to get groups for
* @param designObj is the {@link JasperDesign} instance to add {@link JRDesignGroup} instances to
*/
protected void addGroupsFor(final ReportInfo report, final JasperDesign designObj) throws Exception {
for (final Field field : report.getClass().getDeclaredFields()) {
if (isGroup(field)) {
LOG.info("Adding a group for field "+ field.getName());
final JRDesignGroup group = createGroup(report, field);
designObj.addGroup(group);
}
}
}
/**
* Determines from the {@link Class} of a {@link ReportInfo} instance what {@link Field}s attached to it are
* qualified as {@link JRDesignParameter}s to add to your {@link JasperDesign}.
*
* @param report the {@link ReportInfo} instance representing the report
* @param designObj the {@link JasperDesign} instance to add parameters to
*/
protected void addReportParametersFor(final ReportInfo report, final JasperDesign designObj) throws Exception {
for (final Field field : report.getClass().getDeclaredFields()) {
if (isParameter(field)) {
final JRDesignParameter designParameter = new JRDesignParameter();
designParameter.setName(field.getName());
designParameter.setValueClass(field.getType());
designObj.addParameter(designParameter);
LOG.debug("Added parameter "+ designParameter.getName());
}
if (isSubreport(field)) {
final JRDesignParameter designParameter = new JRDesignParameter();
designParameter.setName(field.getName() + "Subreport");
designParameter.setValueClass(JasperReport.class);
designObj.addParameter(designParameter);
LOG.debug("Added parameter "+ designParameter.getName());
}
else if (isCrosstab(field)) {
final JRDesignDataset dataset = new JRDesignDataset(false);
addReportFieldsFor(report, dataset);
dataset.setName(initialCaps(field.getName()));
designObj.addDataset(dataset);
}
}
final JRDesignParameter designParameter = new JRDesignParameter();
designParameter.setName("report");
designParameter.setValueClass(report.getClass());
designObj.addParameter(designParameter);
LOG.debug("Added parameter "+ designParameter.getName());
}
/**
* Determines from the {@link Class} of a {@link ReportInfo} instance what {@link Field}s attached to it are
* qualified as {@link JRDesignParameter}s to add to your {@link JasperDesign}
*
* @param report the {@link ReportInfo} instance representing the report
* @param designObj the {@link JasperDesign} instance to add parameters to
*/
protected void addReportFieldsFor(final ReportInfo report, final JasperDesign designObj) throws Exception {
final Class dataClass = findDataClassFor(report);
LOG.debug("Found data class "+ dataClass);
for (final Field field : dataClass.getDeclaredFields()) {
final JRDesignField designField = new JRDesignField();
LOG.debug("Adding field " + field.getName());
designField.setName(field.getName());
designField.setValueClass(field.getType());
designObj.addField(designField);
}
}
/**
* Determines from the {@link Class} of a {@link ReportInfo} instance what {@link Field}s attached to it are
* qualified as {@link JRDesignField}s to add to your {@link JasperDesign}.
*
* @param report the {@link ReportInfo} instance representing the report
* @param designObj the {@link JasperDesign} instance to add {@link JRDesignField}s to
*/
protected void addReportFieldsFor(final ReportInfo report, final JRDesignDataset dataset) throws Exception {
final Class dataClass = findDataClassFor(report);
LOG.debug("Found data class "+ dataClass);
for (final Field field : dataClass.getDeclaredFields()) {
final JRDesignField designField = new JRDesignField();
designField.setName(field.getName());
designField.setValueClass(field.getType());
dataset.addField(designField);
}
}
/**
* A {@link ReportInfo} can have one type that represents the structure that all data will be. This method determines what
* that is by accessing the parameterized type of a setter method who's getter returns a {@link JRDataSource} instance.
*
* @param report is the report to get the data class for
* @return {@link Class} instance that is the type for the data
*/
protected Class findDataClassFor(final ReportInfo report) throws Exception {
final Class reportClass = report.getClass();
// Digging up all the setters
for (final Field field : reportClass.getDeclaredFields()) {
LOG.debug("Examinine field "+ field+ " with type "+ field.getType());
if (field.getType().equals(JRDataSource.class)) {
// get the dataset for this class
final String setterName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
final Method setter = reportClass.getMethod(setterName, new Class[] {Collection.class});
LOG.debug("Determining what that data class should be. Found dataset setter method "+ setter.getName());
// Should only have one parameter
final ParameterizedType methodParamType = (ParameterizedType) (setter.getGenericParameterTypes()[0]);
return (Class) methodParamType.getActualTypeArguments()[0];
}
}
return null;
}
/**
* A lot like {@link #designReport(ReportInfo)} except it is intended for {@link SubReport}s
*
* @param report is an instance of the {@link ReportInfo} which represents a report.
* @param field {@link Field} instance with a {@link SubReport} annotation
* @return a {@link JasperDesign} instance used in a {@link JasperReport}
* @see org.kuali.kfs.module.tem.report.service.TravelReportFactoryService#designReport(org.kuali.kfs.sys.report.ReportInfo)
*/
public JasperDesign designReport(final ReportInfo report, final Field field) throws Exception {
LOG.info("Designing a subreport for field "+ field.getName());
LOG.debug("Checking the "+ field.getName()+ " for data");
try {
field.setAccessible(true);
if (field.get(report) == null) {
return null;
}
LOG.debug("Subreport has data. Proceeding to design subreport.");
}
catch (Exception e) {
throw new RuntimeException(e);
}
final JasperDesign designObj = new JasperDesign();
designObj.setName(field.getName());
designObj.setTitle(createTitle(report, getReportTitle(report)));
addReportParametersFor(report, designObj);
designObj.addImport(report.getClass().getName());
addReportFieldsFor(report, designObj);
final JRDesignBand titleBand = new JRDesignBand();
titleBand.setHeight(23);
final JRDesignStaticText headerLine4 = h4(initialCaps(field.getName())).toStaticText();
addDesignElementTo(titleBand, headerLine4, 0, 0, 356, 22);
designObj.setPageHeader(titleBand);
designObj.setPageWidth(595);
designObj.setPageHeight(REPORT_HEIGHT);
designObj.setLeftMargin(MARGIN);
designObj.setRightMargin(MARGIN);
designObj.setTopMargin(MARGIN);
designObj.setBottomMargin(MARGIN);
if (hasDetail(report)) {
LOG.debug("Creating detail for subreport");
designObj.setDetail(createDetail(report, 0));
}
else {
LOG.debug("Creating summary for subreport");
designObj.setSummary(createSummary(field));
}
return designObj;
}
/**
* Populate the design of a report. Report's main content container is a design.
*
* @param report in
* @return {@link JasperDesign} out
* @throws Exception because there is a lot of under-the-hood I/O and reflection going on.
*/
@Override
public JasperDesign designSummary(final ReportInfo report) throws Exception {
final JasperDesign designObj = new JasperDesign();
final String reportTitle = getReportTitle(report);
designObj.setName(reportTitle);
designObj.setTitle(createTitle(report, reportTitle));
LOG.info("Summary: Loading report parameters");
addReportParametersFor(report, designObj);
designObj.addImport(report.getClass().getName());
LOG.info("Summary: Loading report fields");
addReportFieldsFor(report, designObj);
LOG.info("Summary: Setting report dimensions");
designObj.setPageWidth(595);
designObj.setPageHeight(REPORT_HEIGHT);
designObj.setLeftMargin(MARGIN);
designObj.setRightMargin(MARGIN);
designObj.setTopMargin(MARGIN);
designObj.setBottomMargin(MARGIN);
LOG.info("Summary: Adding header and footer");
final JRDesignBand header = new JRDesignBand();
final JRDesignStaticText headerLine4 = h4("Summary").toStaticText();
addDesignElementTo(header, headerLine4, 0, PAGEHEADER_HEIGHT, 356, 22);
header.setHeight(PAGEHEADER_HEIGHT * 2);
designObj.setPageHeader(header);
LOG.info("Creating report detail");
final Field summaryField = getFieldWithAnnotation(report, Summary.class);
final JRBand summary = createSummary(summaryField);
if (summary != null) {
designObj.setSummary(summary);
return designObj;
}
return null;
}
/**
* Populate the design of a report. Report's main content container is a design.
*
* @param report in
* @return {@link JasperDesign} out
* @throws Exception because there is a lot of under-the-hood I/O and reflection going on.
*/
@Override
public JasperDesign designReport(final ReportInfo report, final Integer reportIndex) throws Exception {
final JasperDesign designObj = new JasperDesign();
final String reportTitle = getReportTitle(report);
designObj.setName(reportTitle);
if (reportIndex < 1) {
designObj.setTitle(createTitle(report, reportTitle));
}
LOG.info("Loading report parameters");
addReportParametersFor(report, designObj);
designObj.addImport(report.getClass().getName());
LOG.info("Loading report fields");
addReportFieldsFor(report, designObj);
LOG.info("Setting report dimensions");
designObj.setPageWidth(595);
designObj.setPageHeight(REPORT_HEIGHT);
designObj.setLeftMargin(MARGIN);
designObj.setRightMargin(MARGIN);
designObj.setTopMargin(MARGIN);
designObj.setBottomMargin(MARGIN);
// Groups before detail
LOG.info("Handling groups");
addGroupsFor(report, designObj);
LOG.info("Creating report detail");
final JRBand detail = createDetail(report, reportIndex);
if (detail != null) {
designObj.setDetail(detail);
}
else {
return null;
}
return designObj;
}
/**
* Determines what to do with a {@link Field} in a {@link ReportInfo} instance by the annotations on that
* {@link Field} and creates a {@link JRDesignElement} from it.
*
*
* @param param
* @param field
* @return {@link JRDesignElement} instance
* @throws Exception
*/
protected JRChild createElementForField(final ReportInfo report, final Field field) throws Exception {
LOG.info("Processing field "+ field.getName());
if (isSubreport(field)) {
LOG.debug("Creating a report element from field "+ field.getName());
return createSubreport(report, field);
}
if (isCrosstab(field)) {
LOG.debug("Creating a crosstab from field "+ field.getName());
final JRDesignCrosstab crosstab = createCrosstab(report, field);
final JRDesignCrosstabDataset dataset = new JRDesignCrosstabDataset();
final JRDesignDatasetRun dsRun = new JRDesignDatasetRun();
final JRDesignExpression dsExpression = new JRDesignExpression();
final String getterName = "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
dsExpression.setText("$P{report}." + getterName + "()");
dsExpression.setValueClass(JRDataSource.class);
dsRun.setDatasetName(initialCaps(field.getName()));
dsRun.setDataSourceExpression(dsExpression);
dataset.setDatasetRun(dsRun);
dataset.setDataPreSorted(true);
crosstab.setDataset(dataset);
return crosstab;
}
if (isSummary(field)) {
LOG.debug("Building Summary JRDesignBand");
final JRDesignBand summary = new JRDesignBand();
final Class dataClass = findDataClassFor(report);
final JRDesignStaticText header = h3("Summary").toStaticText();
addDesignElementTo(summary, header, 0, 0, CT_HEADER_WIDTH, PAGEHEADER_HEIGHT);
int fieldIdx = 0;
for (final Field dataField : dataClass.getDeclaredFields()) {
final JRDesignStaticText headerField = h5(dataField.getName()).toStaticText();
addDesignElementTo(summary, headerField, (CELL_WIDTH * 3 + 5) * fieldIdx, PAGEHEADER_HEIGHT, CELL_WIDTH * 2, CELL_HEIGHT);
final JRDesignTextField textElement = normal("$F{" + dataField.getName() + "}").toTextField(dataField.getType());
LOG.debug("Adding summary field "+ dataField.getName()+ " at ("+ (CELL_WIDTH + 5) * fieldIdx+ "+ 0");
addDesignElementTo(summary, textElement, (CELL_WIDTH * 3 + 5) * fieldIdx, PAGEHEADER_HEIGHT + CELL_HEIGHT, CELL_WIDTH * 3, CELL_HEIGHT);
fieldIdx++;
}
summary.setHeight(SUMMARY_HEIGHT);
return summary;
}
return null;
}
/**
* A report (the parent) is created from a normal {@link ReportInfo} instance. {@link SubReport}s do not have a {@link ReportInfo} instance.
* Their fields are gotten instead via the fields of its data type. These are required to be passed through to the report
* when the report is created so that they can become fields in the report.
*
* @param report the parent report
* @return a {@link Collection} instance containing {@link Field} objects that will be mapped to report fields when creating the
* report.
*/
@Override
public Collection<Field> getSubreportFieldsFrom(final ReportInfo report) throws Exception {
final Collection<Field> retval = new ArrayList<Field>();
for (final Field field : report.getClass().getDeclaredFields()) {
if (field.getAnnotation(SubReport.class) != null) {
retval.add(field);
}
}
return retval;
}
/**
* Create a subreport {@link JRDesignSubreport}. Since all kinds of reports are created through the {@link TravelReportFactoryServiceImpl} class,
* even sub report instances will be considered normal reports and created individually. This method just marks where in the
* parent report to place the sub report. This does not actually create another report.
*
* @param report is the parent report
* @field is the field that is a {@link SubReport}
* @return {@link JRDesignSubreport} to be added to report
*/
protected JRDesignSubreport createSubreport(final ReportInfo report, final Field field) throws Exception {
final JRDesignSubreport retval = new JRDesignSubreport(new JasperDesign());
final JRDesignExpression dsExpression = new JRDesignExpression();
final String getterName = "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
dsExpression.setText("$P{report}." + getterName + "()");
dsExpression.setValueClass(JRDataSource.class);
retval.setDataSourceExpression(dsExpression);
final JRDesignExpression expression = new JRDesignExpression();
expression.setValueClass(JasperReport.class);
expression.setText("$P{" + field.getName() + "Subreport}");
retval.setExpression(expression);
retval.setHeight(SUBREPORT_HEIGHT);
return retval;
}
/**
* Create a subreport for the specified field. {@link Field} instance must have {@link SubReport} annotation
*
* @return JasperReport as a subreport
*/
@Override
public JasperReport processReportForField(final ReportInfo report, final Field field) throws Exception {
final JasperDesign design = designReport(report, field);
if (design == null) {
return null;
}
final JasperReport retval = JasperCompileManager.compileReport(design);
retval.setWhenNoDataType(JasperReport.WHEN_NO_DATA_TYPE_ALL_SECTIONS_NO_DETAIL);
return retval;
}
/**
* Constructs a header for the {@link JRDesignGroup}
*
* @return {@link JRBand} instance that is your header
*/
protected JRBand createGroupHeader(final ReportInfo report) {
final JRDesignBand retval = new JRDesignBand();
retval.setHeight(PAGEHEADER_HEIGHT * 4);
final JRDesignTextField expenseType = h4("$F{expenseType}").toTextField();
addDesignElementTo(retval, expenseType, 0, PAGEHEADER_HEIGHT, CT_HEADER_WIDTH, CELL_HEIGHT + 10);
final JRDesignStaticText nameField = h5("Expense").toStaticText();
addDesignElementTo(retval, nameField, (CELL_WIDTH * 3 + 5) * 0, PAGEHEADER_HEIGHT * 2 + CELL_HEIGHT, CELL_WIDTH * 3, CELL_HEIGHT);
final JRDesignStaticText dateField = h5("Date").toStaticText();
addDesignElementTo(retval, dateField, (CELL_WIDTH * 3 + 5) * 1, PAGEHEADER_HEIGHT * 2 + CELL_HEIGHT, CELL_WIDTH * 3, CELL_HEIGHT);
final JRDesignStaticText amountField = h5("Amount").toStaticText();
addDesignElementTo(retval, amountField, (CELL_WIDTH * 3 + 5) * 2, PAGEHEADER_HEIGHT * 2 + CELL_HEIGHT, CELL_WIDTH * 3, CELL_HEIGHT);
return retval;
}
/**
* Constructs a footer for the {@link JRDesignGroup}
*
* @return {@link JRBand} instance that is your footer
*/
protected JRBand createGroupFooter(final ReportInfo report) {
final JRDesignBand band = new JRDesignBand();
band.setSplitAllowed(true);
return band;
}
/**
* Field name to group data on
*
* @param field with a {@link Group} annotation
* @return String
*/
protected String getGroupFieldFrom(final Field field) {
final Group group = field.getAnnotation(Group.class);
if (group == null) {
// field isn't for a group Ahhh!!!
throw new IllegalArgumentException(field + " is not grouped!");
}
return group.value();
}
/**
* Factory method for creating a {@link JRDesignGroup} instance. This can really only be used on
* reports that have at least 1 {@link Field} with a {@link Group} annotation and implements the {@link DetailedReport} interface.
*
* @param report instance the {@link JRDesignGroup} belongs to
* @param field that the {@link Group} will draw data from
* @return {@link JRDesignGroup} instance that can be set on a {@link JasperDesign}
*/
protected JRDesignGroup createGroup(final ReportInfo report, final Field field) {
final JRDesignGroup group = new JRDesignGroup();
final String groupFieldName = getGroupFieldFrom(field);
final JRDesignExpression expression = new JRDesignExpression();
expression.setValueClass(String.class);
expression.setText("$F{" + groupFieldName + "}");
group.setExpression(expression);
group.setName(groupFieldName);
group.setGroupHeader(createGroupHeader(report));
group.setGroupFooter(createGroupFooter(report));
return group;
}
protected boolean hasGroup(final ReportInfo report) {
return groupCount(report) > 0;
}
protected int groupCount(final ReportInfo report) {
int retval = 0;
try {
for (final Field field : report.getClass().getDeclaredFields()) {
if (field.getAnnotation(Group.class) != null) {
retval++;
}
}
}
catch (Exception e) {
LOG.warn("Unable to get group count");
}
return retval;
}
protected int subReportCount(final JRBand design) {
int retval = 0;
for (final Object obj : design.getElements()) {
if (obj instanceof JRDesignSubreport) {
retval++;
}
}
return retval;
}
protected int subReportCount(final ReportInfo report) {
int retval = 0;
try {
for (final Field field : report.getClass().getDeclaredFields()) {
if (field.getAnnotation(SubReport.class) != null) {
retval++;
}
}
}
catch (Exception e) {
LOG.warn("Unable to get subreport count");
}
return retval;
}
protected int groupTotalHeight(final ReportInfo report) {
return groupCount(report) + GROUP_HEIGHT;
}
/**
* Overloaded version of {@link #processFields(ReportInfo, Class ...)}.
*
* @param report {@link ReportInfo} instance to process fields on
* @return {@link Collection} of {@link JRDesignElement} instances that are part of the {@link ReportInfo}
* @throws Exception because there are a lot of I/O and Reflection actions performed that can
* potentially cause problems.
*/
protected Collection<JRChild> processFields(final ReportInfo report) throws Exception {
return processFields(report, new Class[] {});
}
/**
* Create {@link JRDesignElement} instances used in a {@link JasperReport} based on fields in a {@link ReportInfo} instance.
* This method tries to be smart about what fields in the {@link ReportInfo} are to be used and how they
* are used in the input.
*
* @param report {@link ReportInfo} instance to process fields on
* @param annotations is a filter on what fields by the {@link Annotation} they might have.
* @return {@link Collection} of {@link JRDesignElement} instances that are part of the {@link ReportInfo}
* @throws Exception because there are a lot of I/O and Reflection actions performed that can
* potentially cause problems.
*/
protected Collection<JRChild> processFields(final ReportInfo report, final Class ... annotations) throws Exception {
final Collection<JRChild> retval = new ArrayList<JRChild>();
for (final Field field : report.getClass().getDeclaredFields()) {
boolean valid = true;
valid &= hasAnnotations(field, annotations) && !hasAnnotations(field, Summary.class, SubReport.class);
if (valid) {
retval.add(createElementForField(report, field));
}
}
return retval;
}
/**
* Overloaded convenience method for creating {@link JRDesignCrosstabCell} instances. This instance assumes there is no column group.
*
* @param type is a {@link Class} is used to determine how to render the {@link JRDesignTextField} contained
* in the {@link JRDesignCrosstabCell}
* @param value is the {@link String} value to render in the {@link JRDesignCrosstabCell}
* @param rowGroup is the name of the rowgroup this cell is part of.
*/
protected JRDesignCrosstabCell crosstabCell(final Class type, final String value, final String rowGroup) {
return crosstabCell(type, value, rowGroup, null);
}
/**
* Convenience method for creating {@link JRDesignCrosstabCell} instances. It is possible to be part of a row group and a column group.
*
* @param type is a {@link Class} is used to determine how to render the {@link JRDesignTextField} contained
* in the {@link JRDesignCrosstabCell}
* @param value is the {@link String} value to render in the {@link JRDesignCrosstabCell}
* @param rowGroup is the name of the rowgroup this cell is part of.
* @param colGroup is the name of the column group this cell is part of.
*/
protected JRDesignCrosstabCell crosstabCell(final Class type, final String value, final String rowGroup, final String colGroup) {
LOG.debug("Creating cell with row group = "+ rowGroup+ " and column group = "+ colGroup);
final JRDesignCrosstabCell retval = new JRDesignCrosstabCell();
final JRDesignCellContents contents = new JRDesignCellContents();
final JRDesignTextField field = normal(value).toTextField(type);
addDesignElementTo(contents, field, 0, 0, CELL_WIDTH, CELL_HEIGHT);
contents.setMode(MODE_OPAQUE);
contents.setBackcolor(Color.LIGHT_GRAY);
final JRDesignFrame frame = new JRDesignFrame();
frame.copyBox(new TravelReportLineBox(contents));
contents.setBox(frame);
retval.setWidth(CELL_WIDTH);
retval.setHeight(CELL_HEIGHT);
retval.setContents(contents);
retval.setRowTotalGroup(rowGroup);
retval.setColumnTotalGroup(colGroup);
return retval;
}
/**
* Overloaded convenience method for creating {@link JRDesignCrosstabCell} instances. This case assumes there is no row or column group
*
* @param type is a {@link Class} is used to determine how to render the {@link JRDesignTextField} contained
* in the {@link JRDesignCrosstabCell}
* @param value is the {@link String} value to render in the {@link JRDesignCrosstabCell}
*/
protected JRDesignCrosstabCell crosstabCell(final Class type, final String value) {
return crosstabCell(type, value, null, null);
}
/**
* Creates a crosstab. Intended for use withing a summary. Creates a crosstab and a custom dataset for the
* crosstab to use in a {@link Summary}
*/
protected JRDesignCrosstab createCrosstab(final ReportInfo report, final Field field) throws Exception {
final JRDesignCrosstab crosstab = new JRDesignCrosstab();
LOG.debug("<crosstab>");
LOG.debug("<reportElement width=\"400\" height=\"" + (SUMMARY_HEIGHT - 25) + "\" />");
crosstab.setWidth(595);
crosstab.setHeight(0);
final JRDesignCellContents nodataCell = new JRDesignCellContents();
nodataCell.setBackcolor(Color.LIGHT_GRAY);
nodataCell.setMode(MODE_OPAQUE);
final JRDesignFrame frame = new JRDesignFrame();
frame.copyBox(new TravelReportLineBox(nodataCell));
nodataCell.setBox(frame);
LOG.debug("<crosstabHeaderCell/>");
crosstab.setHeaderCell(nodataCell);
final JRDesignCrosstabRowGroup rowGroup = new JRDesignCrosstabRowGroup();
final JRDesignCellContents rowHeader = new JRDesignCellContents();
final JRDesignCellContents rowTotalHeader = new JRDesignCellContents();
rowHeader.setMode(MODE_OPAQUE);
rowHeader.setBackcolor(Color.LIGHT_GRAY);
final JRDesignFrame rowFrame = new JRDesignFrame();
rowFrame.copyBox(new TravelReportLineBox(rowHeader));
rowHeader.setBox(rowFrame);
final JRDesignStaticText rowTotalText = h3("Daily Total").toStaticText();
addDesignElementTo(rowTotalHeader, rowTotalText, 0, 0, CELL_WIDTH, CELL_HEIGHT);
final JRDesignTextField rowHeaderField = normal("$V{Expenses}").toTextField();
addDesignElementTo(rowHeader, rowHeaderField, 0, 0, CT_HEADER_WIDTH, CELL_HEIGHT);
rowGroup.setName("Expenses");
rowGroup.setWidth(CT_HEADER_WIDTH);
rowGroup.setHeader(rowHeader);
rowGroup.setTotalHeader(rowTotalHeader);
rowGroup.setBucket(bucket("$F{name}", String.class));
final JRDesignCrosstabColumnGroup columnGroup = new JRDesignCrosstabColumnGroup();
final JRDesignCellContents columnHeader = new JRDesignCellContents();
final JRDesignCellContents columnTotalHeader = new JRDesignCellContents();
columnHeader.setMode(MODE_OPAQUE);
columnHeader.setBackcolor(Color.LIGHT_GRAY);
final JRDesignFrame columnFrame = new JRDesignFrame();
columnFrame.copyBox(new TravelReportLineBox(columnHeader));
columnHeader.setBox(columnFrame);
final JRDesignStaticText columnTotalText = h3("Expense Totals").toStaticText();
addDesignElementTo(columnTotalHeader, columnTotalText, 0, 0, CELL_WIDTH, CELL_HEIGHT);
final JRDesignTextField columnHeaderField = normal("$V{Days}").toTextField();
addDesignElementTo(columnHeader, columnHeaderField, 0, 0, CELL_WIDTH, CELL_HEIGHT);
columnGroup.setName("Days");
columnGroup.setHeight(CELL_HEIGHT);
columnGroup.setHeader(columnHeader);
columnGroup.setTotalHeader(columnTotalHeader);
columnGroup.setBucket(bucket("$F{date}", java.lang.String.class));
LOG.debug("<rowGroup name=\"Expenses\" width=\"400\">");
crosstab.addRowGroup(rowGroup);
crosstab.addColumnGroup(columnGroup);
crosstab.addMeasure(measure("amount"));
crosstab.addCell(crosstabCell(java.math.BigDecimal.class, "$V{amountMeasure}"));
crosstab.addCell(crosstabCell(java.math.BigDecimal.class, "$V{amountMeasure}", "Expenses"));
crosstab.addCell(crosstabCell(java.math.BigDecimal.class, "$V{amountMeasure}", null, "Days"));
crosstab.addCell(crosstabCell(java.math.BigDecimal.class, "$V{amountMeasure}", "Expenses", "Days"));
return crosstab;
}
/**
* Creates a crosstab. Intended for a subreport because this assumes the main dataset.
* @return a {@link JRDesignCrosstab} instance
*/
protected JRDesignCrosstab createCrosstab() throws Exception {
final JRDesignCrosstab crosstab = new JRDesignCrosstab();
crosstab.setRunDirection(RUN_DIRECTION_LTR);
crosstab.setWidth(595);
crosstab.setHeight(0);
final JRDesignCellContents nodataCell = new JRDesignCellContents();
nodataCell.setBackcolor(Color.LIGHT_GRAY);
nodataCell.setMode(MODE_OPAQUE);
final JRDesignFrame frame = new JRDesignFrame();
frame.copyBox(new TravelReportLineBox(nodataCell));
nodataCell.setBox(frame);
final JRDesignCrosstabRowGroup rowGroup = new JRDesignCrosstabRowGroup();
final JRDesignCellContents rowHeader = new JRDesignCellContents();
final JRDesignCellContents rowTotalHeader = new JRDesignCellContents();
rowHeader.setMode(MODE_OPAQUE);
rowHeader.setBackcolor(Color.LIGHT_GRAY);
final JRDesignFrame rowFrame = new JRDesignFrame();
rowFrame.copyBox(new TravelReportLineBox(rowHeader));
rowHeader.setBox(rowFrame);
final JRDesignStaticText rowTotalText = h3("Daily Total").toStaticText();
addDesignElementTo(rowTotalHeader, rowTotalText, 0, 0, CELL_WIDTH, CELL_HEIGHT);
final JRDesignTextField rowHeaderField = normal("$V{Expenses}").toTextField();
addDesignElementTo(rowHeader, rowHeaderField, 0, 0, CT_HEADER_WIDTH, CELL_HEIGHT);
rowGroup.setPosition(POSITION_X_LEFT);
rowGroup.setName("Expenses");
rowGroup.setWidth(CT_HEADER_WIDTH);
rowGroup.setHeader(rowHeader);
rowGroup.setTotalHeader(rowTotalHeader);
rowGroup.setBucket(bucket("$F{name}", String.class));
final JRDesignCrosstabColumnGroup columnGroup = new JRDesignCrosstabColumnGroup();
final JRDesignCellContents columnHeader = new JRDesignCellContents();
final JRDesignCellContents columnTotalHeader = new JRDesignCellContents();
columnHeader.setMode(MODE_OPAQUE);
columnHeader.setBackcolor(Color.LIGHT_GRAY);
final JRDesignFrame columnFrame = new JRDesignFrame();
columnFrame.copyBox(new TravelReportLineBox(columnHeader));
final JRDesignStaticText columnTotalText = h3("Expense Totals").toStaticText();
addDesignElementTo(columnTotalHeader, columnTotalText, 0, 0, CELL_WIDTH, CELL_HEIGHT);
final JRDesignTextField columnHeaderField = normal("$V{Days}").toTextField();
addDesignElementTo(columnHeader, columnHeaderField, 0, 0, CELL_WIDTH, CELL_HEIGHT);
columnGroup.setPosition(POSITION_Y_TOP);
columnGroup.setName("Days");
columnGroup.setHeight(CELL_HEIGHT);
columnGroup.setHeader(columnHeader);
columnGroup.setTotalHeader(columnTotalHeader);
columnGroup.setBucket(bucket("$F{date}", java.lang.String.class));
crosstab.addRowGroup(rowGroup);
crosstab.addColumnGroup(columnGroup);
crosstab.addMeasure(measure("amount"));
crosstab.addCell(crosstabCell(java.math.BigDecimal.class, "$V{amountMeasure}"));
crosstab.addCell(crosstabCell(java.math.BigDecimal.class, "$V{amountMeasure}", "Expenses"));
crosstab.addCell(crosstabCell(java.math.BigDecimal.class, "$V{amountMeasure}", null, "Days"));
crosstab.addCell(crosstabCell(java.math.BigDecimal.class, "$V{amountMeasure}", "Expenses", "Days"));
return crosstab;
}
/**
* Tells us if a given {@link ReportInfo} instance defines any crosstabs in its
* class definition
*
* @param report is a {@link ReportInfo} instance
*/
protected boolean hasCrosstabs(final ReportInfo report) {
for (final Field field : report.getClass().getDeclaredFields()) {
if (isCrosstab(field)) {
return true;
}
}
return false;
}
protected boolean hasFieldWithAnnotations(final ReportInfo report, final Class ... annotations) {
return hasFieldWithAnnotations(report.getClass(), annotations);
}
protected boolean hasFieldWithAnnotations(final Class reportClass, final Class ... annotations) {
for (final Field field : reportClass.getDeclaredFields()) {
if (hasAnnotations(field, annotations)) {
return true;
}
}
return false;
}
@Override
public Field getFieldWithAnnotation(final ReportInfo report, final Class ... annotations) {
return getFieldsWithAnnotation(report.getClass(), annotations).iterator().next();
}
protected Field getFieldWithAnnotation(final Class searchClass, final Class ... annotations) {
return getFieldsWithAnnotation(searchClass, annotations).iterator().next();
}
protected Collection<Field> getFieldsWithAnnotation(final Class searchClass, final Class ... annotations) {
final Collection<Field> retval = new ArrayList<Field>();
for (final Field field : searchClass.getDeclaredFields()) {
if (hasAnnotations(field, annotations)) {
retval.add(field);
}
}
return retval;
}
protected boolean hasAnnotations(final Field field, final Class ... annotations) {
for (final Class annotation : annotations) {
LOG.debug("Checking if field "+ field.getName()+ " has annotation "+ annotation.getSimpleName());
if (field.getAnnotation(annotation) == null) {
return false;
}
}
return true;
}
/**
* Determine whether a {@link Field} in the {@link ReportInfo} instance is a report {@link Subreport} or not.
*
* @param field a {@link Field} instance in a {@link ReportInfo} class
* @param true if the {@link Subreport} annotation is on a {@link Field}
*/
protected boolean isSubreport(final Field field) {
return field.getAnnotation(SubReport.class) != null;
}
/**
* Determine whether a {@link Field} in the {@link ReportInfo} instance is a report {@link Summary} or not.
*
* @param field a {@link Field} instance in a {@link ReportInfo} class
* @param true if the {@link Summary} annotation is on a {@link Field}
*/
@Override
public boolean isSummary(final Field field) {
return field.getAnnotation(Summary.class) != null;
}
/**
* Determine whether a {@link Field} in the {@link ReportInfo} instance is a report {@link Crosstab} or not.
*
* @param field a {@link Field} instance in a {@link ReportInfo} class
* @param true if the {@link Crosstab} annotation is on a {@link Field}
*/
protected boolean isCrosstab(final Field field) {
for (final Annotation annotation : field.getDeclaredAnnotations()) {
if (annotation instanceof Crosstab) {
return true;
}
}
return false;
}
/**
* Determine whether a {@link Field} in the {@link ReportInfo} instance is a report {@link Group} or not.
*
* @param field a {@link Field} instance in a {@link ReportInfo} class
* @param true if the {@link Group} annotation is on a {@link Field}
*/
protected boolean isGroup(final Field field) {
for (final Annotation annotation : field.getDeclaredAnnotations()) {
if (annotation instanceof Group) {
return true;
}
}
return false;
}
/**
* Determine whether a {@link Field} in the {@link ReportInfo} instance is a report {@link Parameter} or not.
*
* @param field a {@link Field} instance in a {@link ReportInfo} class
* @param true if the {@link Parameter} annotation is on a {@link Field}
*/
protected boolean isParameter(final Field field) {
for (final Annotation annotation : field.getDeclaredAnnotations()) {
if (annotation instanceof Parameter) {
return true;
}
}
return false;
}
/**
* Gets the styles property.
* @return Returns the styles.
*/
public Map<String,RTextStyle> getStyles() {
return styles;
}
/**
* Sets the styles property value.
* @param styles The styles to set.
*/
public void setStyles(final Map<String,RTextStyle> styles) {
this.styles = styles;
}
private static class TravelReportLineBox extends JRBaseLineBox {
protected TravelReportLineBox(JRBoxContainer container) {
super(container);
final JRBaseBoxPen pen = new JRBaseBoxPen(this) {
@Override
public Color getLineColor() {
return BLACK;
}
@Override
public Byte getLineStyle() {
return LINE_STYLE_SOLID;
}
@Override
public Float getLineWidth() {
return 0.5f;
}
};
this.pen = pen;
}
}
}