BIRT Extension Mechanism, Part 1: Custom Report Items

Abstract

This article introduces the extension mechanism of BIRT report model, engine and designer, and shows how to create custom custom report items step-by-step.

By Zhiqiang Qian, Actuate Corporation
December 9, 2008

Introduction

For many people, BIRT (Business Intelligence and Reporting Tools) may be only a reporting tool. But in fact, the real strength of BIRT is not only its built-in reporting functionality, but also its extension capabilities. Using extensions, a user can easily extend the functionality of BIRT, creating custom extended report items, adding custom emitters, and even having custom editor pages. The power of extensions enables the user to create custom reporting features that can meet very specific requirements. In this article, we explore how to leverage the BIRT extension mechanism to extend the BIRT capabilities. The provided samples cover most of the essential extension points that provided by BIRT report model, engine and designer.

This article assumes the reader already has the knowledge about the Eclipse extension mechanism. If you are not familiar with that, you can read this article first about the Eclipse Plug-in Architecture to get some initial concept.

This article also uses an Eclipse plug-in project to hold all the source code and demonstrate the extension mechanism. You can either import the project directly from this archive or create your own plug-in project through the Eclipse IDE.

The Basics

BIRT already contains the basic Label and Text report items, but these items can only show horizontal text. Sometimes, user would like to show angled text; in this case, alternative solution is needed. In the following sections, we introduce how to implement a RotatedText extended report item through BIRT extensions, and how to enhance the functionality step-by-step by implementing more extension points.

Note: This example is a modified version based on the RotatedText sample on BIRT CVS, the original code can be retrieved from Eclipse CVS.

To implement an extended report item, we need look at the following extension points:


These three extensions provide the basics to implement an extended report item. In brief, org.eclipse.birt.report.model.reportItemModel provides the report model extensibility, org.eclipse.birt.report.designer.ui.reportitemUI provides the report designer extensibility, and org.eclipse.birt.report.engine.reportitemPresentation provides the report engine extensibility.

org.eclipse.birt.report.model.reportItemModel

This extension point is provided by the BIRT Report Model. Normally the user uses this extension point to define the extended report item model.

First we need define a new report item model extension. To achieve this, we create a "reportItem" element under the extension and specify the extensionName property as "RotatedText". The extension name is the identifier for the extended item, it's also the only symbol that connects between the report model and engine/designer extensions.

Then we define the model factory class as org.eclipse.birt.sample.reportitem.rotatedtext.RotatedTextItemFactory. This factory class will be used to create and initialize the IReportItem instance, as well as providing optional localization support:

public class RotatedTextItemFactory extends ReportItemFactory
{
	public IReportItem newReportItem( DesignElementHandle modelHandle )
	{
		if ( modelHandle instanceof ExtendedItemHandle && RotatedTextItem.EXTENSION_NAME.equals( ( (ExtendedItemHandle) modelHandle ).getExtensionName( ) ) )
		{
			return new RotatedTextItem( (ExtendedItemHandle) modelHandle );
		}
		return null;
	}

	public IMessages getMessages( )
	{
		// TODO implement this to support localization
		return null;
	}
}

From the "reportItem" properties page, we can see some other settings like "defaultStyle", "extendsFrom", etc. They can be used to control more capabilities of the extended item. As we are not using them in this example, we just omit them here.

Here we define two new properties for the RotatedText extended report item:

We can see in the properties page there are also some settings that we didn't touch. Normally they are for more complex property types and are out of the scope of this article, so we just omit them here.

Once we completed the model definition, we need give a IReportItem implementation. This class is used to encapsulate the extended report item model, and providing some convenient property accessing APIs:

public class RotatedTextItem extends ReportItem
{
	public static final String EXTENSION_NAME = "RotatedText"; //$NON-NLS-1$
	public static final String TEXT_PROP = "text"; //$NON-NLS-1$
	public static final String ROTATION_ANGLE_PROP = "rotationAngle"; //$NON-NLS-1$

	private ExtendedItemHandle modelHandle;

	RotatedTextItem( ExtendedItemHandle modelHandle )
	{
		this.modelHandle = modelHandle;
	}

	public String getText( )
	{
		return modelHandle.getStringProperty( TEXT_PROP );
	}

	public int getRotationAngle( )
	{
		return modelHandle.getIntProperty( ROTATION_ANGLE_PROP );
	}

	public void setText( String value ) throws SemanticException
	{
		modelHandle.setProperty( TEXT_PROP, value );
	}

	public void setRotationAngle( int value ) throws SemanticException
	{
		modelHandle.setProperty( ROTATION_ANGLE_PROP, value );
	}
}

We can see most of the methods here are just getter/setters. For a simple extended report item, this is normally enough.

org.eclipse.birt.report.designer.ui.reportitemUI

