JasperReports and Scala
Join the DZone community and get the full member experience.
Join For FreeReporting. The ungrateful tasks usually left to the peasants of programming teams. In this post, I’ll try to make it more bearable, even interesting; showing you how to use JasperReports.
JasperReports
Generating report from a database using JasperReports is relatively easy. All you have to do is to provide a JRDataSource
or Connection
that can fill the report (details) with the data. Things get more
interesting when you want to produce a report that uses a collection of
your objects. In standard JasperReports, you can use the JRBeanCollectionDataSouce
or JRBeanArrayDataSource
that map collection or array of Java Bean-style objects to rows and
columns in JasperReports. As you can guess, the complication is tying
this to Scala, particularly to case classes.
The first stab at solving this would be to implement JRProductListDataSource
, taking a List[A]
where A < Product
and be done with it. But let’s take it even further and implement more
pleasant way of interacting with JasperReports engine, a mechanism that
reports errors nicely, that deals with loading and compiling the reports
nicely, and that will allow some kind of DSL addition in the future.
The main blocks
It seems that there are three main blocks to running reports. We need to be able to load a report design from some source, compile the design to the report, run the report. We don’t want to tie ourselves down to specific input types, nor a specific way of compiling the reports. In preparation for the future DSL, we don’t want to be passing raw values that will be given to the JasperReports machinery. Right-ho! Let’s outline the three components.
trait ReportLoader { type In def load(in: In): ReportT[InputStream] }
trait ReportCompiler { this: ReportLoader => def compileReport(in: In): ReportT[JasperReport] }
class ReportRunner { this: ReportCompiler with ReportLoader => def runReportT(in: In) (parametersExpression: Expression = EmptyExpression, dataSourceExpression: DataSourceExpression = EmptyDataSourceExpression): ReportT[Array[Byte] = ??? }
As you can guess from the names, the ReportLoader
‘s implementations are responsible for turning the input of type In
into some boxed InputStream
. The box, ReportT
is your friend EitherT
from Scalaz. We define
type ReportT[A] = EitherT[Id, Throwable, A]
That way, we can sensibly sequence the computation of the reports and report any errors.
The loader and compiler
Let’s start with the implementation of the easy blocks: the loader and compiler; starting with the most trivial loader.
trait InputStreamReportLoader extends ReportLoader { type In = InputStream import scalaz.syntax.monad._ def load(in: InputStream) = in.point[ReportT] }
This loader is a simple pass-through: it takes an InputStream
and “boxes” it in ReportT
. To make this loader more strict, let’s make it reject null
InputStream
s.
case class NullInputStreamException() extends RuntimeException trait InputStreamReportLoader extends ReportLoader { type In = InputStream import scalaz.syntax.monad._ def load(in: InputStream) = if (in == null) EitherT.left[Id, Throwable, InputStream]( NullInputStreamException()) else in.point[ReportT] }
Without digging in the details, if the in
parameter is null
, we return box with value on the left: an error. If the in
is valid, we return box with value on the right
.
The second loader is for convenience, really: it loads the definition as classpath resource:
trait ClasspathResourceReportLoader extends ReportLoader { import scalaz.syntax.monad._ type In = String def load(in: String) = { val is = getClass.getResourceAsStream(in) if (is == null) EitherT.left[Id, Throwable, InputStream]( MissingClasspathResourceException(in)) else is.point[ReportT] } }
Now, to compile the reports, I will show only the dynamic compiler that takes the jrxml file and creates the ReportDefinition
.
trait JRXmlReportCompiler extends ReportCompiler { this: ReportLoader => def compileReport(in: In): ReportT[JasperReport] = { for { loaded <- load(in) input <- fromTryCatch[Id, JasperDesign] { JRXmlLoader.load(loaded) } design <- fromTryCatch[Id, JasperReport] { JasperCompileManager.compileReport(input) } } yield design } }
You can now see how the operations sequence nicely. We first load the report definition source from some input, then we turn the source into the definition, and finally we compile the source into the report. If any of the steps fail, we abort the whole process and return the error on the left.
The runner
The runner’s job is to use the loader and compiler and to actually run the report. Before it can do that, it needs to deal with the report parameters and the report data source. Because we don’t want to deal with the raw JasperReports, we have a structure of expressions that we evaluate and then map to the underlying JasperReport primitives.
sealed trait Expression case object EmptyExpression extends Expression case class ReportExpression[A] (name: String, subreport: A, expressions: List[Expression]) extends Expression case class ParametersExpression (expressions: List[Expression]) extends Expression sealed trait DataSourceExpression extends Expression case object EmptyDataSourceExpression extends DataSourceExpression case class ProductParameterExpression[A <: Product] (value: A, name: Option[String] = None) extends DataSourceExpression case class ProductListParameterExpression[A <: Product] (value: List[A], name: Option[String] = None) extends DataSourceExpression
When evaluated, we turn these expressions into matching expression values.
private[reporting] sealed trait ExpressionValue private[reporting] case object EmptyExpressionValue extends ExpressionValue private[reporting] case class ReportExpressionValue (name: String, subreport: JasperReport, expressionValues: List[ExpressionValue]) extends ExpressionValue private[reporting] case class ParametersExpressionValue (value: List[ExpressionValue]) extends ExpressionValue ...
Excellent. This all allows us to run our reports very easily. Suppose I want to run the reports using the JRXML compiler, using the classpath resource loader. I mix in the components together:
val runner = new ReportRunner with JRXmlReportCompiler with ClasspathResourceReportLoader val rows = User(...) :: User(...) :: User(...) :: Nil runner.runReport ("empty.jrxml") (dataSourceExpression = ProductListParameterExpression(rows))
The runner
now takes a String
, which is
interpreted as classpath resource and that resource is compiled from its
JRXML form into the report, which is then filled in with a list of User
instances. How? Easily:
class ReportRunner { this: ReportCompiler with ReportLoader => private def toDataSource(value: ExpressionValue): JRDataSource private def toMap(value: ExpressionValue): java.util.Map[String, AnyRef] private def eval(expression: Expression): ReportT[ExpressionValue] def runReportT(in: In) (parametersExpression: Expression = EmptyExpression, dataSourceExpression: DataSourceExpression = EmptyDataSourceExpression): ReportT[Array[Byte]] = { for { root <- compileReport(in) parametersValues <- eval(parametersExpression) parameters = toMap(parametersValues) dataSourceValue <- eval(dataSourceExpression) dataSource = toDataSource(dataSourceValue) out <- EitherT.fromTryCatch[Id, Array[Byte]] { JasperRunManager.runReportToPdf( root, parameters, dataSource) } } yield out } }
I have skipped the bodies of the toDataSource
, toMap
and eval
functions, but you can get the whole code from Akka Patterns; the implementation of eval
is particularly interesting!
val runner = new ReportRunner with JRXmlReportCompiler with ClasspathResourceReportLoader val rows = User(...) :: User(...) :: Nil runner.runReport("empty.jrxml") (dataSourceExpression = ProductListParameterExpression(rows))
If you want to complain about my report writing, you’ll get such a smack!
Summary
So, you can now use case classes as data source for your reports; and you can use JasperReports with a nice wrapper in Scala in your projects. The code is at https://github.com/janm399/akka-patterns, but I will improve it over the next few days and pull it out into a separate–open source, of course–project. Watch this space.
Published at DZone with permission of Jan Machacek, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments