This article explains how the crystal report can be integrated with Ruby on Rails application. With reference from http://wiki.rubyonrails.org/rails/pages/howtointegratejasperreports, I integrated the crystal reports with my ROR application. Thanks to the publisher of that article.
As of now, there is no API for integrating Crystal Reports with ROR. But we can achieve this with the Java API. We can run the java classes from ROR application to generate the crystal report.
Steps for Integration:
1. Placing the jar files in the ROR application
2. Creating the java classes to generate the report with the given inputs
3. Generating the XML and Schema in ROR application
4. Writing the business logic to run the java class that generates the report
5. Writing the controller code
1. Placing the jar files in the ROR application:
We have to keep the following jar files in our application.
cecore.jar
celib.jar
commons-collections-3.1.jar
commons-configuration-1.2.jar
commons-discovery.jar
commons-lang-2.1.jar
commons-logging.jar
Concurrent.jar
corbaidl.jar
CRDBJavaServerCommon.jar
CRDBXMLExternal.jar
CRDBXMLServer.jar
CrystalCharting.jar
CrystalCommon.jar
CrystalContentModels.jar
CrystalDatabaseConnectors.jar
CrystalExporters.jar
CrystalExportingBase.jar
CrystalFormulas.jar
CrystalQueryEngine.jar
CrystalReportEngine.jar
CrystalReportingCommon.jar
ebus405.jar
icu4j.jar
jrcadapter.jar
jrcerom.jar
keycodeDecoder.jar
log4j.jar
MetafileRenderer.jar
pullparser.jar
rasapp.jar
rascore.jar
ReportViewer.jar
rpoifs.jar
serialization.jar
xbean.jar
xercesImpl.jar
xml-apis.jar
I placed these jar files in <MY_RAILS_APP_ROOT>/public/jars/crystal folder. You can place these jar files wherever you want. You can also check and remove some jar files if they won’t be needed.
2. Creating the java classes to generate the report with the given inputs:
Now we have to create a java class that communicates with the API and generates the report for the given inputs. The following class gets the xml data file path, crystal report template file path and the xml schema content as command-line arguments. Please note that I am reading the xml data from the local file system and the xml schema as a command-line argument. You can use either the file system or the command-line for both XML and Schema.
XmlCrystalInterface.java:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.PrintWriter;
import com.crystaldecisions.reports.sdk.*;
import com.crystaldecisions.sdk.occa.report.data.*;
import com.crystaldecisions.sdk.occa.report.exportoptions.ExportOptions;
import com.crystaldecisions.sdk.occa.report.exportoptions.PDFExportFormatOptions;
import com.crystaldecisions.sdk.occa.report.exportoptions.ReportExportFormat;
import com.crystaldecisions.sdk.occa.report.lib.*;
import com.crystaldecisions.sdk.occa.report.reportsource.IReportSource;
/**
* Takes xml data file path, crystal report template path and xml schema string as arguments
* and generates the report for the given data with the given template design
*/
public class XmlCrystalInterface {
public static void main(String[] args) {
String reportDesign = null;
String xmlSchema = null;
String xmlFilePath = null;
// Parse the arguments
for (int indexOfArgs = 0; indexOfArgs < args.length; indexOfArgs++)
if (args[indexOfArgs].startsWith(”-f”))
reportDesign = args[indexOfArgs].substring(2);
else if (args[indexOfArgs].startsWith(”-x”))
xmlFilePath = args[indexOfArgs].substring(2);
else if (args[indexOfArgs].startsWith(”-s”))
xmlSchema = args[indexOfArgs].substring(2);
try {
// Open the report
ReportClientDocument reportClientDocument = new ReportClientDocument();
reportClientDocument.open(reportDesign, 0);
// Read the xml file
File xmlFile = new File(xmlFilePath);
InputStream inputStream = new FileInputStream(xmlFile);
byte[] xmlByteArray = new byte[inputStream.available()];
inputStream.read(xmlByteArray);
inputStream.close();
// Create the byte array for xml data
IByteArray xmlDataByteArray = new ByteArray(xmlByteArray);
// Create the xml schema byte array
byte[] sByteArray = new byte[xmlSchema.length()];
sByteArray = xmlSchema.getBytes();
IByteArray xmlScehmaByteArray = new ByteArray(sByteArray);
// Create the XmlDataSet and add the xml data and shema
IXMLDataSet xmlDataSet = new XMLDataSet();
xmlDataSet.setXMLData(xmlDataByteArray);
xmlDataSet.setXMLSchema(xmlScehmaByteArray);
// Set the data source as the xml data source
reportClientDocument.getDatabaseController().setDataSource(
xmlDataSet, “”, “”);
// Open the report in the Crystal Report Viewer
IReportSource reportSource = reportClientDocument.getReportSource();
reportClientDocument.close();
ExportOptions expOpt = new ExportOptions();
expOpt.setExportFormatType(ReportExportFormat.PDF);
PDFExportFormatOptions pdfExpOpt = new PDFExportFormatOptions();
expOpt.setFormatOptions(pdfExpOpt);
ReportStateInfoImpl repStateInfo = new ReportStateInfoImpl();
RequestContextImpl reqCont = new RequestContextImpl();
reqCont.setReportStateInfo(repStateInfo);
InputStream byteArrayInputStream = reportSource.export(expOpt, reqCont);
byte reportByteArray[] = new byte[byteArrayInputStream.available()];
byteArrayInputStream.read(reportByteArray);
byteArrayInputStream.close();
System.out.write(reportByteArray);
} catch (Exception e) {
PrintWriter out = null;
try {
// Write the exception in the log file
out = new PrintWriter(”./log/XmlCrystalInterface.log”);
out.write(e.getMessage());
} catch (FileNotFoundException filenotfoundexception) {
}
out.close();
}
}
}
Two more classes are needed to set the report export options.
RequestContextImpl.java:
class RequestContextImpl implements
com.crystaldecisions.sdk.occa.report.reportsource.IRequestContext {
private com.crystaldecisions.sdk.occa.report.reportsource.IReportStateInfo repStateInfo = null;
public com.crystaldecisions.sdk.occa.report.reportsource.ISubreportRequestContext getSubreportRequestContext() {
return null;
}
public com.crystaldecisions.sdk.occa.report.reportsource.ITotallerNodeID getTotallerNodeID() {
return null;
}
public void setSubreportRequestContext(
com.crystaldecisions.sdk.occa.report.reportsource.ISubreportRequestContext arg0) {
}
public void setTotallerNodeID(
com.crystaldecisions.sdk.occa.report.reportsource.ITotallerNodeID arg0) {
}
public com.crystaldecisions.sdk.occa.report.lib.PropertyBag getClientCapability() {
return null;
}
public com.crystaldecisions.sdk.occa.report.reportsource.IReportStateInfo getReportStateInfo() {
return this.repStateInfo;
}
public void setClientCapability(
com.crystaldecisions.sdk.occa.report.lib.PropertyBag arg0) {
}
public void setReportStateInfo(
com.crystaldecisions.sdk.occa.report.reportsource.IReportStateInfo iReportStateInfo) {
this.repStateInfo = iReportStateInfo;
}
}
ReportStateInfoImpl.java:
import com.crystaldecisions.sdk.occa.report.data.ConnectionInfos;
class ReportStateInfoImpl implements
com.crystaldecisions.sdk.occa.report.reportsource.IReportStateInfo {
private com.crystaldecisions.sdk.occa.report.data.Fields parameters = null;
public com.crystaldecisions.sdk.occa.report.data.ConnectionInfos getDatabaseOnInfos() {
return null;
}
public String getGroupSelectionFormula() {
return null;
}
public com.crystaldecisions.sdk.occa.report.data.Fields getParameterFields() {
return this.parameters;
}
public String getSelectionFormula() {
return null;
}
public String getViewTimeSelectionFormula() {
return null;
}
public void setDatabaseLogOnInfos(
com.crystaldecisions.sdk.occa.report.data.ConnectionInfos arg0) {
}
public void setGroupSelectionFormula(String arg0) {
}
public void setParameterFields(
com.crystaldecisions.sdk.occa.report.data.Fields parameters) {
this.parameters = parameters;
}
public void setSelectionFormula(String arg0) {
}
public void setViewTimeSelectionFormula(String arg0) {
}
public Object clone(boolean arg0) {
return null;
}
public void copyTo(Object arg0, boolean arg1) {
}
public ConnectionInfos getDatabaseLogOnInfos() {
return null;
}
public boolean hasContent(Object arg0) {
return false;
}
}
Create class files for these classes and place it in a folder. I placed these classes in “<MY_RAILS_APP_ROOT>/public/classes/crystal” folder. Also add a log4j.properties file along with the java class files.
log4j.properties:
# Set root category priority to INFO and its only appender to CONSOLE.
log4j.rootCategory=INFO, CONSOLE
#log4j.rootCategory=INFO, CONSOLE, LOGFILE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Threshold=INFO
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=- %m%n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=axis.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.Threshold=INFO
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%-4r [%t] %-5p %c %x – %m%n
The java environment has been set up now. Now we have to integrate these with our ROR application.
3. Generating the XML and Schema in ROR application:
We can generate the xml by iterating the result objects in the view file and rendering it as a string. The following is the code which generates the xml from the resuts.
export_report.rxml:
xml.instruct!
xml.report do
@results.each do |result|
xml << result.to_xml(:dasherize=>false,:skip_instruct=>true, :root => ‘your_tag’)
end
end
For generating the xml schema, to my knowledge, there is no built-in approach in ruby. So I have generated the schema according to my XML. I have written the method in crystal_report_generator.rb(see below) to generate my XML schema. You can customize as per your application need.
4. Writing the business logic to run the java class that generates the report:
Now we have to write the business logic that runs the java class and returns the report content. The “generate_report” method accepts the xml data, xml schema and the report design template path as arguments. Class path is set for all the jar files and java class files and the “XmlCrystalInterface ” java class is run with the command-line arguments. It is just like running the java class in the command prompt. The output is read from the stream as a string.
crystal_report_generator.rb:
class CrystalReportGenerator
include Config
# Generates the crystal report with the help of XmlCrystalInterface java class. Returns the String output(PDF formatted)
# to the controller. The Controller will send the string output to the browser in the PDF format.
def self.generate_report(xml_data, xml_schema, report_design)
xml_temp_file_path = Dir.getwd+”/tmp/xml_tmp.xml”
# Create a temporary xml file for xml_data
xml_file = File.new(xml_temp_file_path, “w”)
xml_file.print(xml_data)
xml_file.close
# Set the class path for java class files and jar files
interface_classpath=Dir.getwd+”/public/classes/crystal”
case CONFIG['host']
when /mswin32/
Dir.foreach(Dir.getwd+”/public/jars/crystal”) do |file|
interface_classpath << “;#{Dir.getwd}/public/jars/crystal/”+file if (file != ‘.’ and file != ‘..’ and file.match(/.jar/))
end
else
Dir.foreach(Dir.getwd+”/public/jars/crystal”) do |file|
interface_classpath << “:#{Dir.getwd}/public/jars/crystal/”+file if (file != ‘.’ and file != ‘..’ and file.match(/.jar/))
end
end
result = “”
# Pass the arguments and run the java class to generate the report
IO.popen “java -cp \”#{interface_classpath}\” XmlCrystalInterface \”-x#{xml_temp_file_path}\” \”-f#{report_design}\” \”-s#{xml_schema}\””, “w+b” do |pipe|
# Read the result pdf content from the stream
result = pipe.read
pipe.close
end
# Delete the temporary xml file
File.delete(xml_temp_file_path) if File.exists?(xml_temp_file_path)
return result
end
# Generate the xml schema for the query results
def self.build_xml_schema(header_element, body_elements)
schema = “<?xml version=’1.0′ encoding=’utf-8′?>”
schema << “<xs:schema version=’1.0′ xmlns:xs=’http://www.w3.org/2001/XMLSchema’>”
schema << “<xs:element name=’report’>”
schema << “<xs:complexType><xs:sequence>”
schema << “<xs:element maxOccurs=’unbounded’ name=’#{header_element}’ minOccurs=’0′>”
schema << “<xs:complexType><xs:sequence>”
body_elements.each do |element|
schema << “<xs:element name=’#{element.to_s(:underscored)}’ minOccurs=’0′/>”
end
schema << “</xs:sequence></xs:complexType></xs:element>”
schema << “</xs:sequence></xs:complexType></xs:element></xs:schema>”
return schema
end
end
5. Writing the controller code:
Here I am generating the XML and XML Schema and calling the CrystalReportGenerator.generate_report method to generate the report. The result is sent as a PDF to the user.
def export_report
# Your code here
xml_data = “”
file_name = # Your code here. File name with which the report should be saved or opened
@results = # Your code here. This is an array of result objects.
body_elements = # Your code here. xml tag names for generating schema.
template_file_path = # Your code here. Path of the report design template file.
xml_data = escape_xml_data(render_to_string(:template => ‘my_controller/export_report’, :layout => false))
xml_schema = CrystalReportGenerator.build_xml_schema(’header_element’, body_elements)
send_data CrystalReportGenerator.generate_report(xml_data, xml_schema, template_file_path),
:filename => “#{file_name}.pdf”, :type => ‘application/pdf’
end
private
# Parse and sanitize the xml data
def escape_xml_data(xml_data)
xml_data = xml_data.gsub(”\n”, ”)
xml_data = xml_data.gsub(”\””, “‘”)
xml_data = xml_data.gsub(/[\>]([ ])*[\<]/, ‘><’)
return xml_data
end
This approach worked fine for me. You can customize any code or approach according to your application need.