This extension point is provided by BIRT Designer, the user uses this extension point to define the UI behavior for extended report items.

First we need create a "model" element under the extension. This is to bind the Designer extension with the Model extension. To do this, we specify the extensionName property as "RotatedText", which exactly matches the model extension name.

The next step, we create some other elements like "palette", "editor" and "outline" under the extension, and specify the detailed settings for this extended report item as following:

The last step, we need specify the UI provider for this extended report item.

The UI provider defines how to display and interact with the extended report item within the editor. BIRT designer support three types of basic UI providers:

In this example, we choose to implement the simplest Label UI Provider first, in subsequent examples, we will introduce other types of UI providers.

To register the Label UI provider, we create a "reportItemLabelUI" element under the extension, and specify the implementor class, which must implement IReportItemLabelProvider interface.

Here our Label UI Provider implementor class is org.eclipse.birt.sample.reportitem.rotatedText.RotatedTextLabelUI; the code is very simple:

public class RotatedTextLabelUI implements IReportItemLabelProvider
{
	public String getLabel( ExtendedItemHandle handle )
	{
		try
		{
			IReportItem item = handle.getReportItem( );
			if ( item instanceof RotatedTextItem )
			{
				return ( (RotatedTextItem) item ).getText( );
			}
		}
		catch ( ExtendedElementException e )
		{
			e.printStackTrace( );
		}
		return null;
	}
}

You can see that what it does is to read the text property value from the model and return it as a string.

After we completed the designer extension, we can run the designer and check what we have: the Palette view now contains the new RotatedText extended report item.

If you right-click anywhere in the editor, you can see the RotatedText item is also available in the "Insert" context menu.

When you insert one RotatedText item into the layout, it appears in the Outline view.

Select the RotatedText item, and open the Properties view. It immediately lists all the properties that belong to this extended report item. Among them, we can see the "Rotation Angle" and "Text Content" as well as other inherited properties. The user can edit these property values through this view.

Now that the designer extension is done, we'll continue to the Report Engine extension.

org.eclipse.birt.report.engine.reportitemPresentation

This extension point is provided by BIRT Report Engine, the user uses this extension point to define the presentation behavior of the extended report item.

First we bind the engine extension with the model extension. To achieve this, we create a "reportItem" element under the engine extension and specify the name property as "RotatedText", also we specify the presentation implementor class as org.eclipse.birt.sample.reportitem.rotatedtext.RotatedTextPresentationImpl. The presentation implementor class must implement the IReportItemPresentation interface.

Our plan for implementing the rotated text effect is to create an image dynamically, in this image, we use Swing graphics API to render the rotated text.

Here is the code that we need:

public class RotatedTextPresentationImpl extends ReportItemPresentationBase
{
	private RotatedTextItem textItem;
	
	public void setModelObject( ExtendedItemHandle modelHandle )
	{
		try
		{
			textItem = (RotatedTextItem) modelHandle.getReportItem( );
		}
		catch ( ExtendedElementException e )
		{
			e.printStackTrace( );
		}
	}
	
	public int getOutputType( )
	{
		return OUTPUT_AS_IMAGE;
	}
	
