Reporting in a Struts application...

December 2004

Jakarta Struts is today dominant in the arena of Java web applications. The Struts framework targets complex web apps to provide a strong layer of control over servlets, jsps, taglibs and other components of a typical Java web application.

Perhaps its strongest suit is the ability to scrape all the elements off a web page using an ActionForm and to present the data to a controller-like Action. There's no need to write scads of code that interrogate the HTTP request. Each web page features an ActionForm and an Action that handles data capture and navigation.

Normally, this is all wonderful stuff. But when the number of web pages starts to increase, the maintenance of multiple classes per web page can be daunting. Quite often, you will see a single web pages represented by an Action and ActionForm classes. For large applications, this structure can be cumbersome.

This is nowhere more apparent than when the the Struts programmer has to deal with a reporting-based application. A reporting application tends to be a unique beast. You typically start with a very few well known reporting solutions -- nothing more than a couple of pages -- and then the business starts to react with requests for more of the same. Over time, the application starts to grow with no end in sight.

This doesn't work particularly well with Struts, especially if you adhere to the one-page-one-ActionForm scenario. The cost of a new report becomes several days' effort rather than several hours. And pretty soon the application starts to show sign of bloat.

Solution...

The Reporting web application is often a series of almost identical pages, differiented only by its data. The display is normally a read-only HTML table. Input parameters are captured on pages that display a drop-down or two and a submit button. There is often a requirement to summarize the data or apply various function-like modules, like a record counter or a Total function for numeric fields.

This is really a scenario that requires an application design to utilize very few Action and ActionForm classes. As well, to maintain consistency of page format, a minimum of jsp code should be utilized. Where the Action is well-suited to defining navigation and the ActionForm is ideal as gathering and validating page data, the Reporting application needs a layer that reacts specificly to the report data.

The Struts Reporting Framework....

This article defines a design that allows a Struts application to capture data from multiple sources and to apply formatting rules that are fairly simple to specify and implement.

The basis of the design is that each report should be defined in an XML repository. Each report is represented by an XML file acting as a specification. Each file requires three sections: input, data and formatting specifications.

