Create Great Reports with JasperReports

Devdatta Kane
Share

JasperReports_Logo

Ruby on Rails is an amazing framework for building web applications. The Ruby and Rails ecosystems are very active and mature, with many libraries for solving the various problems encountered by developers. But one particular area where I feel Ruby lacks options is reporting. Many solutions exist in Rails for generating reports, most of them rely on generating HTML and converting to PDF. This approach has its advantages, but falls apart for complex and pixel perfect reports.

Enter JasperReports. JasperReports is a very popular open source reporting library widely used in Java world. Many of you may be curious about how to use a Java library in Rails. There are many approaches, but the one I prefer is to use JRuby. This requires you to convert your application to JRuby, which is quite simple to do these days. So, let’s get started.

Sample Rails Application

We will create a sample Rails application with a contact model and CRUD actions. The application will use JRuby, Rails 4.2, and SQLite as database to keep things simple. First of all, install JRuby with RVM or rbenv.

Switch to JRuby and bundle install the latest Rails gem. Now create a new Rails application, like so:

rails new contactbook

After the application is generated, create a Contact scaffold, which has amodel and CRUD resource operations:

cd contactbook
rails g scaffold Contact name:string address:string city:string phone:string email:string

Now migrate the database:

rake db:migrate

Let’s check how its working:

rails s

Point your favorite browser to http://localhost:3000/contacts and make sure everything is working properly. Create some records so we can use those later.

A Sample JasperReport

Let’s create a simple report that shows list of contacts. Download the JasperSoft Studio suitable to your platform from the Jaspersoft Studio site. Install and run Jaspersoft Studio. It is full fledged IDE based on Eclipse, specifically to design JasperReports. It has a learning curve, for sure, but we will not get into those details for now. You can refer to Jaspersoft Studio – Resources for more information.

Create a report named contacts using Jaspersoft Studio which should look as follows:

jaspersoft-studio

Compile the report and paste the contacts.jasper file into app/reports/ in our Rails application.

Integrating JasperReports

Now comes the important part, integrating JasperReports into the Rails application. We will be using JasperReports 6.1 version for this tutorial. Head over to the JasperReports Library page and download jasperreports-6.1.0-project.tar.gz. This archive contains the JasperReports JAR files in its dist folder, along with dependencies in lib folder. Copy all the files from both of those folders and put them in a folder named jasperreports under the lib directory in our Rails application.

contactbook
  - app
  ...
  - lib
    - jasperreports
      - jasperreports-6.1.0.jar
      ...

We have all required files for JasperReports in place, so let’s create a class to integrate JasperReports. Create a file called jasper_report.rb in lib directory with following code:

Dir.entries("#{Rails.root}/lib/jasperreports").each do |lib|
  require "jasperreports/#{lib}" if lib =~ /\.jar$/
end

require 'java'

java_import Java::net::sf::jasperreports::engine::JasperFillManager
java_import Java::net::sf::jasperreports::engine::JasperExportManager
java_import Java::net.sf.jasperreports.engine.JRResultSetDataSource

class JasperReport
  DIR = "#{Rails.root}/app/reports"

  def initialize(report, query, params = nil)
    @model = report
    @report_params = params
    @conn = ActiveRecord::Base.connection.jdbc_connection
    @query = query
  end

  def to_pdf
    stmt = @conn.create_statement
    @result = JRResultSetDataSource.new(stmt.execute_query(@query))
    report_source = "#{DIR}/#{@model}.jasper"
    raise ArgumentError, "#@model does not exist." unless File.exist?(report_source)
    params = {}
    params.merge!(@report_params) if @report_params.present?
    fill = JasperFillManager.fill_report(report_source, params, @result)
    pdf = JasperExportManager.export_report_to_pdf(fill)
    return String.from_java_bytes(pdf)
  end
end

Let’s see what the code actually does. First we require all JasperReports files into our class, like so:

Dir.entries("#{Rails.root}/lib/jasperreports").each do |lib|
  require "jasperreports/#{lib}" if lib =~ /\.jar$/
end

Add the Java import declarations for JasperReports:

require 'java'

java_import Java::net::sf::jasperreports::engine::JasperFillManager
java_import Java::net::sf::jasperreports::engine::JasperExportManager
java_import Java::net.sf.jasperreports.engine.JRResultSetDataSource

Define the location where all reports will be stored:

DIR = "#{Rails.root}/app/reports"

Add the initialization code in the class constructor:

def initialize(report, query, params = nil)
    @model = report
    @report_params = params
    @conn = ActiveRecord::Base.connection.jdbc_connection
    @query = query
end

As a first step, we initialized all required variables:

  • The report’s file name – report
  • The report’s SQL query – query
  • Any optional parameters to pass on to the report – params

@conn is a JDBC connection from the ActiveRecord connection pool, since JasperReports requires a JDBC connection to execute the query.

Finally, we have added a method to fill and export the report to PDF format:

def to_pdf
  stmt = @conn.create_statement
  @result = JRResultSetDataSource.new(stmt.execute_query(@query))
  report_source = "#{DIR}/#{@model}.jasper"
  raise ArgumentError, "#@model does not exist." unless File.exist?(report_source)
  params = {}
  params.merge!(@report_params) if @report_params.present?
  fill = JasperFillManager.fill_report(report_source, params, @result)
  pdf = JasperExportManager.export_report_to_pdf(fill)
  return String.from_java_bytes(pdf)
end

The first item is a JDBC statement, stmt = @conn.create_statement, which returns a JDBC ResultSet by executing @result = JRResultSetDataSource.new(stmt.execute_query(@query)). The report (.jasper) file created earlier using JasperStudio is set to the report_source. After that, invoke the JasperFillManager.fill_report to fill the report with the provided ResultSet and parameters. Lastly, invoke JasperExportManager.export_report_to_pdf to export the report into a PDF bytestream which is returned using String.from_java_bytes.

We now have the report as a PDF bytestream, but we’ve no way to send it to user, yet. So, add a small helper method in application_controller.rb as follows:

def respond_to_report(name, query, filename, download = false, report_params = nil)
  @report = JasperReport.new(name, query, report_params) 
  disposition = (download.nil? || download == false) ? 'inline' : 'attachment'
  send_data @report.to_pdf, :filename => filename, :type => :pdf, :disposition => disposition
end

This helper method simplifies the report invocation and sends the response back to the user. There are also added options for providing a filename, along with a disposition option to open the file within the browser or download it as an attachment.

Now, we will to add a action to the ContactsController that calls the report. Add the following code to app/controllers/contacts_controller.rb:

def report
  respond_to_report('contacts', 'select * from contacts', 'contacts.pdf')
end

As we have no parameters to pass and want to open the report in browser, we have skipped the download and report_params from the method call.

Update routes.rb to add the new action:

resources :contacts do
  get :report, on: :collection
end

Add a link for the report in the contacts/index.html.erb view:

...
<h1>Listing Contacts</h1>

<%= link_to 'Download as PDF', report_contacts_path %>
...

Fire up the server and go to http://localhost:3000/contacts. Click on ‘Download as PDF’. You should now see a PDF file with all contacts listed.

final-report

Wrapping Up

This was a quick primer on how to integrate JasperReports with Rails to create reports without much effort. There is much more that can be done with JasperReports and Rails, such as search criteria, complex reports, subreports, etc. But that’s for another tutorial.

I hope you enjoyed this post. Your comments and insights are always welcome.

CSS Master, 3rd Edition