Enthusiastic Java, Scala and Haskell programmer with a long history of large and successful systems. Known author, speaker, motivator and coach. Jan is a DZone MVB and is not an employee of DZone and has posted 26 posts at DZone. You can read more from them at their website. View Full User Profile

JasperReports and Scala

03.12.2013
| 1838 views |
  • submit to reddit

Reporting. 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 InputStreams.

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, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)