Features of the specification:

  • The data specification is design to accomodate multiple datasources: both jdbc and bean-based datasources are built-in and you can easily add others.
  • Each input --normally represented as a text box or a drop down list -- is defined in a simple 4 or 5 attribute xml structure.
  • Formatting consists mainly of sections where a series of columns and summary sections are defined.
  • Other features of the framework:

  • normally requires just one Action and Actionform with 2 jsps
  • formatting is driven almost entirely from Cascading Style Sheets(CSS)
  • handles input from lists, texts, hidden fields.
  • supports both multiple record views, but also single-record displays.
  • design is simple and is built for customization
  • features row banding, 5 built-in functions, summary section
  • Example specification file

    <Report>
       <reportTitle>Products by Invoice</reportTitle>
       <outputClass>
         com.javazoid.report.format.HtmlJdbcTableFormatter
       </outputClass>
    	
    	
       <inputSpecification>
          <inputs>
    	<input>
    	   <type>
    		com.javazoid.report.parameter.format.ListParameter
    	   </type>
    	   <invokeMethod>getInvoices</invokeMethod>
    	   <invokeClass>
    		com.javazoid.report.demo.Lookup
    	   </invokeClass>
    	   <label>Invoice</label>
    	   <name>invoiceId</name>
    	   <size>20</size>
    	   <validationClass>
    		com.javazoid.report.input.validate.MustBeNumeric
    	   </validationClass>
    	</input>
    
         </inputs>
       </inputSpecification>
    	
    	
       <dataSpecification>
          <type>jdbc</type>
    	<connectionClass>
    	    com.javazoid.report.demo.HsqlConnection
    	</connectionClass>
    	<connectionMethod>getConnection</connectionMethod>
    	<connectionMethodParameter/>	
    	<sql>
    	   select product.*, item.*  from product, item 
    	   where product.id=item.productid AND item.invoiceid=?
    	   order by product.name 
    	</sql>
    		
       </dataSpecification>
    	
    	
       <formatSpecification>
    	<tableStyle>table</tableStyle>
    
    
    	<rowStyles>
       	   <rowStyle>darkRow</rowStyle>	
    	   <rowStyle>lightRow</rowStyle>
    	</rowStyles>
    		
    	<columns>
    	   <column>
    		<title>Product Name</title>
    		<widthPercent>30</widthPercent>
    		<alignment>left</alignment>
    		<dataName>NAME</dataName>
    		<styleClass>highlight</styleClass>
    	   </column>
    	   <column>
    		<title>PRICE</title>
    		<widthPercent>20</widthPercent>
    		<alignment>right</alignment>
    		<dataName>PRICE</dataName>
    		<formatter>
    	  	   com.javazoid.report.cell.DecimalCellFormatter
    		</formatter>
    		<formatPattern>#,###.00</formatPattern>
    	   </column>
    	   <column>
    		<title>QUANTITY</title>
    		<widthPercent>25</widthPercent>
    		<alignment>right</alignment>
    		<dataName>QUANTITY</dataName>
    	   </column>
    	   <column>
    		<title>Cost</title>
    		<widthPercent>25</widthPercent>
    		<alignment>right</alignment>
    		<dataName>COST</dataName>
    		<formatterClass>
    	   	   com.javazoid.report.cell.DecimalCellFormatter
    		</formatterClass>
    		<formatPattern>#,###.00</formatPattern>	
    	   </column>
    	</columns>
    			
    	<summaries>
    	   <summary>
    		<summaryCaption>Total Costs</summaryCaption>
    		<summaryColumn>COST</summaryColumn>
    		<summaryFunction>
    	  	   com.javazoid.report.function.Total
    		</summaryFunction>
    		<summaryFormatPattern>
    		   $#,###.00
    		</summaryFormatPattern>	
    	   </summary>
    	</summaries>		
       </formatSpecification>
    
    </Report>
    

    Specification in detail

    Specifications are normally read-in once, during the first report access. Each report has one specification contained in an xml file. When this file is read, it is translated to a Java value object format when the specification build() method is invoked.

    The ReportSpecification class contains three elements that define all attributes required to render the report.

    Input

    The input section defines a series of components that will display components to capture inputs to the data layer. These components are currently geared towards Html display and include List, LongList, Text and Hidden inputs. These components honor a Parameter interface with methods like

    public String getLabel();
    public String getComponent();
    public void setValues(Object in);
    

    The getComponent() method returns a String which containing HTML markup for SELECTs or INPUTs. The INPUT style Parameters are, of course, easiest to render. Data capture is also pretty straightforward. However, for SELECT components, it is necessary for the XML specification to pass along a class and a method that can be invoked to obtain display data for the OPTION elements of the SELECT. The syntax of the XML is

    <inputSpecification>
          <inputs>
    	<input>
    	   <type>
    		com.javazoid.report.parameter.format.ListParameter
    	   </type>
    	   <invokeMethod>getInvoices</invokeMethod>
    	   <invokeClass>
    		com.javazoid.report.demo.Lookup
    	   </invokeClass>
    	   <label>Invoice</label>
    	   <name>invoiceId</name>
    	   <size>20</size>
    	   <validationClass>
    		com.javazoid.report.input.validate.MustBeNumeric
    	   </validationClass>
    	</input>
    
         </inputs>
       </inputSpecification>
    

    The Parameter component is rendered on a page called "genericinput.jsp" that iterates the list of Parameter objects, display its label and calls its get components for the appropriate html. In this way the generic input can be dynamically built from the xml specification. Each component will display a name, param and displayName attribute that will be captured from the "genericinput" jsp. Here's what each attribute holds:

  • name - the id of the component.
  • param - the actual data captured from user input
  • displayName - this is useful for Lists where the selected OPTION has an value and a displayValue. The displayValue is not useful for querying data, but may be needed to display back to the user.

    Data

    Data specifications indicate how the data is to be fetched within the application. Normally, most reporting applications will specify some sort of data-accessing class and method that will return either a stream-like structure like a JDBC ResultSet or a List of JavaBeans. AS much as possible, I have tried to permit report customizers the ability to define their own structure. However, to become completely flexible will require stepping away from having the framework return Specification objects.

    That said, there are currently two supported structures, jdbc and listBean. They are, of course, completely different structures, so I will include examples of both.

    JDBC
      <dataSpecification>
          <type>jdbc</type>
    	<connectionClass>
    	    com.javazoid.report.demo.HsqlConnection
    	</connectionClass>
    	<connectionMethod>getConnection</connectionMethod>
    	<connectionMethodParameter/>	
    	<sql>
    	   select product.*, item.*  from product, item 
    	   where product.id=item.productid AND item.invoiceid=?
    	   order by product.name 
    	</sql>
    		
       </dataSpecification>	
    
    

    In the above example, I am supposing that the application has a standard means of obtaining a JDBC Database connection, against which the SQL can be prepared.

    Bean Data Access
       <dataSpecification>
         	<type>listBean</type>
    	<accessorClass>
    	   com.javazoid.report.demo.CustomerAccessor
    	</accessorClass>
        	<accessorMethod>getCustomers</accessorMethod>
       </dataSpecification>	
    

    The assumption here is that the CustomorAccessor's getCustomers() method will return a java.util.List of JavaBean objects. You don't need to specify what the type of the objects in this List, but you do need to make sure that the type follows a JavaBean convention because each Column in the report will have a dataName like "getFirstName" against which this object will be queried using Java relection.

    Format

    The Format section of the specification xml contains elements that assist in the page layout. As much as possible, formatting is deferred to Cascading Style Sheets. The Html layout is specified by an Html TABLE tag and all its descendents. However, beyond those tags, most of the formatting is accomplished through the CSS speicification. Struts Reporting accomodate three layers of formatting, TABLE, ROW and CELL formatting -- all of which are optional.

  • tableStyle defines a class the CSS file and will generate a tag like
    <TABLE class="XXX">
    
  • rowStyles represent a collection of class that are attached to each HTML TR tag, like
    <TR class="xxx">
    
    The styles are applied sequentially so that indicating two styles will apply the first to row one and the second to row 2. This can accomplish a banded-look in this instance.

  • styleClass is an attribute of a Column object and is attached to each cell, i.e.,
    <TD class="XXX">
    
    Some of the attributes for a column can be specified outside the style sheet, but I have kept this to a relative minimum.

    Columns collection

       <columns>
          <column>
    	<title>Product Name</title>
    	<widthPercent>30</widthPercent>
    	<alignment>left</alignment>
    	<dataName>NAME</dataName>
    	<styleClass>highlight</styleClass>
          </column>
       </columns>
    

    The bulk of the attributes required for a layout is specified in the Columns collection, where each Column object is applied to the TD attribute of an Html layout. Here a list of the currently supported items:

  • title , the string that will appear at the top of the report.
  • dataName, the string that will permit the data fetching layer to identify an attribute. In Sql, this would be the columName. In JavaBean data, this would be the getter method.
  • widthPercent - since the table is defined as 100% of width (it doesn't have to be the full width of a page -- you can define that in you style sheet), you can allot width percentages for each column. Normally all columns should add up to 100%, I suppose...
  • alignment You can define this here RIGHT, LEFT or CENTER or you can omit this attribute and specify it in the style sheet.
  • formatterClass - I have defined an interface com.javazoid.report.cell which this attribute should honor. I have written a couple of these which tend to be Java wrapper around java.text.DecimalFormat and java.text.SimpleDateFormat. However, you should think about writing others. My NullCellFormatter will allow you to apply a pattern to a NULL value. For example, you could specifiy a "N/A" string to appear when a value is null.
  • formatterPattern - the pattern should provide some indication to the formatter about how a value should be transformed. An example, an Object representing a java.util.Date might have a pattern of "MM/dd/yyyy" which would translate a Date object into something like "12/30/2004". In the case of the NullCellFormatter, the pattern provides a substitution. You pass "N/A" as the pattern and when a value is null, that string will appear.
  • styleClass a string identifying the class for the TD attribute which will hopefully exist in you CSS file.

    Which of these attribute are required and which optional? The truth is that none of these are required, unless the run-time objects are written to require them. For this reason, I have not committed to a DTD or any definition of structure. My hope is that the Specification layer will become flexible enough to let developers add any attributes that their runtime Report Formatter objects. That type of flexibility will have to wait unfortunately because I've code the Specification layer to include JavaBeans that would themselves need to be subclassed or re-written to accomodate my goal of complete flexibility. Perhaps in a future release!

    Summaries

    	<summaries>
    	   <summary>
    		<summaryCaption>Total Costs</summaryCaption>
    		<summaryColumn>COST</summaryColumn>
    		<summaryFunction>
    	  	   com.javazoid.report.function.Total
    		</summaryFunction>
    		<summaryFormatPattern>
    		   $#,###.00
    		</summaryFormatPattern>	
    	   </summary>
    	</summaries>	
    

    Beyond Column definition, there are also Summaries, a collection of "summary" objects that represent a single HTML row in the layout. These are, of course, presented at the end of the report and include the following

  • summaryCaption a String stretched across all but the last column of the report. These are aligned to the right.
  • summaryColumn an attribute representing a Column data item. So if you have a String here that I can't find in a Column's dataName attribute, you are going to get an Exception. I understand that many report-end data items may not go against a particular column (for example, a Count function which is really counting rows...) but, for now it is required and must also match up with one Column's dataname attribute.
  • summaryFunction is a class honoring the com.javazoid.report.function.Function interface. These classes are meant to accept the data from all the row cells of a particular Column and to calculate something. Right now I have built in Average, Minimum, Maximum, Total and Count. But you could certainly build functions that operate against String or Date objects.
  • summaryPattern is applied against the Function's getFormattedValue() method. In the case of the built-in functions, I keep a java.text.DecimalFormat object internally that converts raw numbers like "12004.25" to Strings like "$12,004.25".

    A note and apology about consistency. Whereas all the Column attributes are optional, all the Summary attributes are required. I may change this...

    What happens at Runtime...

    I have devised the Runtime objects so that they must honor interface com.javazoid.report.format.Formatter, which defines a series of methods to pass the various specification elements and a format() method which should fire off all the work to fetch and transform the data.

    Most of the formatters currently supported are subclasses of HtmlFormatter, which proved the bulk of the code required to represent Titles, headers, data rows and report-end summaries. You'll find implementations of methods like these

    makeRow(StringBuffer sb)
    getCellStart(ColumnValues column, StringBuffer buffer) 
    printColumnHeaders()
    

    Unfortunately, the actual data that these methods will work on is unknown. It could be a JDBC ResultSet. It could be a JavaBean. To handle variety in the abstract superclass, I have required the subclasses to transfer their row data into a Map that contains the row data , keyed by the "dataName" attribute of a Column. The type of this data is java.lang.Object, but we assume that calling the object's toString() method will deliver legible data for the report. Where items are not possible to represent via the toString() method, I would recommend speicifying (...and maybe implementing) a CellFormatter object in the data's respective Column.formatterClass attribute).

    The implementation of a particular subclass should be relatively easy. For example, an Html ResultSet can be formatted via the HtmlJdbcTableFormatter. JavaBean is likewise translated to a Map object and passed along via the HtmlBeanTableFormatter. Finally, I've implemented a HtmlJdbcRowFormatter, which works on only one row in a ResultSet.

    How this works with the Struts Action

    The single Struts Action class we use handle all these reports. The particular formatter that is required to render the report is specified in the report XML and it remains only for the Action to instantiate that class and call its format() method.

    However, the Struts Action also needs to evaluate whether there is enough data present to even show the report. If the specification indicates that inputs of some sort are required and the parameters normally available from these are missing on the Struts ActionForm, the Action will forward to the "genericinput" page, which is designed to display generic input controls like SELECTs and text INPUTs . To do this, the action must instantiate a parameter builder that provides the code for the generic input page.

    When a user posts back to the Action from genericinput, the Action should recognize that the parameter list's data is available and the report can now be shown.

    How to install and configure...

    I recommend evaluating the framework by examining the example application. That should give you enough detail to determine how much customization you will need to support the framework in your Struts application.

    You will need to either start a brand new Struts app or modify an existing on. These are the instructions to get the frame up and running:

  • There are two archives.The strutsReporting jar contain the reporting engine only. This needs to go into your WEB-INF/lib folder. The other is strutsReportWebApp zip contains all the files you will need to edit or copy to your Struts application.
  • Edit your struts-config.xml with some of the contents of the strutsReportWebApp zip's struts-config. Mostly, you need to copy the form bean and action mapping definition to your own app.
  • Copy the jsps from the zip file to your WEB Content folder. You will probably need to edit the struts-config action mapping to reflect the new location.
  • Copy the report specification xml files from the zip to a folder in you web app. If you choose a path other than what is indicated in the zip, you may need to edit the the Struts Action source to relfect this. You can check the com.javazoid.report.struts.action.ReportAction's getSpecification() method. Currently, it really wants you to drop the file in a reportXml folder in your web content path.
  • Copy the report.css style sheet to a folder of your choosing, but edit the 4 jsps to reflect the new location. Eventually, you will want to thrash this style sheet and insert your own...
  • Data?If you want to use the sample database, check the instructions in the margins. HSQL is very lightweight and ideal for demos. Make sure the hsql.jar driver is in your WEB-INF/lib folder.
  • When this is all installed, invoke the Welcome.jsp which has the sample report menu.

    How to customize...

    I can speak to some experience with customizing the package to suite a client's requirement. The framework was flexible enough to take care of the bulk of the client's reporting needs, but there was certainly a need to customize things.

  • Subclass or rewrite the Acion class I found that the client wanted to put the specific data in the showReport.jsp and to maintain the xml specifications in the application classpath, rather than the web content path. I also had to subclass the ActionForm to support these new attributes.
  • User-defined attributes I discovered early on that it would be nice to have some user-defined attributes that could pass specification information back to the newly sub-classed Action class. This is now built into the xml readers and you can add similar attribute just under the document root.
    <Report>
       <userDefined>
          <downloadable>true</downloadable>
          <downloadFormatter>
    	  org.myorg.reports.MySubclassedCSVFormatter
          </downloadFormatter>
       </userDefined>
       ...
    
  • Customer formatters The client also had a unique method of fetching data, which didn't work with the data access specification, so I subclassed the HtmlJdbcTableFormatter formatter. This turned out to be quite easier than I suspected, since we only had to rewrite the format() method.
  • Some reports were weird. I should state that this is almost always the case. In one instance, the report required 3 calls to the data access layer to get all the data that went into the report. The only way to accomplish this is to write a Formatter-type class specific to the report. Again, it was mainly a need to add to the format() method and to add some additional methods to fetch the extra data.
  • Word of advice Before examing a path including a lot of formatter subclasses, I would advise quite a bit of analysis to understand if the framework can meet 70-80 per cent of the reports without writing extra code. If it can't maybe this is not the right package. In that case, maybe the correct method is look around for another design or even roll your own. In all, despite having to write custom subclasses, the framework was able to put handle quite a bit of the grunt work without writing very much Java (beyond some changes to the Action and ActionForm to support branding.)