Ruby on Rails
HowtoIntegrateJasperReports (Version #95)

JasperReports

Acknowledgement

This is an update from someone who posted this very valuable information and I absolutely want to give all credits to this person, that I regrettably don’t know.
The original information however was becoming a little bit out of date and I tried to update it, and add some things I found usefulll in setting up the integration between Ruby on Rails and JasperReports.
I’ve left the original text after my own text, so you can still read through the original text for additional information.
I’m not a native English speaker, so please excuse me if I made some errors against the English language. Feel free to correct my mistakes.

FlexRails.

Introduction

JasperReports is a powerful—and even more important—well known open source Java reporting tool that has the ability to deliver rich content in formats such as PDF, RTF, HTML, CSV and XML. It is widely used and appreciated in the Java community because of its flexibility and the availability of various GUI tools for rapid report design.
For this example I will be using iReport.

The following paragraphs explain how to interface Rails and JasperReports.
The interface I’m describing on this page is based on a command line interface between rails and JasperReports. There is also a possibility to interface using webservices, but this is not in my scope yet.

Prerequisites

  • You have a working rails installation and a working rails application, running on your localhost on port 3000.
    I will refer to you rails application directory like this : <RAILSAPP>
  • You have iReports installed and working on your development machine. This implies of course also a suitable java environment for iReport. I will refer to your iReport installation directory like this : <IREPORT>

Steps

These are the steps to succes (hopefully)
  • Create a xml data source in rails
  • Create a report in iReport using the rails data source
  • Installing a JasperReports ‘runtime’ in your <RAILSAPP>
  • Do some rails magic
  • Running your report from rails and returning a JasperReports generated pdf to the webbrowser.

Creating the rails data source

The data we will pass from rails to JasperReports will be in XML. XML is a standard datasource for JasperReports and rails is actually very good at generating XML data.

I have an Accounting controller. In this Accounting controller I created the following procedure to create a list of customers. I’ll show you the controller code, and the accompanying view.
The controller code : (<RAILSAPP>/app/controllers/accounting_controller.rb)

def customer_list
      @customers=Customer.find(:all)
end

The view code :
(<RAILSAPP>/app/views/accounting/customer_list.rxml)
xml.instruct!
xml.customer_list_result do
    xml.invoice_customers do
        @customers.each do |customer|
            xml << customer.to_xml(:dasherize=>false,:skip_instruct=>true,:only=>[:id,:name,:city],:root=>"customer")
        end
    end
end

As you can see I fooled around a little bit with the rails to_xml method. You can keep it simpler off course but it is worth investigating the possibilities of to_xml.
Don’t you love how much you can do with a few lines of Ruby on Rails.

Now start your browser and goto http://localhost:3000/accounting/customer_list.
The xml result is now shown in your browser, and depending on your browser the output is in a pretty outline or not.
Anyway, goto the source of the page you are looking at. Every browser has this possibility, and you should now see your complete XML document. Copy-Paste it into your favorite text editor and save it somewhere on your disk. We will use this xml file for developing the report. Later on we will of course switch to a live rails data source and you can delete this file.
My XML file looks like this

<?xml version="1.0" encoding="UTF-8"?>
<customer_list_result>
  <invoice_customers>
    <customer>
      <id type="integer">711</id>
      <name>3D-Design</name>
      <city>Bigtown</city>
    </customer>
    <customer>
      <id type="integer">496</id>
      <name>A Nuino</name>
      <city>Smalltown</city>
    </customer>
    <customer>
      <id type="integer">368</id>
      <name>International Inc.</name>
      <city>Expensivetown</city>
    </customer>
  </invoice_customers>
</customer_list_result>

Create a report in iReport

  • Start your iReport and create a new Report.
  • Choose menu : Data—> Connections / DataSources
    Click the new button, select ‘XML file data source’ from the list and click the ‘Next’ button.
    In this next window, give your XML datasource a name, browse and select the XML file from the previous step, and choose ‘Use the report XPath expression when filling the report’. Depending on the data in the report you might want to tinker with date settings and stuff. Finally save this datasource.
  • Choose menu : Data—> Set active connection and select the data source you just created.
  • Choose menu : Data—> Report query.
    Select xpath2 in the ‘query language’ drop down and enter you xpath expression. For my example this is :
    /customer_list_result/invoice_customers/customer
    You can probably figure out why if you look at my XML.
    Next drag the xml field nodes for the fields you will need on your report from the xml structure on the right, into the fields table at the bottom of this screen.
  • Now you can use iReport to lay out a beautifull report. This is however not an iReport tutorial so you are on your own here.
  • Where do you save your reports ? Well I save them in <RAILSAPP>/app/reports which I created for my reports. You will see that for every report JasperReports creates 3 files : .jrxml file which contains the definition for you report, a .java file which is the java code that was generated from the report definition, and a compliled .jasper file which is actually what you will need for running your reports. I’ve saved my report as custrep.jrxml.
  • If you see a pretty output in iReport you could (and should) now switch to the live rails data source like this.
    Choose menu : Data—> Connections / DataSources.
    Click the new button, select ‘Remote XML file data source’ from the list and click the Next button.
    In this next window give it a name and enter the datasource like this :
    http://localhost:3000/accounting/customer_list. That’s right, the exact same url you used before. Now save this data source and set it as the active data connection (choose menu : Data—> set active connection).
    iReport now calls your rails action to retrieve it’s data. Neat, right.
    But we still have to call the report from our Rails application

Install the JasperReports ‘runtime’

JasperReports is no more than a Java library that offers the ability to fill previously designed and compiled reports with some data and to save these reports in a specified file format.
I have installed this library in <RAILSAPP>/app/jasper.
If you put the runtime in this jasper directory and the reports you create in the reports directory, like I showed before.
The complete JasperReports will be deployed to your production server, just by deploying the <RAILSAPP>/app on the production server. There is nothing else to do on the production server. That’s the way I like it.
Okay, let’s go :
  • create the following directories :
    <RAILSAPP>/app/jasper/lib and
    <RAILSAPP>/app/jasper/bin
  • Copy the following files from <IREPORT>/lib to <RAILSAPP>/app/jasper/lib
    • commons-beanutils-1.7.jar
    • commons-collections-2.1.jar
    • commons-logging-1.0.2.jar
    • itext-1.3.1.jar
    • jasperreports-2.0.4.jar
    • jcommon-1.0.0.jar
    • jdt-compiler.jar
    • jfreechart-1.0.0.jar
    • log4j-1.2.13.jar
    • poi-3.0.1-FINAL-20070705.jar
    • xalan.jar
iReports has great support for adding barcodes to your reports. If you plan on using barcodes then you’ll also need these two jar files:
  • iReport.jar (7.6 MB!)
  • barbecue-1.5-beta1.jar

Actually, copying all files from <IREPORT>/lib to <RAILSAPP>/app/jasper/lib, is even better, because then all the features from iReport are available to your rails application.

It’s very well possible that for some files you will see other version numbers but the main thing is that you have to copy these files from your iReport lib into you rails app jasper lib. The reports you have created with your version have to stay in sync with the libraries from your iReport installation.

  • Download this
    • Copy only the file XmlJasperInterface.class from this download to <RAILSAPP>/app/jasper/bin

Your JasperReports runtime is now installed.

The rails magic

With the JasperReports runtime installed we need to add some rails code to make things work.
  • Create a model called ‘Document’ in your rails application and paste in this code :
    class Document
      include Config
      def self.generate_report(xml_data, report_design, output_type, select_criteria)
        report_design << '.jasper' if !report_design.match(/\.jasper$/)
        interface_classpath=Dir.getwd+"/app/jasper/bin" 
        case CONFIG['host']
          when /mswin32/
            mode = "w+b" #windows requires binary mode
            Dir.foreach(Dir.getwd+"/app/jasper/lib") do |file|
              interface_classpath << ";#{Dir.getwd}/app/jasper/lib/"+file if (file != '.' and file != '..' and file.match(/.jar/))
            end
          else
            mode = "w+" 
            Dir.foreach(Dir.getwd+"/app/jasper/lib") do |file|
              interface_classpath << ":#{Dir.getwd}/app/jasper/lib/"+file if (file != '.' and file != '..' and file.match(/.jar/))
            end
        end
            result=nil
            IO.popen "java -Djava.awt.headless=true -cp \"#{interface_classpath}\" XmlJasperInterface -o#{output_type} -f#{Dir.getwd}/app/reports/#{report_design} -x#{select_criteria}", mode do |pipe|
                pipe.write xml_data
                pipe.close_write
                result = pipe.read
                pipe.close
            end
        return result
      end
    end
    
  • Create a new file : <RAILSAPP>/app/helpers/send_doc_helper.rb and paste in the following code :
    module SendDocHelper
      protected
      def cache_hack
        if request.env['HTTP_USER_AGENT'] =~ /msie/i
          headers['Pragma'] = ''
          headers['Cache-Control'] = ''
        else
          headers['Pragma'] = 'no-cache'
          headers['Cache-Control'] = 'no-cache, must-revalidate'
        end
      end
    
      def send_doc(xml, xml_start_path, report, filename, output_type = 'pdf')
        case output_type
        when 'rtf'
          extension = 'rtf'
          mime_type = 'application/rtf'
          jasper_type = 'rtf'
        else # pdf
          extension = 'pdf'
          mime_type = 'application/pdf'
          jasper_type = 'pdf'
        end
    
        cache_hack
        send_data Document.generate_report(xml, report, jasper_type, xml_start_path),
            :filename => "#{filename}.#{extension}", :type => mime_type, :disposition => 'inline'
      end
    end
    

There you have it. The rails magic. Don’t ask to many questions.
One important thing to notice however, is that there are no calls to shell scripts or bat files. Rails makes a direct call to Java and communicates with the Java process through pipes. Although there is a little bit of performance loss in starting the Java process, the pipes based communication is very fast.

Finally ! Running the report.

To run the report add this to a controller where you want to add an action for running the report :

helper :send_doc
include SendDocHelper

def customer_report
  @customers=Customer.find(:all)
  send_doc(
    render_to_string(:template => 'accounting/customer_list', :layout => false),
     '/customer_list_result/invoice_customers/customer', 
    'custrep',
    'CustomerReport', 
    'pdf')
end

The parameters to the send_doc method are :
  1. the <RAILSAPP>/app/views/accounting/customer_list.rxml
  2. the xpath expression you used in iReport
  3. the name of the report as you saved it in <RAILSAPP>/app/reports
  4. the suggested name that your users will see when they save your pdf from their browsers
  5. the fact that you want to send the report as a pdf to your users.

Now open your browser again and go to :
http://localhost:3000/accounting/customer_report and a nice pdf should be displayed in your browser.

That’s it. I wish you all lots of success.

Manually Invoking Report

Invoking Jasper through a Ruby Java call does not always generate meaningful error messages.

To assist with resolving configuration issues, it may be helpful to manually invoke the call to Jasper from your command line.

This can be achieved using the following (or similar):

java -Djava.awt.headless=true -cp "C:/project/app/jasper/bin;C:/project/app/jasper/lib/commons-logging-1.0.2.jar;C:/project/app/jasper/lib/itext-1.3.1.jar;......" XmlJasperInterface -opdf -fC:/project/app/reports/CustomerReport.jasper -x/customer_list_result/invoice_customers/customer < customer.xml > custrep.pdf

THE ORIGINAL TEXT

Following you will find the original text before I started to mess around with it. It probably still contains usefull information for you to read/

Interface

JasperReports is no more than a Java library that offers the ability to fill previously designed and compiled reports with some data and to save these reports in a specified file format. Valid data sources are JDBC connections and XML files (using JRXmlDatasource), amongst others. Reading the data for the report from a database using raw SQL commands is of course possible but introduces duplicate logic, database vendor-specific code and concurrency issues. Besides that, there is no elegant way of reporting data that is not stored in a database but managed in Rails. There are no such problems if we use the XML data source and generate XML data in Rails using Builder templates—which is extremely easy and comfortable. For these reasons, the XML data source is used for feeding Rails data into JasperReports.

JasperReports is integrated into Rails by execution of a simple Java application that receives the XML data for the reports through an IO pipe, creates the report and passes the results back trough the pipe right into the Rails application.

The whole interface consists of the following parts:
  • JasperReports library
  • Java interface application that takes XML data and produces reports
  • Shell script to invoke the Java application
  • Rails code that builds an appropriate CLASSPATH, executes the Java interface application, and reads/writes through an IO pipe
  • Rails code that generates the XML data (using Builder templates)
  • Some rails code that sends the generated report to the browser or stores it somewhere

This might seem like a lot of work but actually it is not. At least, if you don’t have to write all the code yourself ;).