	public Object onRowSets( IRowSet[] rowSets ) throws BirtException
{ if ( textItem == null ) { return null; } int angle = textItem.getRotationAngle( ); String text = textItem.getText( ); BufferedImage rotatedImage = SwingGraphicsUtil.createRotatedTextImage( text, angle, new Font( "Default", 0, 12 ) ); //$NON-NLS-1$ ByteArrayInputStream bis = null; try { ImageIO.setUseCache( false ); ByteArrayOutputStream baos = new ByteArrayOutputStream( ); ImageOutputStream ios = ImageIO.createImageOutputStream( baos ); ImageIO.write( rotatedImage, "png", ios ); //$NON-NLS-1$ ios.flush( ); ios.close( ); bis = new ByteArrayInputStream( baos.toByteArray( ) ); } catch ( IOException e ) { e.printStackTrace( ); } return bis; } }

This implementation generate an Image dynamically in the onRowSets() method. To return the image content, we create an in-memory stream and put the image content into it.

Here is the utility class that implements the key rotation rendering algorithm:

public class SwingGraphicsUtil
{
	public static BufferedImage createRotatedTextImage( String text, int angle, Font ft )
	{
		Graphics2D g2d = null;
		try
		{
			if ( text == null || text.trim( ).length( ) == 0 )
			{
				return null;
			}
			BufferedImage stringImage = new BufferedImage( 1, 1, BufferedImage.TYPE_INT_ARGB );
			g2d = (Graphics2D) stringImage.getGraphics( );
			g2d.setFont( ft );
			FontMetrics fm = g2d.getFontMetrics( );
			Rectangle2D bounds = fm.getStringBounds( text, g2d );
			TextLayout tl = new TextLayout( text, ft, g2d.getFontRenderContext( ) );
			g2d.dispose( );
			g2d = null;
			return createRotatedImage( tl, (int) bounds.getWidth( ), (int) bounds.getHeight( ), angle );
		}
		catch ( Exception e )
		{
			e.printStackTrace( );
			if ( g2d != null )
			{
				g2d.dispose( );
			}
		}
		return null;
	}
	
	private static BufferedImage createRotatedImage( Object src, int width, int height, int angle )
	{
		angle = angle % 360;
		if ( angle < 0 )
		{
			angle += 360;
		}
		if ( angle == 0 )
		{
			return renderRotatedObject( src, 0, width, height, 0, 0 );
		}
		else if ( angle == 90 )
		{
			return renderRotatedObject( src, -Math.PI / 2, height, width, -width, 0 );
		}
		else if ( angle == 180 )
		{
			return renderRotatedObject( src, Math.PI, width, height, -width, -height );
		}
		else if ( angle == 270 )
		{
			return renderRotatedObject( src, Math.PI / 2, height, width, 0, -height );
		}
		else if ( angle > 0 && angle < 90 )
		{
			double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );
			double cosTheta = Math.abs( Math.cos( angleInRadians ) );
			double sineTheta = Math.abs( Math.sin( angleInRadians ) );
			int dW = (int) ( width * cosTheta + height * sineTheta );
			int dH = (int) ( width * sineTheta + height * cosTheta );
			return renderRotatedObject( src, angleInRadians, dW, dH, -width * sineTheta * sineTheta, width * sineTheta * cosTheta );
		}
		else if ( angle > 90 && angle < 180 )
		{
			double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );
			double cosTheta = Math.abs( Math.cos( angleInRadians ) );
			double sineTheta = Math.abs( Math.sin( angleInRadians ) );
			int dW = (int) ( width * cosTheta + height * sineTheta );
			int dH = (int) ( width * sineTheta + height * cosTheta );
			return renderRotatedObject( src, angleInRadians, dW, dH, -( width + height * sineTheta * cosTheta ), -height / 2 );
		}
		else if ( angle > 180 && angle < 270 )
		{
			double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );
			double cosTheta = Math.abs( Math.cos( angleInRadians ) );
			double sineTheta = Math.abs( Math.sin( angleInRadians ) );
			int dW = (int) ( width * cosTheta + height * sineTheta );
			int dH = (int) ( width * sineTheta + height * cosTheta );
			return renderRotatedObject( src, angleInRadians, dW, dH, -( width * cosTheta * cosTheta ), -( height + width * cosTheta * sineTheta ) );
		}
		else if ( angle > 270 && angle < 360 )
		{
			double angleInRadians = ( ( -angle * Math.PI ) / 180.0 );
			double cosTheta = Math.abs( Math.cos( angleInRadians ) );
			double sineTheta = Math.abs( Math.sin( angleInRadians ) );
			int dW = (int) ( width * cosTheta + height * sineTheta );
			int dH = (int) ( width * sineTheta + height * cosTheta );
			return renderRotatedObject( src, angleInRadians, dW, dH, ( height * cosTheta * sineTheta ), -( height * sineTheta * sineTheta ) );
		}
		return renderRotatedObject( src, 0, width, height, 0, 0 );
	}
	
	private static BufferedImage renderRotatedObject( Object src, double angle, int width, int height, double tx, double ty )
	{
		BufferedImage dest = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB );
		Graphics2D g2d = (Graphics2D) dest.getGraphics( );
		g2d.setColor( Color.black );
		g2d.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON );
		g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
		AffineTransform at = AffineTransform.getRotateInstance( angle );
		at.translate( tx, ty );
		g2d.setTransform( at );
		if ( src instanceof TextLayout )
		{
			TextLayout tl = (TextLayout) src;
			tl.draw( g2d, 0, tl.getAscent( ) );
		}
		else if ( src instanceof Image )
		{
			g2d.drawImage( (Image) src, 0, 0, null );
		}
		g2d.dispose( );
		return dest;
	}
}

The Result

Create a report and insert some RotatedText items first:

Here we have inserted quite a few RotatedText items that with different angle settings.

When we preview the report as HTML, it looks like this:

Preview the report as PDF, it results like this:

Done! We've successfully implemented a RotatedText extended report item for BIRT, and integrated it with Designer and Report Engine.

Summary

BIRT provides very good extension mechanism, which allows user to create custom reporting features. This article introduced how to write a custom RotatedText extended report item for BIRT.

Resources

[1] The complete source code: you can extract and import it into Eclipse workspace as a plug-in project directly

[2] BIRT Official Site: http://eclipse.org/birt

[3] How to access Eclipse CVS: http://wiki.eclipse.org/CVS_Howto