Friday, April 18, 2008

Return To Eureka

Two weeks ago I had a Eureka moment. In that post I described how I was able to use reflection to get the DesignElementHandle from an ICell object. It was a fairly simple solution since there was only one super class to the ICell.

Yesterday, I started to re-factor my other example classes to use this helper utility and I figured out that the helper utility need a little more work. It turns out that the ITable class is a bit more complicated then the ICell class. For the ITable object there are five levels super classes to get back to the DesignElement class.

So the ITable object actually resolves to:

  • class org.eclipse.birt.report.engine.script.internal.element.Table
The hierarchy for this class is:
  • class org.eclipse.birt.report.engine.script.internal.element.Listing
  • class org.eclipse.birt.report.engine.script.internal.element.ReportItem
  • class org.eclipse.birt.report.engine.script.internal.element.ReportElement
  • class org.eclipse.birt.report.engine.script.internal.element.DesignElement
When we use reflection to find the protected field we have to navigate all the way back to DesignElement which can be n levels up the hierarchy. So we need to use a loop to search for the parent.

Once we have the DesignElement we can use reflection to get access to the protected field for the simple api DesignElement. The simple api has the same hiearchy issue, with one additional super-class.
  • class org.eclipse.birt.report.model.simpleapi.Table
  • class org.eclipse.birt.report.model.simpleapi.Listing
  • class org.eclipse.birt.report.model.simpleapi.MultiRowItem
  • class org.eclipse.birt.report.model.simpleapi.ReportItem
  • class org.eclipse.birt.report.model.simpleapi.ReportElement
  • class org.eclipse.birt.report.model.simpleapi.DesignElement
Fortunately, the solution is not that difficult. The addition of one relatively simple loop allows us to find the parent classes we need.


private static final String DESIGN_ELEMENT_NAME = ".DesignElement";

private static Class findDesignElementParent(Class childClass) throws NoSuchFieldException {
System.out.println(childClass.toString());
do {
childClass = childClass.getSuperclass();
if (childClass == null)
throw new NoSuchFieldException("Did not find DesignElement");
System.out.println(childClass.toString());
} while (!childClass.toString().endsWith(DESIGN_ELEMENT_NAME));
return childClass;
}


You may ask scratch your head and ask why am I searching for the class using the name of the class as opposed to a reference to the actual class name. This goes back to the issue that the classes that we are referencing are not actually exposed through the runtime interface.

In any case, once I have made these changes my utility method is still relatively simple.

public static DesignElementHandle getDesignElementFromScript(IDesignElement scriptObj)
throws IllegalAccessException, SecurityException, NoSuchFieldException {

Class scriptParentClass = findDesignElementParent(scriptObj.getClass());
Field fieldFromScript = scriptParentClass.getDeclaredField("designElementImpl");
if (fieldFromScript == null) {
return null;
}

// Access the DesignElement field from the Script object
fieldFromScript.setAccessible(true);
Object designElement = fieldFromScript.get(scriptObj);

// The DesignElement object has a field named "handle"
// which is the DesignElementHandle for that DesignElement
// Use reflection again to get this field
Class simpleApiParentClass = findDesignElementParent(designElement.getClass());
Field fieldFromSimpleApi = simpleApiParentClass.getDeclaredField("handle");
if (fieldFromSimpleApi == null) {
return null;
}

// Now access the DesignElementHandle from the DesignElement
fieldFromSimpleApi.setAccessible(true);
return (DesignElementHandle) fieldFromSimpleApi.get(designElement);
}


The complete source is available in the script.lib project on the http://longlake.minnovent.com/repos/birt_example subversion project. I have moved the code into the ScriptUtil class.

There is an underlying question here. You may ask why do I have to go through those gyrations to get a handle to the object? Shouldn't the API just support this method. That's a good question. The issue is that working with the DesignElementHandle object directly is a relatively advanced feature.

The BIRT script objects are meant to be a report developer interface and have been designed to protect the report developer from obscure and hard to debug conditions. When you start working with the ElementHandles there is very little that is protecting the developer from doing something they don't want to do.

For example, in my Eureka example I demonstrated changing the column span for a cell. What I didn't mention is that for the example in question, the change I made was always going to be appropriate. In reality when you change the column span on one cell in the row, you have to account for the total number of cells. This means that you have to:
  • Modify the column span of another cell in the row
  • Drop cells from the row
  • Add cells to the row
These operations are not difficult, but they do require a level of sophistication and thought that many beginning report developers are not ready to take on. So is there a way to expose methods, but somehow warn that these methods are for power users only? Is it better to just not expose the power methods and use a utility class such as the one I have demonstrated?

I have opened a bugzilla item to expose the DesignElementHandle through the API. I would like to hear what other people think of this idea. How do you expose the power interface while still protecting users that don't want or need access to objects that powerful?

2 comments:

Aaron Digulla said...

Nice one. Just one tip: Search for:

private static final String DESIGN_ELEMENT_NAME = ".DesignElement";

Note that leading dot which makes sure you don't get an IDesignElement or somesuch back. Or use the full class name and equals() which would be even more safe.

Scott Rosenbaum said...

Thanks Aaron, good idea to include the .DesignElement. I can't actually use the full class name since I am looking for the DesignElement for both the internal script object and the simpleapi object.

I have modified the code to include the .DesignElement.