JasperReports and Scala

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!

All this effort with the following code snipped produced this beautiful report:

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.

This entry was posted in Jan's Blog and tagged , , , . Bookmark the permalink.

11 Responses to JasperReports and Scala

  1. Pingback: This week in #Scala (08/03/2013) | Cake Solutions Team Blog

  2. Michael Adams says:

    Hello Jan,

    your article is quite interesting.

    I want to use your little framework to implement an engine based on XText/Scala to generate Specification documents based on DSL’s. Therefore I modelled some Scala Classes like “SpecComponent”, “SpecUse” and so on which should be used as the resulting code generator nodes. These nodes have representations which could be realized as Jasper reports.

    Now my question:
    I managed to generate a plain report based on the entity SpecComponent. A SpecComponent has a list of SpecUseCases. On the report side these should be embedded as subreports inside a SpecComponent-Master-Report. I generated a subreport useCase.jrxml. My main report component.jrxml uses this subreport in the following way:

    The problem is that the Jasper-engine requires parameters for dataSource and a reference to the jasper-subreport to work correctly.
    By using your ReportExpression-construct I thought these informations would be handed over to the Jasper engine.

    ReportExpression[String](“subreportParameter”, “useCase.jrxml”, List(ProductListParameterExpression(component.useCases,
    Some(“dataSourceExpression”))

    But in fact this is not the case. I do not see a subreport output in my master report.

    I wonder whether i have to construct the subreport-element like so:

    Can you please give me a hint, how to correctly build a subreport based on your framework.

    I use the following call to generate the report:

    override def build(sbbs: List[SpecBuildingBlock]) = {
    output = Some(runReport(resource)(
    format =
    (r: JasperReport, rp: ReportParameters, ds: JRDataSource) =>
    out(JasperRunManager.runReportToPdf(r, rp, ds)),
    parametersExpression = params,
    dataSourceExpression = ProductListParameterExpression(sbbs)))
    output.get
    }

    where r is the master-report

    Thank’s for your work and support.

    Michael

  3. Jan Machacek says:

    Hi Michael,

    I’ll get back to you over the weekend!

    –J

  4. Michael Adams says:

    Hi Jan,

    thanks for your quick answer. As i read my post again i realized that some XML-content was not sent. How do i have to quote it?

    I am looking forward to discuss my problem with you over the weekend.

    Thank’s

    Michael

  5. Jan Machacek says:

    Yes, if you escape the < and > you should be fine.

    –J

  6. Michael Adams says:

    Hi Jan,

    ok i post my question again with escaped XML. I hope the escaped XML is still readable.

    I want to use your little framework to implement an engine based on XText/Scala to generate Specification documents based on DSL’s. Therefore I modelled some Scala Classes like “SpecComponent”, “SpecUse” and so on which should be used as the resulting code generator nodes. These nodes have representations which could be realized as Jasper reports.

    Now my question:
    I managed to generate a plain report based on the entity SpecComponent. A SpecComponent has a list of SpecUseCases. On the report side these should be embedded as subreports inside a SpecComponent-Master-Report. I generated a subreport useCase.jrxml. My main report component.jrxml uses this subreport in the following way:

    <subreport>
    </subreport>

    The problem is that the Jasper-engine requires parameters for dataSource and a reference to the jasper-subreport to work correctly.
    By using your ReportExpression-construct I thought these informations would be handed over to the Jasper engine.

    ReportExpression[String](“subreportParameter”, “useCase.jrxml”, List(ProductListParameterExpression(component.useCases,
    Some(“dataSourceExpression”))

    But in fact this is not the case. I do not see a subreport output in my master report.

    I wonder whether i have to construct the subreport-element like so:

    <subreport>
    <reportElement uid=…/>
    <dataSourceExpression><![CDATA[new JRBeanCollectionDataSource($F{useCases})]]>
    </dataSourceExpression>
    <subreportExpression><! [CDATA["useCase.jasper"]]>
    </subreportExpression>
    </subreport>

    Can you please give me a hint, how to correctly build a subreport based on your framework.

    I use the following call to generate the report:

    override def build(sbbs: List[SpecBuildingBlock]) = {
    output = Some(runReport(resource)(
    format =
    (r: JasperReport, rp: ReportParameters, ds: JRDataSource) =>
    out(JasperRunManager.runReportToPdf(r, rp, ds)),
    parametersExpression = params,
    dataSourceExpression = ProductListParameterExpression(sbbs)))
    output.get
    }

    where r is the master-report

    Thank’s for your work and support.

    Michael

  7. Michael Adams says:

    Hello Jan,

    may i have a chance that you answer my question?

    Thanks

    Michael

  8. Jan Machacek says:

    Sorry–I got over-run by life & work… I’ll get back to you ASAP.

    –J

  9. Michael Adams says:

    Hi Jan,

    no problem, take your time.

    Michael

  10. Michael Adams says:

    Hi Jan,

    do you have the time to answer my question this weekend?

    Thanks

    Michael

  11. Michael Adams says:

    Jan,

    if you do not have the time i have a look at it by myself and if i fail, i maybe ask again later.

    Thanks

    Michael

Leave a Reply