AppFuseJasperReports |
|
Your trail: |
About this Tutorial
This tutorial will show you how to use JasperReports in appfuse based applications. As an example, a report listing all registered users will be created.
Table of Contents
- Configuring JasperReports on AppFuse
- Designing Reports
- SpringMVC integration
- JSF integration
Configuring JasperReports on AppFuse
Below are the steps to (AppFuseAddLibrary) set up JasperReports on AppFuse. For that, we will include the lastest release version of JasperReports, which in the hour of this writing was 1.2.0. JasperReports itself depends on the following projects:
- BeanShell
- Commons BeanUtils - it comes with AppFuse
- Commons Collections - it comes with AppFuse
- Commons Digester - it comes with AppFuse
- Commons Logging - it comes with AppFuse
- iText
- POI
1. Create a new folder for your library in /lib using the pattern libname-version (e.g. /lib/iText-1.1).
/jasper-reports-1.2.0
/itext-1.1.4
/poi-2.5.1
/bsh-2.0b2
2. Add all required .jar's into the folder you created. (e.g. /lib/iText-1.1/iText.jar)
/lib/jasper-reports-1.2.0/jasperreports-1.2.0.jar
/lib/jasper-reports-1.2.0/jr-bsh-compiler.jar
/lib/itext-1.1.4/itext-1.1.4.jar
/lib/poi-2.5.1/poi-2.5.1.jar
/lib/bsh-2.0b2/bsh-2.0b2.jar
3. Edit /lib/lib.properties to include a section like the following for your library.
#
# JasperReports - http://jasperreports.sourceforge.net/
#
jasperreports.version=1.2.0
jasperreports.dir=${lib.dir}/jasper-reports-${jasperreports.version}
jasperreports.jar=${jasperreports.dir}/jasperreports-${jasperreports.version}.jar
#
# BeanShell - http://www.beanshell.org
#
bsh.version=2.0b2
bsh.dir=${lib.dir}/bsh-${bsh.version}
bsh.jar=${bsh.dir}/bsh-${bsh.version}.jar
#
# Poi - http://jakarta.apache.org/poi/index.html
#
poi.version=2.5.1
poi.dir=${lib.dir}/poi-${poi.version}
poi.jar=${poi.dir}/poi-${poi.version}.jar
# Itext - http://www.lowagie.com/iText/
#
itext.version=1.1.4
itext.dir=${lib.dir}/itext-${itext.version}
itext.jar=${itext.dir}/itext-${itext.version}.jar
4. Add library to the necessary classpaths in /properties.xml.
<path id="web.compile.classpath">
...
<fileset dir="${jasperreports.dir}" includes="*.jar"/>
<pathelement location="${itext.jar}"/>
<pathelement location="${poi.jar}"/>
</path>
5. The last step is making sure the library gets added to your deployed webapp. In the package-web target of /build.xml look for the element and add another element.
<target name="package-web" depends="compile-web,jsp-2">
...
<lib dir="${jasperreports.dir}" includes="*.jar"/>
<lib file="${itext.jar}"/>
<lib file="${poi.jar}"/>
</war>
</target>
Designing Reports
The amused part arrived! With the aid of the designing tool iReports, we will construct a report based on the list of users of the AppFuse. We will use the lastest release of iReports, which in the hour of this writing was 1.2.0. It will not be a complex report, only a list of accounts of users with the following columns:
- UserName
- Name
- Country
- Province
- City
See the result of designing time:
And execution time:
SpringMVC integration
Create the UserReportController Controller
One of the forms to call a report is to implement a new controller. We go to use one of the great characteristic that Spring MVC possess, the possibility of implement/extend one of existing controllers - one in special, MultiActionController. For that we will create the UserReportController controller, which will extends from MultiActionController controller:
public class UserReportController extends MultiActionController {
private final Log log = LogFactory.getLog(UserReportController.class);
private UserManager userManager = null;
public void setUserManager(UserManager mgr) {
this.userManager = mgr;
}
public ModelAndView userReport(HttpServletRequest request, HttpServletResponse response)throws Exception {
log.debug("entering 'userReport' method...");
Map model = new HashMap();
//It is the path of our webapp - is there a better way to do this?
String reportResourcePath = getServletContext().getRealPath("/");
String format = request.getParameter("format");
//Default format to pdf
if (StringUtils.hasText(format)){
if (!(format.equalsIgnoreCase("pdf") || format.equalsIgnoreCase("html")
|| format.equalsIgnoreCase("csv") || format.equalsIgnoreCase("xls"))){
format = "pdf";
}
}else{
format = "pdf";
}
model.put("format", format);
model.put("WEBDIR", reportResourcePath);
model.put("dataSource", getUserManager().getUsers(new User()));
return new ModelAndView("userMultiFormatReport", model);
}
}
|
MultiActionController resolves method names based on a method name resolver. The default method name resolver is InternalPathMethodNameResolver, which resolves method names based on URL patterns. But Spring comes with two other method name resolvers:
- ParameterMethodNameResolver—Resolves the execution method name based on a parameter in the request
- PropertiesMethodNameResolver—Resolves the name of the execution method by consulting a list of key/value pairs
Regardless of which method name resolver you choose, you’ll need to wire it into the methodNameResolver property of the MultiActionController to override the default. In the configuration file action-servlet.xml, makes the following modifications - we will use the first method to decide names:
‹bean id="methodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver"›
‹property name="paramName"›
‹value›action‹/value›
‹/property›
‹/bean›
‹bean id="userReportController" class="org.appfuse.webapp.action.UserReportController"›
‹property name="methodNameResolver" ref="methodNameResolver"/›
‹property name="userManager" ref="userManager"/›
‹/bean›
|
The requests would be of the following form: /appfuse/userReport.html?action=userReport and thus for ahead, for each method that you add in the class userReportController. Another thing that we do not have to forget is to establish which URL will use to call this controller. As this URL is one of the administrative tasks, we will go to place this mapping in the Bean adminUrlMapping:
‹property name="mappings"›
‹props›
‹prop key="/activeUsers.html"›filenameController‹/prop›
‹prop key="/users.html"›userController‹/prop›
‹prop key="/userReport.html"›userReportController‹/prop›
‹prop key="/flushCache.html"›filenameController‹/prop›
‹prop key="/reload.html"›reloadController‹/prop›
‹/props›
‹/property›
|
Now the most important, let's verify if everything is correct through the execution of the test of unit for this class. We already know that the current test will fail! This because we do not create our report and therefore we do not define who will resolve the name of the JasperReports's report file, userList.jrxml. Then, in our test class UserReportControllerTest, we have to test the existence of view userReport with format pdf:
public class UserReportControllerTest extends BaseControllerTestCase {
private UserReportController c;
private MockHttpServletRequest request;
private ModelAndView mav;
protected void setUp() throws Exception {
// needed to initialize a user
super.setUp();
c = (UserReportController) ctx.getBean("userReportController");
}
protected void tearDown() {
c = null;
}
public void testUserReportDefalutPdf() throws Exception {
mav = c.userReport(new MockHttpServletRequest(),
(HttpServletResponse) null);
Map m = mav.getModel();
assertEquals("Default format is pdf",m.get("format").toString(),"pdf");
assertEquals(mav.getViewName(), "userMultiFormatReport");
}
}
|
Run ant test-web -Dtestcase=UserReportController.
Putting all together
Finally we will configure the jrxml file and the resolver for pdf, xsl, csv and HTML formats.
1. Create a new folder for your reports in /web/WEB-INF with the name reports.
2. Add the files in the format jrxml in the directory that you created.
/WEB-INF/reports/userList.jrxml
3. Edit /WEB-INF/action-servlet.xml file to include the following section for the resolver and view, that we will use. You should have noted that in the UserReportController' method, we're using one parameter to define the format of the report's result at runtime. That is, we will use JasperReportsMultiFormatView view. First the resolver:
<!-- If a JasperReports view requires more complex configuration then use the BeanNameViewResolver to map a given view name to a given view bean -->
<bean id="nameViewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
|
And then the view:
<bean id="userMultiFormatReport" class="org.springframework.web.servlet.view.jasperreports.JasperReportsMultiFormatView">
<property name="contentDispositionMappings">
<props>
<prop key="html">inline; filename=report.html</prop>
<prop key="csv">inline; filename=report.csv</prop>
<prop key="pdf">inline; filename=report.pdf</prop>
<prop key="xls">inline; filename=report.xls</prop>
</props>
</property>
<property name="url" value="/WEB-INF/reports/userList.jrxml" />
</bean>
|
Run ant test-web -Dtestcase=UserReportController again. Make the deploy and access the url /appfuse/userReport.html?action=userReport&format=pdf. AppFuse rocks!
JSF integration
Add a JSF bean to your faces-config.xml
This bean should be in request scope and have the userManager injected to be able to get the list from the database:
<managed-bean>
<managed-bean-name>reports</managed-bean-name>
<managed-bean-class>org.appfuse.webapp.action.Reports</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>userManager</property-name>
<value>#{userManager}</value>
</managed-property>
</managed-bean>
|
JSF Bean implementation
Depending on requirements we can implement a different behaviour for the bean when the user clicks the report link:
- The report is saved on the server file system for further download.
- The report is opened directly on the client side without being saved (regenerated every time!).
Here is an implementation for the "saving file" behaviour:
package org.appfuse.webapp.action;
import ...
public class Reports extends BasePage implements Serializable {
private String format = "pdf";
private String WebDir = null;
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
public String getWebDir() {
return WebDir;
}
// used only in during junit test
public void setWebDir(String webDir) {
WebDir = webDir;
}
public String users() {
List results = userManager.getUsers(null);
Map parameters = new HashMap();
String ctxPath = getServletContext().getRealPath("/");
parameters.put("format", "pdf");
if (WebDir != null){
parameters.put("WEBDIR", WebDir);
}
try{
// prepare report and data
InputStream is = getServletContext().getResourceAsStream("/WEB-INF/reports/userList.jrxml");
JRBeanCollectionDataSource ds = new JRBeanCollectionDataSource(results);
// generate pdf file
JasperDesign jasperDesign = JRXmlLoader.load(is);
JasperReport jasperReport = JasperCompileManager.compileReport(jasperDesign);
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters,
ds);
JasperExportManager.exportReportToPdfFile(jasperPrint, ctxPath + "/resources/userList.pdf");
addMessage("errors.detail", "file created: " + ctxPath + "/resources/userList.pdf");
}
catch(Exception e){
log.error(e);
}
return null;
}
}
|
It is pretty difficult to unit test this class correctly and the only thing I could think off was checking that the file is actually generated. Be sure to update the hard coded value in the report to load images from the correct location during the test.
package org.appfuse.webapp.action;
import java.io.File;
public class ReportsTest extends BasePageTestCase {
private Reports bean;
protected void setUp() throws Exception {
super.setUp();
bean = (Reports) getManagedBean("reports");
// to use hard coded value in report
bean.setWebDir(null);
}
public void testSearch() throws Exception {
String ctxPath = bean.getServletContext().getRealPath("/");
// ensures resources directory is present
File f = new File(ctxPath + "/resources");
f.mkdir();
// generates report
assertNull(bean.users());
// checks the file is present
f = new File(ctxPath + "/resources/userList.pdf");
assertTrue(f.exists());
assertFalse(bean.hasErrors());
}
}
|
Here is the implementation for the "open in browser" behaviour:
package org.appfuse.webapp.action;
import ...
public class Reports extends BasePage implements Serializable {
public String users() {
List results = userManager.getUsers(null);
Map parameters = new HashMap();
parameters.put("format", "pdf");
parameters.put("WEBDIR", getServletContext().getRealPath("/"));
try{
InputStream is = getServletContext()
.getResourceAsStream("/WEB-INF/reports/userList.jrxml");
JRBeanCollectionDataSource ds = new JRBeanCollectionDataSource(results);
getResponse().setContentType("application/pdf");
getResponse().addHeader("Content-Disposition", "attachment; filename=userList.pdf");
JasperDesign jasperDesign = JRXmlLoader.load(is);
JasperReport jasperReport = JasperCompileManager.compileReport(jasperDesign);
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, ds);
JasperExportManager.exportReportToPdfStream(jasperPrint, getResponse().getOutputStream());
getFacesContext().getApplication().getStateManager()
.saveSerializedView(getFacesContext());
getFacesContext().responseComplete();
}
catch(Exception e){
log.error(e);
}
return null;
}
}
|
I have no unit test for this class, if you have one let us know.
Add a link to generate the report on the jsp page
It is as simple as :
<h:commandLink value="get pdf report" action="#{reports.users}"/>
|
Attachments:
|