Code

I added a directory jasper to my rails application root. It contains three subfolders
  • bin – the Java class file of the interface application
  • src – the Java source file of the interface application
  • lib – the JasperReports library and every 3rd-party library that is needed

Apart from that, I added a reports directory to the rails application root that contains the precompiled Jasper reports.

The rails code that builds the CLASSPATH, executes the Java app and handles the IO pipe looks like the following:

# app/models/document.rb
class Document
  include Config
  def self.generate_report(xml_data, report_design, output_type, select_criteria)
    report_design << '.jasper' if !report_design.match(/\.jasper$/)
    interface_classpath=Dir.getwd+"/jasper/bin" 
    case CONFIG['host']
      when /mswin32/
        Dir.foreach("jasper/lib") do |file|
          interface_classpath << ";#{Dir.getwd}/jasper/lib/"+file if (file != '.' and file != '..' and file.match(/.jar/))
        end
      else
        Dir.foreach("jasper/lib") do |file|
          interface_classpath << ":#{Dir.getwd}/jasper/lib/"+file if (file != '.' and file != '..' and file.match(/.jar/))
        end
    end
    pipe = IO.popen "java -Djava.awt.headless=true -cp \"#{interface_classpath}\" XmlJasperInterface -o#{output_type} -freports/#{report_design} -x#{select_criteria}", "w+b" 
    pipe.write xml_data
    pipe.close_write
    result = pipe.read
    pipe.close
    result
  end
