Saturday, December 08, 2007

BIRT: Passing Serialized Objects as Parameters

Recently I had a question about being able to serialize Java Objects and use them as the data source. I have a few possible solutions, but I wanted to look at one of the options a little deeper.

In this scenario, the Java Objects are actually generated outside of the report application, and need to be passed to the report as a parameter. The easiest way was to create a Java class that extends the java.io.Serializable class. In additional to using the Serializable class, I also want to URL encode the serialized class. What this means is that I need to Decode and Deserialize the class inside of the report.

The following is a walkthrough of the classes that I built, and an external Java Event Handler for a Report Design that will handle the object.

To demonstrate this, I used the following Java class.

package com.digiassn.blogspot;

import java.io.Serializable;

public class EmployeeParameter implements Serializable {
static final long serialVersionUID = 11111112;

private int employeeNumber;
private String date;
public int getEmployeeNumber() {
return employeeNumber;
}
public void setEmployeeNumber(int employeeNumber) {
this.employeeNumber = employeeNumber;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}


}



Not much to this class. The serialVersionUID was defined to avoid issues with the URLEncode and URLDecode process, which if not implicitly defined, causes the URLDecode of the object to fail. This is also the same reason I used String as my Date type instead of the java.util.Date. For some reason there were errors with the Date and its use of the serialVersionUID (yet, String, and a few other classes didn’t have any issues).

I wanted to test to see if the whole serialize and URLEncode process would work, so I created the following Unit test.

package com.digiassn.blogspot.tests;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

import junit.framework.TestCase;

import com.digiassn.blogspot.EmployeeParameter;

public class TestEmployeeParameter extends TestCase {
private EmployeeParameter param;

protected void setUp() throws Exception {
super.setUp();

param = new EmployeeParameter();

param.setEmployeeNumber(1);
param.setDate("01-01-2005");
}

protected void tearDown() throws Exception {
super.tearDown();
}

public void testGetEmployeeNumber() {
assertEquals(1, param.getEmployeeNumber());
}

public void testGetDate() {
assertEquals("01-01-2005", param.getDate());
}

public void testSerialize()
{
ByteArrayOutputStream bos = null;
try {
bos = new ByteArrayOutputStream();
ObjectOutputStream obj_out = new ObjectOutputStream (bos);
obj_out.writeObject ( param );
} catch (IOException e) {
e.printStackTrace();
fail("Error with serialization\n\n");
}

String encoded = bos.toString();
try {
encoded = URLEncoder.encode(encoded, "UTF-8");
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();

fail("Unsupported formatting");
}
System.out.print("The serialized output is: " + encoded);

try {
String toDecode = URLDecoder.decode(encoded, "UTF-8");
ByteArrayInputStream bis = new ByteArrayInputStream(toDecode.getBytes());
ObjectInputStream obj_in = new ObjectInputStream (bis);

Object obj = obj_in.readObject();

if (obj instanceof EmployeeParameter)
{
assertEquals(1, ((EmployeeParameter)obj).getEmployeeNumber());
assertEquals("01-01-2005", ((EmployeeParameter)obj).getDate());
}
} catch (IOException e) {
e.printStackTrace();
fail("Error with deserialization");
} catch (ClassNotFoundException e) {
e.printStackTrace();
fail("Error with deserialization");
}

}

}


So, my next task is to create the Event Handler. In BIRT, event handlers need to extend their appropriate event handler type. Since this is a Scripted Data Source, I need to extend the org.eclipse.birt.report.engine.api.script.eventadapter.ScriptedDataSetEventAdapter type.

package com.digiassn.blogspot.handlers;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

import org.eclipse.birt.report.engine.api.script.IReportContext;
import org.eclipse.birt.report.engine.api.script.IUpdatableDataSetRow;
import org.eclipse.birt.report.engine.api.script.ScriptException;
import org.eclipse.birt.report.engine.api.script.eventadapter.ScriptedDataSetEventAdapter;
import org.eclipse.birt.report.engine.api.script.instance.IDataSetInstance;

import com.digiassn.blogspot.EmployeeParameter;

public class EmployeeEventHandler extends ScriptedDataSetEventAdapter {
private EmployeeParameter param;
int count = 0;

@Override
public boolean fetch(IDataSetInstance dataSet, IUpdatableDataSetRow row) {

if (count < 1)
{
try {
if (param == null)
{
row.setColumnValue("employeeNumber", -1);
row.setColumnValue("setDate", "Error, param is null");
}
else
{
row.setColumnValue("employeeNumber", param.getEmployeeNumber());
row.setColumnValue("setDate", param.getDate());
}
count++;

return true;
} catch (ScriptException e) {
e.printStackTrace();
}
}


return false;
}

@Override
public void beforeOpen(IDataSetInstance dataSet,
IReportContext reportContext) {

String myParam = null;

try {
myParam = URLDecoder.decode((String) reportContext.getParameterValue("employeeParam"), "UTF-8");
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
System.out.println("Got parameters");
ByteArrayInputStream bis = new ByteArrayInputStream(myParam.getBytes());

try {
ObjectInputStream obj_in = new ObjectInputStream(bis);

param = (EmployeeParameter)obj_in.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}




So, now I have the event handler made, I need to create the report. To create the report I use the following steps:
1: Create a new report.
2: Create a Scripted Data Source.
3: Create a data set.
4: Add in the following two fields into the data set
-employeeNumber
-setDate
5: Add a report parameter called employeeParam.
6: Drag the data set over to the report design window.
7: Select the new data set in the Data Explorer.
8: In the Property Editor. Select the Event Handler tab.
9: Under the Event Hander, browse and select the Java Object Event handler.

Now, when I run the report, my report will use the external object. I need to pass in the serialized version of the object. In my case, I used the following test string.

%C2%AC%C3%AD%05sr5com.digiassn.blogspot.EmployeeParameter%C2%A9%C5%A0%C3%88%02%02I%0EemployeeNumberL%04datet%12Ljava%2Flang%2FString%3Bxp%01t%0A01-01-2005



I would recommend that you use the Unit test and copy the serialized object from the standard output device.

And that’s it. Now, I deployed this to an Apache Tomcat server using the BIRT Web Viewer to view my report. I had to copy the .class files for EmployeeParameter and the event handler into the shared class folder under Tomcat. I also needed copy all the JAR files in the Web Viewer into the shared lib folder to get around some silly bug in the Web Viewer (note: this was not a problem prior to version 2.2.1).

I still ran into one outstanding issue with this approach. If I tried to use the serialized object in a URL parameter in a browser, I would get a SaxParser error. This wasn’t a problem if I passed the parameter using the dialog in the web viewer, or if I assigned the parameter using the Report Engine API, so it must have something to do with the way the Viewer app handles its SOAP requests.

No comments: