15 March 2008

Bringing Jasper Reports, Rails and Rjb together

Whew, talk about difficult. On Windows Server 2003 anyway (business choice, not mine)

Firstly, Rjb (Ruby Java Bridge) requires, absolutely must have, the SDK not just the JRE.

Secondly, get the stuff working in Ruby before trying Rails.

This is what I did.

Install the Rjb Gem. Set JAVA_HOME. Watch out for space in "Program Files" in path, use PROGRAM~1 instead. I installed the SDK to C:\Java to avoid this.

Follow most of HowtoIntegrateJasperReports

I could not get the pipe IO stuff running on Windows - just locked up the server.
I then read anything found on Jasper and found references to PHP Java Bridge with Jasper here, this got me thinking could I use a Ruby Java Bridge to do something like the XmlJasperInterface.java source in Ruby and Rjb without the command line? Eventually. Yes!!

Here is the local ruby...
require 'rjb'

separator = Config::CONFIG['target_os'] =~ /win/ ? ';' : ':'
classpath = [
"./itext-1.3.1.jar",
"./commons-beanutils-1.7.jar",
"./commons-collections-2.1.jar",
"./commons-logging-1.0.2.jar",
"./jasperreports-2.0.4.jar",
"./jasperreports-extensions-1.3.1.jar",
"./jcommon-1.0.0.jar",
"./jdt-compiler-3.1.1.jar",
"./jfreechart-1.0.0.jar",
"./log4j-1.2.9.jar",
"./poi-3.0.1-FINAL-20070705.jar",
"./xalan.jar"
].join(separator)

# xmls is a string representing the @collection.to_xml when on rails

xmls = %Q¬<?xml version="1.0" encoding="UTF-8"?>
<customs-shipping-details>
<customs-shipping-detail>
... lots of attribute nodes
</customs-shipping-detail>
... more customs shipping detail records
</customs-shipping-details>¬

Rjb::load(classpath, ['-Xmx128m'])

j_jre = Rjb::import('net.sf.jasperreports.engine.JRException')
j_jem = Rjb::import('net.sf.jasperreports.engine.JasperExportManager')
j_jfm = Rjb::import('net.sf.jasperreports.engine.JasperFillManager')
j_jp = Rjb::import('net.sf.jasperreports.engine.JasperPrint')
j_jxds = Rjb::import('net.sf.jasperreports.engine.data.JRXmlDataSource')

j_sxis = Rjb::import('org.xml.sax.InputSource')
j_jrxu = Rjb::import('net.sf.jasperreports.engine.util.JRXmlUtils')
j_iosr = Rjb::import('java.io.StringReader')
j_map = Rjb::import('java.util.HashMap')
out = Rjb::import('java.lang.System').out

compiled_design_file_path = './templates/csd_1.jasper'
xpath_filter = '/customs-shipping-details/customs-shipping-detail'
pdf_path = './out102.pdf'
rmap = j_map.new()

xis = j_sxis.new()
xis.setCharacterStream(j_iosr.new(xmls))
jp = j_jfm.fillReport(compiled_design_file_path, rmap,
j_jxds.new_with_sig('Lorg.w3c.dom.Document;Ljava.lang.String;',
j_jrxu.parse(xis), xpath_filter))
j_jem.exportReportToPdfFile(jp,pdf_path)


The most frustrating bit was to get the JRXmlDataSource to accept String data and not stream data. The new_with_sig method of Rjb is not perfect; I could not invoke the ByteArrayInputStream constructor with a byte array using the getBytes method of a Java String and the '[B;' signature.

After hunting through the Jasper API I found that I could produce a org.w3c.dom.Document from the static parse method of the JRXmlUtils class. But the parse method needed a org.xml.sax.InputSource object.

Again, the Reader based constructor for the InputSource could not be invoked so I had to use the default and set the Reader source with the setCharacterStream method of the InputSource object. It was easy to use the StringReader object to wrap the XML data string before passing it to the setCharacterStream method.

Moving on to rails.
I created a folder under apps called reports with lib and templates subfolders. I copied the jasper templates into templates and the jar files into lib.

You need this in config/environment.rb ENV['JAVA_HOME'] = "C:\\Java\\jdk1.6.0_05", pointing to your java SDK. Obviously the Rjb gem on the Rails server too.

I created this module in the rails lib folder.

module Jasport
require 'rjb'