end

As you can see from this code, there is no need to call any Windows batch file or Bash script. It has not been thoroughly tested but works in Windows and Linux (check that ‘java’ binary path though). Three parameters are passed to XmlJasperInterface:
  • -o the report output type (supported types are pdf, rtf, xml, xls and csv; even though I only really tested pdf and rtf)
  • -f the path and filename of the precompiled report
  • -x an XML select path (refer to Using the XML datasource for further information)

The data returned by generate_report is the final report that can be passed to a browser for example. Include the following module in every controller that needs to create and send reports:

# app/helpers/send_doc_helper.rb

module SendDoc
  protected
  def cache_hack
    if @request.env['HTTP_USER_AGENT'] =~ /msie/i
      @headers['Pragma'] = ''
      @headers['Cache-Control'] = ''
    else
      @headers['Pragma'] = 'no-cache'
      @headers['Cache-Control'] = 'no-cache, must-revalidate'
    end
  end

  def send_doc(xml, xml_start_path, report, filename, output_type = 'pdf')
    case output_type
    when 'rtf'
      extension = 'rtf'
      mime_type = 'application/rtf'
      jasper_type = 'rtf'
    else # pdf
      extension = 'pdf'
      mime_type = 'application/pdf'
      jasper_type = 'pdf'
    end

    cache_hack
    send_data Document.generate_report(xml, report, jasper_type, xml_start_path),
        :filename => "#{filename}.#{extension}", :type => mime_type, :disposition => 'inline'
  end
