ReportBuilder.scala 8.2 KB
Newer Older
Peter van 't Hof's avatar
Peter van 't Hof committed
1
2
3
4
5
6
7
8
9
10
/**
 * Biopet is built on top of GATK Queue for building bioinformatic
 * pipelines. It is mainly intended to support LUMC SHARK cluster which is running
 * SGE. But other types of HPC that are supported by GATK Queue (such as PBS)
 * should also be able to execute Biopet tools and pipelines.
 *
 * Copyright 2014 Sequencing Analysis Support Core - Leiden University Medical Center
 *
 * Contact us at: sasc@lumc.nl
 *
11
 * A dual licensing mode is applied. The source code within this project is freely available for non-commercial use under an AGPL
Peter van 't Hof's avatar
Peter van 't Hof committed
12
13
14
 * license; For commercial users or users who do not want to follow the AGPL
 * license, please contact us to obtain a separate license.
 */
15
16
package nl.lumc.sasc.biopet.core.report

17
import java.io._
18

19
import nl.lumc.sasc.biopet.core.ToolCommandFunction
20
import nl.lumc.sasc.biopet.utils.summary.Summary
Wai Yi Leung's avatar
Wai Yi Leung committed
21
import nl.lumc.sasc.biopet.utils.{ IoUtils, Logging, ToolCommand }
Peter van 't Hof's avatar
Peter van 't Hof committed
22
import org.broadinstitute.gatk.utils.commandline.Input
23
import org.fusesource.scalate.TemplateEngine
24

Peter van 't Hof's avatar
Peter van 't Hof committed
25
import scala.collection.mutable
26
import scala.language.postfixOps
27
28

/**
29
30
31
 * This trait is meant to make an extension for a report object
 *
 * @author pjvan_thof
32
 */
33
trait ReportBuilderExtension extends ToolCommandFunction {
34

35
  /** Report builder object */
36
  def builder: ReportBuilder
37

Peter van 't Hof's avatar
Peter van 't Hof committed
38
39
  def toolObject = builder

40
41
42
  @Input(required = true)
  var summaryFile: File = _

43
  /** OutputDir for the report  */
44
45
  var outputDir: File = _

46
  /** Arguments that are passed on the commandline */
47
48
  var args: Map[String, String] = Map()

49
50
  override def defaultCoreMemory = 4.0
  override def defaultThreads = 3
51

52
53
  override def beforeGraph(): Unit = {
    super.beforeGraph()
54
55
56
57
    jobOutputFile = new File(outputDir, ".report.log.out")
    javaMainClass = builder.getClass.getName.takeWhile(_ != '$')
  }

58
  /** Command to generate the report */
59
60
  override def cmdLine: String = {
    super.cmdLine +
61
62
      required("--summary", summaryFile) +
      required("--outputDir", outputDir) +
Peter van 't Hof's avatar
Peter van 't Hof committed
63
      args.map(x => required("-a", x._1 + "=" + x._2)).mkString
64
65
66
  }
}

67
68
trait ReportBuilder extends ToolCommand {

69
70
71
  case class Args(summary: File = null,
                  outputDir: File = null,
                  pageArgs: mutable.Map[String, Any] = mutable.Map()) extends AbstractArgs
72
73

  class OptParser extends AbstractOptParser {
74
75
76
77
78
79
80