separator = Config::CONFIG['target_os'] =~ /win/ ? ';' : ':'
classpath = [
"#{RAILS_ROOT}/app/reports/lib/itext-1.3.1.jar",
"#{RAILS_ROOT}/app/reports/lib/commons-beanutils-1.7.jar",
"#{RAILS_ROOT}/app/reports/lib/commons-collections-2.1.jar",
"#{RAILS_ROOT}/app/reports/lib/commons-logging-1.0.2.jar",
"#{RAILS_ROOT}/app/reports/lib/jasperreports-2.0.4.jar",
"#{RAILS_ROOT}/app/reports/lib/jdt-compiler-3.1.1.jar",
"#{RAILS_ROOT}/app/reports/lib/jfreechart-1.0.0.jar",
"#{RAILS_ROOT}/app/reports/lib/log4j-1.2.9.jar",
#"#{RAILS_ROOT}/app/reports/lib/jasperreports-extensions-1.3.1.jar",
#"#{RAILS_ROOT}/app/reports/lib/jcommon-1.0.0.jar",
#"#{RAILS_ROOT}/app/reports/lib/poi-3.0.1-FINAL-20070705.jar",
"#{RAILS_ROOT}/app/reports/lib/xalan.jar"
].join(separator)

Rjb::load(classpath, ['-Xmx128m'])

class ReportGenerator

def return_pdf(compiled_design_name, xpath_filter, xml_data)
compiled_design_file_path = "#{RAILS_ROOT}/app/reports/templates/" + compiled_design_name + '.jasper'
@xis.setCharacterStream(@j_iosr.new(xml_data))
@jp = @j_jfm.fillReport(compiled_design_file_path, @rmap, @j_jxds.new_with_sig('Lorg.w3c.dom.Document;Ljava.lang.String;', @j_jrxu.parse(@xis), xpath_filter))
@j_jem.exportReportToPdf(@jp)
end

def initialize
#@j_jre = Rjb::import('net.sf.jasperreports.engine.JRException')
#@j_jp = Rjb::import('net.sf.jasperreports.engine.JasperPrint')

@j_jem = Rjb::import('net.sf.jasperreports.engine.JasperExportManager')
@j_jfm = Rjb::import('net.sf.jasperreports.engine.JasperFillManager')
@j_jxds = Rjb::import('net.sf.jasperreports.engine.data.JRXmlDataSource')
@j_sxis = Rjb::import('org.xml.sax.InputSource')
@j_jrxu = Rjb::import('net.sf.jasperreports.engine.util.JRXmlUtils')
@j_iosr = Rjb::import('java.io.StringReader')
@rmap = Rjb::import('java.util.HashMap').new
@xis = @j_sxis.new()
end
end
end

Note: I am assuming that the JVM is loaded once. I don't know about GC and the Rjb::import objects, it seems that they are ruby proxy stubs so I guess they will get garbage collected as normal. I tried the Rjb::unload method in the local trial but it seg faulted the ruby interpreter, I did not try it on Rails.

This technique opens up using other Java libraries.

5 comments:

Unknown said...

Hello Guy,

I really appreciate your post. I've been working on incorporating JasperReports with rails and the command line approach is just too slow.

I'm hoping that the RJB approach avoids having to start java each time a report is generated.

I've got the ruby version working. But I am having trouble incorporating it into Rails (I'm a bit new to Rails/Ruby).

Where do you place your Jasport model file? How do you call it from the Rails app?

Thanks,

Scotty

Rob Whitener said...

Is there a cleaner way to load jars into the classpath? right now I am also basically just building a long string of path names, but I wonder if there is a simpler way than this:

@path = File.dirname(__FILE__) +"/../lib/wmjsonclient.jar;"
@path = @path + File.dirname(__FILE__) + "/../lib/client.jar;"
@path = @path + File.dirname(__FILE__) + "/../lib/enttoolkit.jar"
Rjb::load(@path)

doesn't look like "Ruby" code.

Guy Boertje said...

@Rob
I guess you could try:

sep = Config::CONFIG['target_os'] =~ /win/ ? ';' : ':'
_fp = File.dirname(__FILE__) + "/../lib/"
@path = _fp
@path << [ "wmjsonclient.jar"
, "client.jar"
, "enttoolkit.jar"
].join(sep + _fp)
Rjb::load(@path)

Lee said...

Hi Guy,

Thank you for your post. It ultimately helped me to fix my problem. I too couldn't get Rjb to call a constructor that required a byte array. You were close when you called the new_with_sig method using '[B;'. It seems that when you use the ';' at the end, it is looking for a class name or interface. Taking off the ';' makes it work for me, i.e. ClassObject.new_with_sig('[B', byte_array).

Guy Boertje said...

@Lee,

Happy to be of help. I have long since moved on from the project that needed jasper reports, so I can't try your mod. However, I would use jruby for jasper in the future.

Guy