end

The controller code could be as follows:

# app/controllers/report_controller.rb
class ReportController < ApplicationController
  model :document
  helper :send_doc
  include SendDoc

  def report
    @course = Course.find(params[:id])
    @user = User.find(session[:user_id])
    send_doc(render_to_string(:template => 'xml_course_report', :layout => false),
      '/Course/Information/Participants/Participant', 'course_report', "course#{@course.id}", 'pdf')
  end

end

In the above code we render the Builder template xml_course_report into a string and pass this string to the send_doc method. course_report is the filename of the precompiled report and course#{@course.id} is the filename of the PDF document that gets sent to the browser. The extension .pdf is added automatically in send_doc.

The Builder template (app/views/xml_course_report.rxml) could look as follows:

xml.instruct!
xml.Course do
  xml.MetaInformation do
    xml.GeneratedAt(Time.now)
    xml.GeneratedBy do
      xml.Name(@user.name)
      xml.UserName(@user.user_name)
      xml.Email(@user.email)
    end
  end
  xml.Information do
    xml.Name(@course.name)
    xml.Participants do
      @course.participants.each do |p|
        xml.Participant do
          xml.Name(p.name)
          xml.Phone(p.phone)
          xml.City(:zip => p.city.zip, :name => p.city.name)
        end
      end
    end
  end
end

VoilĂ , that’s it.

Performance Issues

JasperReports itself is very fast, but bringing up a Java Virtual Machine for each call of generate_doc takes its time (about 2 seconds on my machine). It would be much faster, if the Java interface application would run as a server application all the time and listening on a specific TCP port for incoming report generation requests. Then, the performance issue would be gone. I haven’t found the time to implement a JasperReport server yet. But, if somebody does, please let me know!

I think an option for better performance would be to use Apache Cocoon and call this pages from ruby. This way one could either use Apache FOP and XML/XSLT to generate PDF pages or integrate JasperReports or Eclipse BIRT into Cocoon. When Cocoon is then executed as Tomcat-Webapp the startuptime is nearly null. (markusw@nmmn.com)

What is quite interesting concerning the usage of JasperReports ist Jasper Intelligence, from http://jasperintel.sourceforge.net. They provide JasperReports as a Webservice.

RE:JasperReports as a Webservice. Although their website advertises support for PHP, Python and other scripting languages, this is not entirely true. Scouring through the Net as well as JasperSoft’s documentation does not yield any useful information about the server’s Web Service API. A quick look at their forums reveals that this isn’t fully implemented yet. A workaround to this would probably be to use WWW::Mechanize to get those document formats from the JasperServer. —relaxdiego

RE:RE:JasperReports as a Webservice.

There is a product available for this, and it’s called JasperIntelligence. Although the API is pretty much opaque, I was able to get a rails app to fetch PDF exports of the sample reports provided with JI via the web service. I have instructions on how it can be done in a basic way on the JasperIntelligence forum: http://www.jasperforge.org/index.php?option=com_joomlaboard&Itemid=215&func=view&id=15846&catid=10
I am definitely interested in more attention on this subject, since there don’t appear to be any enterprise class reporting solutions available for rails, and Crystal Reports is well outside of my means ($18k+ per year). +-MariusAgricola

RE:RE:RE Jasper Reports as a webservice

I’m using the following setup:

Jetty server (java) providing a jython interface to jasper reports
You call the jasper jetty service from rails like so :

options = { :c_sql_where=>@grid.search_arg_sql(params) || ‘1=1’, :c_sql_order=>@grid.order_arg_sql(params) , :outputType=>params[:outputType], :compiledDesign=>‘customer_equipments’, } send_jasper_export(:mysql,options,’equipments’)

send_jasper_export is a helper method that does the actual call by :