    head(
      s"""
         |$commandName - Generate HTML formatted report from a biopet summary.json
       """.stripMargin
    )

Peter van 't Hof's avatar
Peter van 't Hof committed
81
    opt[File]('s', "summary") unbounded () required () maxOccurs 1 valueName "<file>" action { (x, c) =>
82
      c.copy(summary = x)
83
84
85
86
    } validate {
      x => if (x.exists) success else failure("Summary JSON file not found!")
    } text "Biopet summary JSON file"

Peter van 't Hof's avatar
Peter van 't Hof committed
87
    opt[File]('o', "outputDir") unbounded () required () maxOccurs 1 valueName "<file>" action { (x, c) =>
88
      c.copy(outputDir = x)
89
90
    } text "Output HTML report files to this directory"

Peter van 't Hof's avatar
Peter van 't Hof committed
91
    opt[Map[String, String]]('a', "args") unbounded () action { (x, c) =>
92
93
      c.copy(pageArgs = c.pageArgs ++ x)
    }
94
95
  }

96
  /** summary object internaly */
97
98
  private var setSummary: Summary = _

99
  /** Retrival of summary, read only */
100
101
  final def summary = setSummary

102
  /** default args that are passed to all page withing the report */
103
104
  def pageArgs: Map[String, Any] = Map()

Peter van 't Hof's avatar
Peter van 't Hof committed
105
106
107
  private var done = 0
  private var total = 0

Peter van 't Hof's avatar
Peter van 't Hof committed
108
  private var _sampleId: Option[String] = None
Peter van 't Hof's avatar
Peter van 't Hof committed
109
  protected[report] def sampleId = _sampleId
Peter van 't Hof's avatar
Peter van 't Hof committed
110
  private var _libId: Option[String] = None
Peter van 't Hof's avatar
Peter van 't Hof committed
111
  protected[report] def libId = _libId
Peter van 't Hof's avatar
Peter van 't Hof committed
112

113
114
  case class ExtFile(resourcePath: String, targetPath: String)

115
116
117
118
119
120
121
122
  def extFiles = List(
    "css/bootstrap_dashboard.css",
    "css/bootstrap.min.css",
    "css/bootstrap-theme.min.css",
    "css/sortable-theme-bootstrap.css",
    "js/jquery.min.js",
    "js/sortable.min.js",
    "js/bootstrap.min.js",
Peter van 't Hof's avatar
Peter van 't Hof committed
123
    "js/d3.v3.5.5.min.js",
124
125
126
    "fonts/glyphicons-halflings-regular.woff",
    "fonts/glyphicons-halflings-regular.ttf",
    "fonts/glyphicons-halflings-regular.woff2"
Peter van 't Hof's avatar
Peter van 't Hof committed
127
  ).map(x => ExtFile("/nl/lumc/sasc/biopet/core/report/ext/" + x, x))
128

129
  /** Main function to for building the report */
130
131
132
133
  def main(args: Array[String]): Unit = {
    logger.info("Start")

    val argsParser = new OptParser
Peter van 't Hof's avatar
Peter van 't Hof committed
134
    val cmdArgs: Args = argsParser.parse(args, Args()) getOrElse (throw new IllegalArgumentException)
135
136
137
138

    require(cmdArgs.outputDir.exists(), "Output dir does not exist")
    require(cmdArgs.outputDir.isDirectory, "Output dir is not a directory")

Peter van 't Hof's avatar
Peter van 't Hof committed
139
    cmdArgs.pageArgs.get("sampleId") match {
140
      case Some(s: String) =>
Peter van 't Hof's avatar
Peter van 't Hof committed
141
142
        cmdArgs.pageArgs += "sampleId" -> Some(s)
        _sampleId = Some(s)
Peter van 't Hof's avatar
Peter van 't Hof committed
143
      case _ =>
Peter van 't Hof's avatar
Peter van 't Hof committed
144
145
146
    }

    cmdArgs.pageArgs.get("libId") match {
147
      case Some(l: String) =>
Peter van 't Hof's avatar
Peter van 't Hof committed
148
149
        cmdArgs.pageArgs += "libId" -> Some(l)
        _libId = Some(l)
Peter van 't Hof's avatar
Peter van 't Hof committed
150
      case _ =>
Peter van 't Hof's avatar
Peter van 't Hof committed
151
152
    }

153
154
    logger.info("Copy Base files")

Peter van 't Hof's avatar
Peter van 't Hof committed
155
    // Static files that will be copied to the output folder, then file is added to [resourceDir] it's need to be added here also
156
157
    val extOutputDir: File = new File(cmdArgs.outputDir, "ext")

158
159
160
161
162
163
164
165
    // Copy each resource files out to the report destination
    extFiles.par.foreach(
      resource =>
        IoUtils.copyStreamToFile(
          getClass.getResourceAsStream(resource.resourcePath),
          new File(extOutputDir, resource.targetPath),
          createDirs = true)
    )
166

Peter van 't Hof's avatar
Peter van 't Hof committed
167
    logger.info("Parsing summary")
168
169
    setSummary = new Summary(cmdArgs.summary)

170
    total = ReportBuilder.countPages(indexPage)
Peter van 't Hof's avatar
Peter van 't Hof committed
171
172
    logger.info(total + " pages to be generated")

Peter van 't Hof's avatar
Peter van 't Hof committed
173
174
    done = 0

Peter van 't Hof's avatar
Peter van 't Hof committed
175
    logger.info("Generate pages")
Peter van 't Hof's avatar
Peter van 't Hof committed
176
    val jobs = generatePage(summary, indexPage, cmdArgs.outputDir,
Peter van 't Hof's avatar
Peter van 't Hof committed
177
      args = pageArgs ++ cmdArgs.pageArgs.toMap ++
178
        Map("summary" -> summary, "reportName" -> reportName, "indexPage" -> indexPage))
179

Peter van 't Hof's avatar
Peter van 't Hof committed
180
    logger.info(jobs + " Done")
181
182
  }

183
  /** This must be implemented, this will be the root page of the report */
184
185
  def indexPage: ReportPage

186
  /** This must be implemented, this will become the title of the report */
187
188
  def reportName: String

189
190
  /**
   * This method will render the page and the subpages recursivly
Peter van 't Hof's avatar
Peter van 't Hof committed
191
   *
192
193
194
195
196
197
198
   * @param summary The summary object
   * @param page Page to render
   * @param outputDir Root output dir of the report
   * @param path Path from root to current page
   * @param args Args to add to this sub page, are args from current page are passed automaticly
   * @return Number of pages including all subpages that are rendered
   */
199
200
201
202
  def generatePage(summary: Summary,
                   page: ReportPage,
                   outputDir: File,
                   path: List[String] = Nil,
Peter van 't Hof's avatar
Peter van 't Hof committed
203
                   args: Map[String, Any] = Map()): Int = {
204

Peter van 't Hof's avatar
Peter van 't Hof committed
205
206
    val pageOutputDir = new File(outputDir, path.mkString(File.separator))
    pageOutputDir.mkdirs()
207
    val rootPath = "./" + Array.fill(path.size)("../").mkString
208
209
210
211
212
213
    val pageArgs = args ++ page.args ++
      Map("page" -> page,
        "path" -> path,
        "outputDir" -> pageOutputDir,
        "rootPath" -> rootPath
      )
214

215
    // Generating subpages
216
217
218
    val jobs = page.subPages.par.flatMap {
      case (name, subPage) => Some(generatePage(summary, subPage, outputDir, path ::: name :: Nil, pageArgs))
      case _               => None
219
220
    }

Peter van 't Hof's avatar
Peter van 't Hof committed
221
222
223
224
    val output = ReportBuilder.renderTemplate("/nl/lumc/sasc/biopet/core/report/main.ssp",
      pageArgs ++ Map("args" -> pageArgs))

    val file = new File(pageOutputDir, "index.html")
225
226
227
228
    val writer = new PrintWriter(file)
    writer.println(output)
    writer.close()

Peter van 't Hof's avatar
Peter van 't Hof committed
229
230
    done += 1
    if (done % 100 == 0) logger.info(done + " Done, " + (done.toDouble / total * 100) + "%")
231
    jobs.sum + 1
232
233
  }
}
Peter van 't Hof's avatar
Peter van 't Hof committed
234
235
236

object ReportBuilder {

237
  /** Single template render engine, this will have a cache for all compile templates */
Peter van 't Hof's avatar
Peter van 't Hof committed
238
  protected val engine = new TemplateEngine()
239
  engine.allowReload = false
240

241
242
243
244
245
  /** This will give the total number of pages including all nested pages */
  def countPages(page: ReportPage): Int = {
    page.subPages.map(x => countPages(x._2)).fold(1)(_ + _)
  }

Peter van 't Hof's avatar
Peter van 't Hof committed
246
247
248
249
250
251
252
  /**
   * This method will render a template that is located in the classpath / jar
   * @param location location in the classpath / jar
   * @param args Additional arguments, not required
   * @return Rendered result of template
   */
  def renderTemplate(location: String, args: Map[String, Any] = Map()): String = {
Peter van 't Hof's avatar
Peter van 't Hof committed
253
254
    Logging.logger.info("Rendering: " + location)

255
    engine.layout(location, args)
256
  }
Peter van 't Hof's avatar
Peter van 't Hof committed
257
}