response = Net::HTTP.post_form(URI.parse(‘http://jasper:8080/jasapp/jasper.py’),arguments)

This works great and the performance is very good.

You can build the report with ireport and use a xml databasource or a sql datasource. All the c_ arguments are customer arguments that are passed as parameters to the report.

If there is interest i could write a bigger tutorial, contact me at daniel[AT]itxl.nl

Jasper Reports with a JDBC datasource.

I’m looking into getting this working using Jasper reports compiled to use a JDBC data source. If anyone has already done this or can see a trivial way to do it then please share here.

Download

You can download the Java interface application, the shell script as well as the JasperReports library 1.1.0 (and the required 3rd-party libraries) bundled from this location.
Notice however, that this Wiki-Page gets updated by people around the world whereas the code you can download here does not and there might be differences between the code shown above and the one you download. For example, I originally used a shell script between Rails and Java, but apparently somebody updated this Wiki-Page with a much more elegant approach that calls the Java Virtual Machine directly from Rails.

Including the JasperReport source file for this sample data would make this much easier to understand. —It is included now.

JasperReports

Acknowledgement

This is an update from someone who posted this very valuable information and I absolutely want to give all credits to this person, that I regrettably don’t know.
The original information however was becoming a little bit out of date and I tried to update it, and add some things I found usefulll in setting up the integration between Ruby on Rails and JasperReports.
I’ve left the original text after my own text, so you can still read through the original text for additional information.
I’m not a native English speaker, so please excuse me if I made some errors against the English language. Feel free to correct my mistakes.

FlexRails.

Introduction

JasperReports is a powerful—and even more important—well known open source Java reporting tool that has the ability to deliver rich content in formats such as PDF, RTF, HTML, CSV and XML. It is widely used and appreciated in the Java community because of its flexibility and the availability of various GUI tools for rapid report design.
For this example I will be using iReport.

The following paragraphs explain how to interface Rails and JasperReports.
The interface I’m describing on this page is based on a command line interface between rails and JasperReports. There is also a possibility to interface using webservices, but this is not in my scope yet.

Prerequisites

  • You have a working rails installation and a working rails application, running on your localhost on port 3000.
    I will refer to you rails application directory like this : <RAILSAPP>
  • You have iReports installed and working on your development machine. This implies of course also a suitable java environment for iReport. I will refer to your iReport installation directory like this : <IREPORT>

Steps

These are the steps to succes (hopefully)
  • Create a xml data source in rails
  • Create a report in iReport using the rails data source
  • Installing a JasperReports ‘runtime’ in your <RAILSAPP>
  • Do some rails magic
  • Running your report from rails and returning a JasperReports generated pdf to the webbrowser.

Creating the rails data source

The data we will pass from rails to JasperReports will be in XML. XML is a standard datasource for JasperReports and rails is actually very good at generating XML data.

I have an Accounting controller. In this Accounting controller I created the following procedure to create a list of customers. I’ll show you the controller code, and the accompanying view.
The controller code : (<RAILSAPP>/app/controllers/accounting_controller.rb)

def customer_list
      @customers=Customer.find(:all)
end

The view code :
(<RAILSAPP>/app/views/accounting/customer_list.rxml)
xml.instruct!
xml.customer_list_result do
    xml.invoice_customers do
        @customers.each do |customer|
            xml << customer.to_xml(:dasherize=>false,:skip_instruct=>true,:only=>[:id,:name,:city],:root=>"customer")
        end
    end
end

As you can see I fooled around a little bit with the rails to_xml method. You can keep it simpler off course but it is worth investigating the possibilities of to_xml.
Don’t you love how much you can do with a few lines of Ruby on Rails.

Now start your browser and goto http://localhost:3000/accounting/customer_list.
The xml result is now shown in your browser, and depending on your browser the output is in a pretty outline or not.
Anyway, goto the source of the page you are looking at. Every browser has this possibility, and you should now see your complete XML document. Copy-Paste it into your favorite text editor and save it somewhere on your disk. We will use this xml file for developing the report. Later on we will of course switch to a live rails data source and you can delete this file.
My XML file looks like this

<?xml version="1.0" encoding="UTF-8"?>
<customer_list_result>
  <invoice_customers>
    <customer>
      <id type="integer">711</id>
      <name>3D-Design</name>
      <city>Bigtown</city>
    </customer>
    <customer>
      <id type="integer">496</id>
      <name>A Nuino</name>
      <city>Smalltown</city>
    </customer>
    <customer>
      <id type="integer">368</id>
      <name>International Inc.</name>
      <city>Expensivetown</city>
    </customer>
  </invoice_customers>
</customer_list_result>

Create a report in iReport

  • Start your iReport and create a new Report.
  • Choose menu : Data—> Connections / DataSources
    Click the new button, select ‘XML file data source’ from the list and click the ‘Next’ button.
    In this next window, give your XML datasource a name, browse and select the XML file from the previous step, and choose ‘Use the report XPath expression when filling the report’. Depending on the data in the report you might want to tinker with date settings and stuff. Finally save this datasource.
  • Choose menu : Data—> Set active connection and select the data source you just created.
  • Choose menu : Data—> Report query.
    Select xpath2 in the ‘query language’ drop down and enter you xpath expression. For my example this is :
    /customer_list_result/invoice_customers/customer
    You can probably figure out why if you look at my XML.
    Next drag the xml field nodes for the fields you will need on your report from the xml structure on the right, into the fields table at the bottom of this screen.
  • Now you can use iReport to lay out a beautifull report. This is however not an iReport tutorial so you are on your own here.
  • Where do you save your reports ? Well I save them in <RAILSAPP>/app/reports which I created for my reports. You will see that for every report JasperReports creates 3 files : .jrxml file which contains the definition for you report, a .java file which is the java code that was generated from the report definition, and a compliled .jasper file which is actually what you will need for running your reports. I’ve saved my report as custrep.jrxml.
  • If you see a pretty output in iReport you could (and should) now switch to the live rails data source like this.
    Choose menu : Data—> Connections / DataSources.
    Click the new button, select ‘Remote XML file data source’ from the list and click the Next button.
    In this next window give it a name and enter the datasource like this :
    http://localhost:3000/accounting/customer_list. That’s right, the exact same url you used before. Now save this data source and set it as the active data connection (choose menu : Data—> set active connection).
    iReport now calls your rails action to retrieve it’s data. Neat, right.
    But we still have to call the report from our Rails application

Install the JasperReports ‘runtime’

JasperReports is no more than a Java library that offers the ability to fill previously designed and compiled reports with some data and to save these reports in a specified file format.
I have installed this library in <RAILSAPP>/app/jasper.
If you put the runtime in this jasper directory and the reports you create in the reports directory, like I showed before.
The complete JasperReports will be deployed to your production server, just by deploying the <RAILSAPP>/app on the production server. There is nothing else to do on the production server. That’s the way I like it.
Okay, let’s go :
  • create the following directories :
    <RAILSAPP>/app/jasper/lib and
    <RAILSAPP>/app/jasper/bin
  • Copy the following files from <IREPORT>/lib to <RAILSAPP>/app/jasper/lib
    • commons-beanutils-1.7.jar
    • commons-collections-2.1.jar
    • commons-logging-1.0.2.jar
    • itext-1.3.1.jar
    • jasperreports-2.0.4.jar
    • jcommon-1.0.0.jar
    • jdt-compiler.jar
    • jfreechart-1.0.0.jar
    • log4j-1.2.13.jar
    • poi-3.0.1-FINAL-20070705.jar
    • xalan.jar
iReports has great support for adding barcodes to your reports. If you plan on using barcodes then you’ll also need these two jar files:
  • iReport.jar (7.6 MB!)
  • barbecue-1.5-beta1.jar

Actually, copying all files from <IREPORT>/lib to <RAILSAPP>/app/jasper/lib, is even better, because then all the features from iReport are available to your rails application.

It’s very well possible that for some files you will see other version numbers but the main thing is that you have to copy these files from your iReport lib into you rails app jasper lib. The reports you have created with your version have to stay in sync with the libraries from your iReport installation.

  • Download this
    • Copy only the file XmlJasperInterface.class from this download to <RAILSAPP>/app/jasper/bin

Your JasperReports runtime is now installed.

The rails magic

With the JasperReports runtime installed we need to add some rails code to make things work.
  • Create a model called ‘Document’ in your rails application and paste in this code :
    class Document
      include Config
      def self.generate_report(xml_data, report_design, output_type, select_criteria)
        report_design << '.jasper' if !report_design.match(/\.jasper$/)
        interface_classpath=Dir.getwd+"/app/jasper/bin" 
        case CONFIG['host']
          when /mswin32/
            mode = "w+b" #windows requires binary mode
            Dir.foreach(Dir.getwd+"/app/jasper/lib") do |file|
              interface_classpath << ";#{Dir.getwd}/app/jasper/lib/"+file if (file != '.' and file != '..' and file.match(/.jar/))
            end
          else
            mode = "w+" 
            Dir.foreach(Dir.getwd+"/app/jasper/lib") do |file|
              interface_classpath << ":#{Dir.getwd}/app/jasper/lib/"+file if (file != '.' and file != '..' and file.match(/.jar/))
            end
        end
            result=nil
            IO.popen "java -Djava.awt.headless=true -cp \"#{interface_classpath}\" XmlJasperInterface -o#{output_type} -f#{Dir.getwd}/app/reports/#{report_design} -x#{select_criteria}", mode do |pipe|
                pipe.write xml_data
                pipe.close_write
                result = pipe.read
                pipe.close
            end
        return result
      end
    end
    
  • Create a new file : <RAILSAPP>/app/helpers/send_doc_helper.rb and paste in the following code :
    module SendDocHelper
      protected
      def cache_hack
        if request.env['HTTP_USER_AGENT'] =~ /msie/i
          headers['Pragma'] = ''
          headers['Cache-Control'] = ''
        else
          headers['Pragma'] = 'no-cache'
          headers['Cache-Control'] = 'no-cache, must-revalidate'
        end
      end
    
      def send_doc(xml, xml_start_path, report, filename, output_type = 'pdf')
        case output_type
        when 'rtf'
          extension = 'rtf'
          mime_type = 'application/rtf'
          jasper_type = 'rtf'
        else # pdf
          extension = 'pdf'
          mime_type = 'application/pdf'
          jasper_type = 'pdf'
        end
    
        cache_hack
        send_data Document.generate_report(xml, report, jasper_type, xml_start_path),
            :filename => "#{filename}.#{extension}", :type => mime_type, :disposition => 'inline'
      end
    end
    

There you have it. The rails magic. Don’t ask to many questions.
One important thing to notice however, is that there are no calls to shell scripts or bat files. Rails makes a direct call to Java and communicates with the Java process through pipes. Although there is a little bit of performance loss in starting the Java process, the pipes based communication is very fast.

Finally ! Running the report.

To run the report add this to a controller where you want to add an action for running the report :

helper :send_doc
include SendDocHelper

def customer_report
  @customers=Customer.find(:all)
  send_doc(
    render_to_string(:template => 'accounting/customer_list', :layout => false),
     '/customer_list_result/invoice_customers/customer', 
    'custrep',
    'CustomerReport', 
    'pdf')
end

The parameters to the send_doc method are :
  1. the <RAILSAPP>/app/views/accounting/customer_list.rxml
  2. the xpath expression you used in iReport
  3. the name of the report as you saved it in <RAILSAPP>/app/reports
  4. the suggested name that your users will see when they save your pdf from their browsers
  5. the fact that you want to send the report as a pdf to your users.

Now open your browser again and go to :
http://localhost:3000/accounting/customer_report and a nice pdf should be displayed in your browser.

That’s it. I wish you all lots of success.

Manually Invoking Report

Invoking Jasper through a Ruby Java call does not always generate meaningful error messages.

To assist with resolving configuration issues, it may be helpful to manually invoke the call to Jasper from your command line.

This can be achieved using the following (or similar):

java -Djava.awt.headless=true -cp "C:/project/app/jasper/bin;C:/project/app/jasper/lib/commons-logging-1.0.2.jar;C:/project/app/jasper/lib/itext-1.3.1.jar;......" XmlJasperInterface -opdf -fC:/project/app/reports/CustomerReport.jasper -x/customer_list_result/invoice_customers/customer < customer.xml > custrep.pdf

THE ORIGINAL TEXT

Following you will find the original text before I started to mess around with it. It probably still contains usefull information for you to read/

Interface

JasperReports is no more than a Java library that offers the ability to fill previously designed and compiled reports with some data and to save these reports in a specified file format. Valid data sources are JDBC connections and XML files (using JRXmlDatasource), amongst others. Reading the data for the report from a database using raw SQL commands is of course possible but introduces duplicate logic, database vendor-specific code and concurrency issues. Besides that, there is no elegant way of reporting data that is not stored in a database but managed in Rails. There are no such problems if we use the XML data source and generate XML data in Rails using Builder templates—which is extremely easy and comfortable. For these reasons, the XML data source is used for feeding Rails data into JasperReports.

JasperReports is integrated into Rails by execution of a simple Java application that receives the XML data for the reports through an IO pipe, creates the report and passes the results back trough the pipe right into the Rails application.

The whole interface consists of the following parts:
  • JasperReports library
  • Java interface application that takes XML data and produces reports
  • Shell script to invoke the Java application
  • Rails code that builds an appropriate CLASSPATH, executes the Java interface application, and reads/writes through an IO pipe
  • Rails code that generates the XML data (using Builder templates)
  • Some rails code that sends the generated report to the browser or stores it somewhere

This might seem like a lot of work but actually it is not. At least, if you don’t have to write all the code yourself ;).

Code

I added a directory jasper to my rails application root. It contains three subfolders
  • bin – the Java class file of the interface application
  • src – the Java source file of the interface application
  • lib – the JasperReports library and every 3rd-party library that is needed

Apart from that, I added a reports directory to the rails application root that contains the precompiled Jasper reports.

The rails code that builds the CLASSPATH, executes the Java app and handles the IO pipe looks like the following:

# app/models/document.rb
class Document
  include Config
  def self.generate_report(xml_data, report_design, output_type, select_criteria)
    report_design << '.jasper' if !report_design.match(/\.jasper$/)
    interface_classpath=Dir.getwd+"/jasper/bin" 
    case CONFIG['host']
      when /mswin32/
        Dir.foreach("jasper/lib") do |file|
          interface_classpath << ";#{Dir.getwd}/jasper/lib/"+file if (file != '.' and file != '..' and file.match(/.jar/))
        end
      else
        Dir.foreach("jasper/lib") do |file|
          interface_classpath << ":#{Dir.getwd}/jasper/lib/"+file if (file != '.' and file != '..' and file.match(/.jar/))
        end
    end
    pipe = IO.popen "java -Djava.awt.headless=true -cp \"#{interface_classpath}\" XmlJasperInterface -o#{output_type} -freports/#{report_design} -x#{select_criteria}", "w+b" 
    pipe.write xml_data
    pipe.close_write
    result = pipe.read
    pipe.close
    result
  end
end

As you can see from this code, there is no need to call any Windows batch file or Bash script. It has not been thoroughly tested but works in Windows and Linux (check that ‘java’ binary path though). Three parameters are passed to XmlJasperInterface:
  • -o the report output type (supported types are pdf, rtf, xml, xls and csv; even though I only really tested pdf and rtf)
  • -f the path and filename of the precompiled report
  • -x an XML select path (refer to Using the XML datasource for further information)

The data returned by generate_report is the final report that can be passed to a browser for example. Include the following module in every controller that needs to create and send reports:

# app/helpers/send_doc_helper.rb

module SendDoc
  protected
  def cache_hack
    if @request.env['HTTP_USER_AGENT'] =~ /msie/i
      @headers['Pragma'] = ''
      @headers['Cache-Control'] = ''
    else
      @headers['Pragma'] = 'no-cache'
      @headers['Cache-Control'] = 'no-cache, must-revalidate'
    end
  end

  def send_doc(xml, xml_start_path, report, filename, output_type = 'pdf')
    case output_type
    when 'rtf'
      extension = 'rtf'
      mime_type = 'application/rtf'
      jasper_type = 'rtf'
    else # pdf
      extension = 'pdf'
      mime_type = 'application/pdf'
      jasper_type = 'pdf'
    end

    cache_hack
    send_data Document.generate_report(xml, report, jasper_type, xml_start_path),
        :filename => "#{filename}.#{extension}", :type => mime_type, :disposition => 'inline'
  end
end

The controller code could be as follows:

# app/controllers/report_controller.rb
class ReportController < ApplicationController
  model :document
  helper :send_doc
  include SendDoc

  def report
    @course = Course.find(params[:id])
    @user = User.find(session[:user_id])
    send_doc(render_to_string(:template => 'xml_course_report', :layout => false),
      '/Course/Information/Participants/Participant', 'course_report', "course#{@course.id}", 'pdf')
  end

end

In the above code we render the Builder template xml_course_report into a string and pass this string to the send_doc method. course_report is the filename of the precompiled report and course#{@course.id} is the filename of the PDF document that gets sent to the browser. The extension .pdf is added automatically in send_doc.

The Builder template (app/views/xml_course_report.rxml) could look as follows:

xml.instruct!
xml.Course do
  xml.MetaInformation do
    xml.GeneratedAt(Time.now)
    xml.GeneratedBy do
      xml.Name(@user.name)
      xml.UserName(@user.user_name)
      xml.Email(@user.email)
    end
  end
  xml.Information do
    xml.Name(@course.name)
    xml.Participants do
      @course.participants.each do |p|
        xml.Participant do
          xml.Name(p.name)
          xml.Phone(p.phone)
          xml.City(:zip => p.city.zip, :name => p.city.name)
        end
      end
    end
  end
end

VoilĂ , that’s it.

Performance Issues

JasperReports itself is very fast, but bringing up a Java Virtual Machine for each call of generate_doc takes its time (about 2 seconds on my machine). It would be much faster, if the Java interface application would run as a server application all the time and listening on a specific TCP port for incoming report generation requests. Then, the performance issue would be gone. I haven’t found the time to implement a JasperReport server yet. But, if somebody does, please let me know!

I think an option for better performance would be to use Apache Cocoon and call this pages from ruby. This way one could either use Apache FOP and XML/XSLT to generate PDF pages or integrate JasperReports or Eclipse BIRT into Cocoon. When Cocoon is then executed as Tomcat-Webapp the startuptime is nearly null. (markusw@nmmn.com)

What is quite interesting concerning the usage of JasperReports ist Jasper Intelligence, from http://jasperintel.sourceforge.net. They provide JasperReports as a Webservice.

RE:JasperReports as a Webservice. Although their website advertises support for PHP, Python and other scripting languages, this is not entirely true. Scouring through the Net as well as JasperSoft’s documentation does not yield any useful information about the server’s Web Service API. A quick look at their forums reveals that this isn’t fully implemented yet. A workaround to this would probably be to use WWW::Mechanize to get those document formats from the JasperServer. —relaxdiego

RE:RE:JasperReports as a Webservice.

There is a product available for this, and it’s called JasperIntelligence. Although the API is pretty much opaque, I was able to get a rails app to fetch PDF exports of the sample reports provided with JI via the web service. I have instructions on how it can be done in a basic way on the JasperIntelligence forum: http://www.jasperforge.org/index.php?option=com_joomlaboard&Itemid=215&func=view&id=15846&catid=10
I am definitely interested in more attention on this subject, since there don’t appear to be any enterprise class reporting solutions available for rails, and Crystal Reports is well outside of my means ($18k+ per year). +-MariusAgricola

RE:RE:RE Jasper Reports as a webservice

I’m using the following setup:

Jetty server (java) providing a jython interface to jasper reports
You call the jasper jetty service from rails like so :

options = { :c_sql_where=>@grid.search_arg_sql(params) || ‘1=1’, :c_sql_order=>@grid.order_arg_sql(params) , :outputType=>params[:outputType], :compiledDesign=>‘customer_equipments’, } send_jasper_export(:mysql,options,’equipments’)

send_jasper_export is a helper method that does the actual call by :

response = Net::HTTP.post_form(URI.parse(‘http://jasper:8080/jasapp/jasper.py’),arguments)

This works great and the performance is very good.

You can build the report with ireport and use a xml databasource or a sql datasource. All the c_ arguments are customer arguments that are passed as parameters to the report.

If there is interest i could write a bigger tutorial, contact me at daniel[AT]itxl.nl

Jasper Reports with a JDBC datasource.

I’m looking into getting this working using Jasper reports compiled to use a JDBC data source. If anyone has already done this or can see a trivial way to do it then please share here.

Download

You can download the Java interface application, the shell script as well as the JasperReports library 1.1.0 (and the required 3rd-party libraries) bundled from this location.
Notice however, that this Wiki-Page gets updated by people around the world whereas the code you can download here does not and there might be differences between the code shown above and the one you download. For example, I originally used a shell script between Rails and Java, but apparently somebody updated this Wiki-Page with a much more elegant approach that calls the Java Virtual Machine directly from Rails.

Including the JasperReport source file for this sample data would make this much easier to understand. —It is included now.