/scalate-core/src/main/scala/org/fusesource/scalate/TemplateEngine.scala

http://github.com/scalate/scalate · Scala · 998 lines · 568 code · 133 blank · 297 comment · 67 complexity · 5abacb094d173534537fa4dbadd9c8f8 MD5 · raw file

  1. /**
  2. * Copyright (C) 2009-2011 the original author or authors.
  3. * See the notice.md file distributed with this work for additional
  4. * information regarding copyright ownership.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. package org.fusesource.scalate
  19. import java.io.{ File, PrintWriter, StringWriter }
  20. import java.net.URLClassLoader
  21. import java.util.concurrent.ConcurrentHashMap
  22. import java.util.concurrent.atomic.AtomicBoolean
  23. import org.fusesource.scalate.filter._
  24. import org.fusesource.scalate.jade.JadeCodeGenerator
  25. import org.fusesource.scalate.layout.{ LayoutStrategy, NullLayoutStrategy }
  26. import org.fusesource.scalate.mustache.MustacheCodeGenerator
  27. import org.fusesource.scalate.scaml.ScamlCodeGenerator
  28. import org.fusesource.scalate.ssp.SspCodeGenerator
  29. import org.fusesource.scalate.support._
  30. import org.fusesource.scalate.util._
  31. import scala.collection.immutable.TreeMap
  32. import scala.collection.mutable.HashMap
  33. import scala.language.existentials
  34. import scala.util.control.Exception
  35. import scala.util.parsing.input.{ OffsetPosition, Position }
  36. import scala.xml.NodeSeq
  37. object TemplateEngine {
  38. val log = Log(getClass)
  39. def apply(sourceDirectories: Iterable[File], mode: String): TemplateEngine = {
  40. new TemplateEngine(sourceDirectories, mode)
  41. }
  42. /**
  43. * The default template types available in Scalate
  44. */
  45. val templateTypes: List[String] = List("mustache", "ssp", "scaml", "jade")
  46. }
  47. /**
  48. * A TemplateEngine is used to compile and load Scalate templates.
  49. * The TemplateEngine takes care of setting up the Scala compiler
  50. * and caching compiled templates for quicker subsequent loads
  51. * of a requested template.
  52. *
  53. * The TemplateEngine uses a ''workingDirectory'' to store the generated scala source code and the bytecode. By default
  54. * this uses a dynamically generated directory. You can configure this yourself to use whatever directory you wish.
  55. * Or you can use the ''scalate.workdir'' system property to specify the workingDirectory
  56. *
  57. * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
  58. */
  59. class TemplateEngine(
  60. var sourceDirectories: Iterable[File] = None,
  61. var mode: String = System.getProperty("scalate.mode", "production")) {
  62. import TemplateEngine.log._
  63. private case class CacheEntry(
  64. template: Template,
  65. dependencies: Set[String],
  66. timestamp: Long) {
  67. def isStale() = timestamp != 0 && dependencies.exists {
  68. resourceLoader.lastModified(_) > timestamp
  69. }
  70. }
  71. /**
  72. * Whether or not markup sensitive characters for HTML/XML elements like &amp; &gt; &lt; are escaped or not
  73. */
  74. var escapeMarkup = true
  75. /**
  76. * Set to false if you don't want the template engine to ever cache any of the compiled templates.
  77. *
  78. * If not explicitly configured this property can be configured using the ''scalate.allowCaching'' system property
  79. */
  80. var allowCaching = "true" == System.getProperty("scalate.allowCaching", "true")
  81. /**
  82. * If true, then the template engine will check to see if the template has been updated since last compiled
  83. * so that it can be reloaded. Defaults to true. YOu should set to false in production environments since
  84. * the templates should not be changing.
  85. *
  86. * If not explicitly configured this property can be configured using the ''scalate.allowReload'' system property
  87. */
  88. var allowReload = "true" == System.getProperty("scalate.allowReload", "true")
  89. private[this] var compilerInstalled = true
  90. /**
  91. * Whether a custom classpath should be combined with the deduced classpath
  92. */
  93. var combinedClassPath = false
  94. /**
  95. * Sets the import statements used in each generated template class
  96. */
  97. var importStatements: List[String] = List(
  98. "import _root_.scala.collection.JavaConverters._",
  99. "import _root_.org.fusesource.scalate.support.TemplateConversions._",
  100. "import _root_.org.fusesource.scalate.util.Measurements._")
  101. /**
  102. * Loads resources such as the templates based on URIs
  103. */
  104. var resourceLoader: ResourceLoader = FileResourceLoader(sourceDirectoriesForwarder)
  105. /**
  106. * A list of directories which are searched to load requested templates.
  107. */
  108. var templateDirectories = List("")
  109. var packagePrefix = ""
  110. var bootClassName = "scalate.Boot"
  111. var bootInjections: List[AnyRef] = List(this)
  112. private[this] val booted = new AtomicBoolean()
  113. def boot(): Unit = {
  114. if (booted.compareAndSet(false, true)) {
  115. if (allowReload) {
  116. // Is the Scala compiler on the class path?
  117. try {
  118. getClass.getClassLoader.loadClass("scala.tools.nsc.settings.ScalaSettings")
  119. } catch {
  120. case e: Throwable =>
  121. // if it's not, then disable class reloading..
  122. debug("Scala compiler not found on the class path. Template reloading disabled.")
  123. allowReload = false
  124. compilerInstalled = false
  125. }
  126. }
  127. ClassLoaders.findClass(bootClassName, List(classLoader, Thread.currentThread.getContextClassLoader)) match {
  128. case Some(clazz) =>
  129. Boots.invokeBoot(clazz, bootInjections)
  130. case _ =>
  131. info("No bootstrap class " + bootClassName + " found on classloader: " + classLoader)
  132. }
  133. }
  134. }
  135. /**
  136. * A forwarder so we can refer to whatever the current latest value of sourceDirectories is even if the value
  137. * is mutated after the TemplateEngine is constructed
  138. */
  139. protected def sourceDirectoriesForwarder = {
  140. this.sourceDirectories
  141. }
  142. /**
  143. * The supported template engines and their default extensions
  144. */
  145. var codeGenerators: Map[String, CodeGenerator] = Map("ssp" -> new SspCodeGenerator, "scaml" -> new ScamlCodeGenerator,
  146. "mustache" -> new MustacheCodeGenerator, "jade" -> new JadeCodeGenerator)
  147. var filters: Map[String, Filter] = Map()
  148. def filter(name: String) = codeGenerators.get(name).map(gen =>
  149. new Filter() {
  150. def filter(context: RenderContext, content: String) = {
  151. context.capture(compileText(name, content))
  152. }
  153. }).orElse(filters.get(name))
  154. var pipelines: Map[String, List[Filter]] = Map()
  155. /**
  156. * Maps file extensions to possible template extensions for custom mappins such as for
  157. * Map("js" -> Set("coffee"), "css" => Set("sass", "scss"))
  158. */
  159. var extensionToTemplateExtension: collection.mutable.Map[String, collection.mutable.Set[String]] = collection.mutable.Map()
  160. /**
  161. * Returns the mutable set of template extensions which are mapped to the given URI extension.
  162. */
  163. def templateExtensionsFor(extension: String): collection.mutable.Set[String] = {
  164. extensionToTemplateExtension.getOrElseUpdate(extension, collection.mutable.Set())
  165. }
  166. private[this] val attempt = Exception.ignoring(classOf[Throwable])
  167. /**
  168. * Returns the file extensions understood by Scalate; all the template engines and pipelines including
  169. * the wiki markup languages.
  170. */
  171. def extensions: Set[String] = (codeGenerators.keySet ++ pipelines.keySet).toSet
  172. // Attempt to load all the built in filters.. Some may not load do to missing classpath
  173. // dependencies.
  174. attempt(filters += "plain" -> PlainFilter)
  175. attempt(filters += "javascript" -> JavascriptFilter)
  176. attempt(filters += "coffeescript" -> CoffeeScriptFilter)
  177. attempt(filters += "css" -> CssFilter)
  178. attempt(filters += "cdata" -> CdataFilter)
  179. attempt(filters += "escaped" -> EscapedFilter)
  180. attempt {
  181. CoffeeScriptPipeline(this)
  182. }
  183. var layoutStrategy: LayoutStrategy = NullLayoutStrategy
  184. lazy val compiler = createCompiler
  185. var compilerInitialized = false
  186. /**
  187. * Factory method to create a compiler for this TemplateEngine.
  188. * Override if you wish to contorl the compilation in a different way
  189. * such as in side SBT or something.
  190. */
  191. protected def createCompiler: Compiler = {
  192. compilerInitialized = true
  193. ScalaCompiler.create(this)
  194. }
  195. def shutdown() = if (compilerInitialized) compiler.shutdown
  196. def sourceDirectory = new File(workingDirectory, "src")
  197. def bytecodeDirectory = new File(workingDirectory, "classes")
  198. def libraryDirectory = new File(workingDirectory, "lib")
  199. def tmpDirectory = new File(workingDirectory, "tmp")
  200. var classpath: String = null
  201. private[this] var _workingDirectory: File = null
  202. var classLoader = getClass().getClassLoader()
  203. /**
  204. * By default lets bind the context so we get to reuse its methods in a template
  205. */
  206. var bindings = Binding("context", "_root_." + classOf[RenderContext].getName, true, None, "val", false) :: Nil
  207. val finderCache = new ConcurrentHashMap[String, String]
  208. private[this] val templateCache = new HashMap[String, CacheEntry]
  209. private[this] var _cacheHits = 0
  210. private[this] var _cacheMisses = 0
  211. // Discover bits that can enhance the default template engine configuration. (like filters)
  212. ClassFinder.discoverCommands[TemplateEngineAddOn]("META-INF/services/org.fusesource.scalate/addon.index").foreach { addOn =>
  213. debug("Installing Scalate add on " + addOn.getClass)
  214. addOn(this)
  215. }
  216. override def toString = getClass.getSimpleName + "(sourceDirectories: " + sourceDirectories + ")"
  217. /**
  218. * Returns true if this template engine is being used in development mode.
  219. */
  220. def isDevelopmentMode = mode != null && mode.toLowerCase.startsWith("d")
  221. /**
  222. * If not explicitly configured this will default to using the ''scalate.workdir'' system property to specify the
  223. * directory used for generating the scala source code and compiled bytecode - otherwise a temporary directory is used
  224. */
  225. def workingDirectory: File = {
  226. // Use a temp working directory if none is configured.
  227. if (_workingDirectory == null) {
  228. val value = System.getProperty("scalate.workdir", "")
  229. if (value != null && value.length > 0) {
  230. _workingDirectory = new File(value)
  231. } else {
  232. val f = File.createTempFile("scalate-", "-workdir")
  233. // now lets delete the file so we can make a new directory there instead
  234. f.delete
  235. if (f.mkdirs) {
  236. _workingDirectory = f
  237. f.deleteOnExit
  238. } else {
  239. warn("Could not delete file %s so we could create a temp directory", f)
  240. _workingDirectory = new File(new File(System.getProperty("java.io.tmpdir")), "_scalate")
  241. }
  242. }
  243. }
  244. _workingDirectory
  245. }
  246. def workingDirectory_=(value: File) = {
  247. this._workingDirectory = value
  248. }
  249. /**
  250. * Compiles the given Moustache template text and returns the template
  251. */
  252. def compileMoustache(text: String, extraBindings: Iterable[Binding] = Nil): Template = {
  253. compileText("mustache", text, extraBindings)
  254. }
  255. /**
  256. * Compiles the given SSP template text and returns the template
  257. */
  258. def compileSsp(text: String, extraBindings: Iterable[Binding] = Nil): Template = {
  259. compileText("ssp", text, extraBindings)
  260. }
  261. /**
  262. * Compiles the given SSP template text and returns the template
  263. */
  264. def compileScaml(text: String, extraBindings: Iterable[Binding] = Nil): Template = {
  265. compileText("scaml", text, extraBindings)
  266. }
  267. /**
  268. * Compiles the given text using the given extension (such as ssp or scaml for example to denote what parser to use)
  269. * and return the template
  270. */
  271. def compileText(extension: String, text: String, extraBindings: Iterable[Binding] = Nil): Template = {
  272. tmpDirectory.mkdirs()
  273. val file = File.createTempFile("_scalate_tmp_", "." + extension, tmpDirectory)
  274. IOUtil.writeText(file, text)
  275. val loader = FileResourceLoader(List(tmpDirectory))
  276. compile(TemplateSource.fromUri(file.getName, loader), extraBindings)
  277. }
  278. /**
  279. * Compiles a template source without placing it in the template cache. Useful for temporary
  280. * templates or dynamically created template
  281. */
  282. def compile(source: TemplateSource, extraBindings: Iterable[Binding] = Nil): Template = {
  283. compileAndLoad(source, extraBindings, 0)._1
  284. }
  285. /**
  286. * Generates the Scala code for a template. Useful for generating scala code that
  287. * will then be compiled into the application as part of a build process.
  288. */
  289. def generateScala(source: TemplateSource, extraBindings: Iterable[Binding] = Nil) = {
  290. source.engine = this
  291. generator(source).generate(this, source, bindings ++ extraBindings)
  292. }
  293. /**
  294. * Generates the Scala code for a template. Useful for generating scala code that
  295. * will then be compiled into the application as part of a build process.
  296. */
  297. def generateScala(uri: String, extraBindings: Iterable[Binding]): Code = {
  298. generateScala(uriToSource(uri), extraBindings)
  299. }
  300. /**
  301. * Generates the Scala code for a template. Useful for generating scala code that
  302. * will then be compiled into the application as part of a build process.
  303. */
  304. def generateScala(uri: String): Code = {
  305. generateScala(uriToSource(uri))
  306. }
  307. /**
  308. * The number of times a template load request was serviced from the cache.
  309. */
  310. def cacheHits = templateCache.synchronized { _cacheHits }
  311. /**
  312. * The number of times a template load request could not be serviced from the cache
  313. * and was loaded from disk.
  314. */
  315. def cacheMisses = templateCache.synchronized { _cacheMisses }
  316. /**
  317. * Expire specific template source and then compile and cache again
  318. */
  319. def expireAndCompile(source: TemplateSource, extraBindings: Iterable[Binding] = Nil): Unit = {
  320. templateCache.synchronized {
  321. templateCache.get(source.uri) match {
  322. case None => {
  323. sourceMapLog.info(s"${source.uri} not exist in cache just compiling")
  324. cache(source, compileAndLoadEntry(source, extraBindings))
  325. }
  326. case Some(entry) => {
  327. sourceMapLog.info(s"${source.uri} found in cache to expire")
  328. templateCache.remove(source.uri)
  329. cache(source, compileAndLoadEntry(source, extraBindings))
  330. sourceMapLog.info(s"${source.uri} removed from cache and compiled")
  331. }
  332. }
  333. }
  334. }
  335. /**
  336. * Compiles and then caches the specified template. If the template
  337. * was previously cached, the previously compiled template instance
  338. * is returned. The cache entry in invalidated and then template
  339. * is re-compiled if the template file has been updated since
  340. * it was last compiled.
  341. */
  342. def load(
  343. source: TemplateSource,
  344. extraBindings: Iterable[Binding] = Nil): Template = {
  345. source.engine = this
  346. templateCache.synchronized {
  347. // on the first load request, check to see if the INVALIDATE_CACHE JVM option is enabled
  348. if (_cacheHits == 0 && _cacheMisses == 0 && java.lang.Boolean.getBoolean("org.fusesource.scalate.INVALIDATE_CACHE")) {
  349. // this deletes generated scala and class files.
  350. invalidateCachedTemplates
  351. }
  352. // Determine whether to build/rebuild the template, load existing .class files from the file system,
  353. // or reuse an existing template that we've already loaded
  354. templateCache.get(source.uri) match {
  355. // Not in the cache..
  356. case None =>
  357. _cacheMisses += 1
  358. try {
  359. // Try to load a pre-compiled template from the classpath
  360. cache(source, loadPrecompiledEntry(source, extraBindings))
  361. } catch {
  362. case _: Throwable =>
  363. // It was not pre-compiled... compile and load it.
  364. cache(source, compileAndLoadEntry(source, extraBindings))
  365. }
  366. // It was in the cache..
  367. case Some(entry) =>
  368. // check for staleness
  369. if (allowReload && entry.isStale) {
  370. // Cache entry is stale, re-compile it
  371. _cacheMisses += 1
  372. cache(source, compileAndLoadEntry(source, extraBindings))
  373. } else {
  374. // Cache entry is valid
  375. _cacheHits += 1
  376. entry.template
  377. }
  378. }
  379. }
  380. }
  381. /**
  382. * Compiles and then caches the specified template. If the template
  383. * was previously cached, the previously compiled template instance
  384. * is returned. The cache entry in invalidated and then template
  385. * is re-compiled if the template file has been updated since
  386. * it was last compiled.
  387. */
  388. def load(file: File, extraBindings: Iterable[Binding]): Template = {
  389. load(TemplateSource.fromFile(file), extraBindings)
  390. }
  391. /**
  392. * Compiles and then caches the specified template. If the template
  393. * was previously cached, the previously compiled template instance
  394. * is returned. The cache entry in invalidated and then template
  395. * is re-compiled if the template file has been updated since
  396. * it was last compiled.
  397. */
  398. def load(file: File): Template = {
  399. load(TemplateSource.fromFile(file))
  400. }
  401. /**
  402. * Compiles and then caches the specified template. If the template
  403. * was previously cached, the previously compiled template instance
  404. * is returned. The cache entry in invalidated and then template
  405. * is re-compiled if the template file has been updated since
  406. * it was last compiled.
  407. */
  408. def load(uri: String, extraBindings: Iterable[Binding]): Template = {
  409. load(uriToSource(uri), extraBindings)
  410. }
  411. /**
  412. * Compiles and then caches the specified template. If the template
  413. * was previously cached, the previously compiled template instance
  414. * is returned. The cache entry in invalidated and then template
  415. * is re-compiled if the template file has been updated since
  416. * it was last compiled.
  417. */
  418. def load(uri: String): Template = {
  419. load(uriToSource(uri))
  420. }
  421. /**
  422. * Returns a template source for the given URI and current resourceLoader
  423. */
  424. def source(uri: String): TemplateSource = TemplateSource.fromUri(uri, resourceLoader)
  425. /**
  426. * Returns a template source of the given type of template for the given URI and current resourceLoader
  427. */
  428. def source(uri: String, templateType: String): TemplateSource = source(uri).templateType(templateType)
  429. /**
  430. * Returns true if the URI can be loaded as a template
  431. */
  432. def canLoad(source: TemplateSource, extraBindings: Iterable[Binding] = Nil): Boolean = {
  433. try {
  434. load(source, extraBindings) != null
  435. } catch {
  436. case e: ResourceNotFoundException => false
  437. }
  438. }
  439. /**
  440. * Returns true if the URI can be loaded as a template
  441. */
  442. def canLoad(uri: String): Boolean = {
  443. canLoad(uriToSource(uri))
  444. }
  445. /**
  446. * Returns true if the URI can be loaded as a template
  447. */
  448. def canLoad(uri: String, extraBindings: Iterable[Binding]): Boolean = {
  449. canLoad(uriToSource(uri), extraBindings)
  450. }
  451. /**
  452. * Invalidates all cached Templates.
  453. */
  454. def invalidateCachedTemplates() = {
  455. templateCache.synchronized {
  456. templateCache.clear
  457. finderCache.clear
  458. IOUtil.recursiveDelete(sourceDirectory)
  459. IOUtil.recursiveDelete(bytecodeDirectory)
  460. sourceDirectory.mkdirs
  461. bytecodeDirectory.mkdirs
  462. }
  463. }
  464. // Layout as text methods
  465. //-------------------------------------------------------------------------
  466. /**
  467. * Renders the given template URI using the current layoutStrategy
  468. */
  469. def layout(uri: String, context: RenderContext, extraBindings: Iterable[Binding]): Unit = {
  470. val template = load(uri, extraBindings)
  471. layout(template, context)
  472. }
  473. /**
  474. * Renders the given template using the current layoutStrategy
  475. */
  476. def layout(template: Template, context: RenderContext): Unit = {
  477. RenderContext.using(context) {
  478. val source = template.source
  479. if (source != null && source.uri != null) {
  480. context.withUri(source.uri) {
  481. layoutStrategy.layout(template, context)
  482. }
  483. } else {
  484. layoutStrategy.layout(template, context)
  485. }
  486. }
  487. }
  488. /**
  489. * Renders the given template URI returning the output
  490. */
  491. def layout(
  492. uri: String,
  493. attributes: Map[String, Any] = Map(),
  494. extraBindings: Iterable[Binding] = Nil): String = {
  495. val template = load(uri, extraBindings)
  496. layout(uri, template, attributes)
  497. }
  498. def layout(
  499. uri: String,
  500. out: PrintWriter,
  501. attributes: Map[String, Any]): Unit = {
  502. val template = load(uri)
  503. layout(uri, template, out, attributes)
  504. }
  505. protected def layout(
  506. uri: String,
  507. template: Template,
  508. out: PrintWriter,
  509. attributes: Map[String, Any]): Unit = {
  510. val context = createRenderContext(uri, out)
  511. for ((key, value) <- attributes) {
  512. context.attributes(key) = value
  513. }
  514. layout(template, context)
  515. }
  516. /**
  517. * Renders the given template returning the output
  518. */
  519. def layout(
  520. uri: String,
  521. template: Template,
  522. attributes: Map[String, Any]): String = {
  523. val buffer = new StringWriter()
  524. val out = new PrintWriter(buffer)
  525. layout(uri, template, out, attributes)
  526. buffer.toString
  527. }
  528. // can't use multiple methods with default arguments so lets manually expand them here...
  529. def layout(uri: String, context: RenderContext): Unit = layout(uri, context, Nil)
  530. def layout(uri: String, template: Template): String = layout(uri, template, Map[String, Any]())
  531. /**
  532. * Renders the given template source using the current layoutStrategy
  533. */
  534. def layout(source: TemplateSource): String = layout(source, Map[String, Any]())
  535. /**
  536. * Renders the given template source using the current layoutStrategy
  537. */
  538. def layout(source: TemplateSource, attributes: Map[String, Any]): String = {
  539. val template = load(source)
  540. layout(source.uri, template, attributes)
  541. }
  542. /**
  543. * Renders the given template source using the current layoutStrategy
  544. */
  545. def layout(
  546. source: TemplateSource,
  547. context: RenderContext,
  548. extraBindings: Iterable[Binding]): Unit = {
  549. val template = load(source, extraBindings)
  550. layout(template, context)
  551. }
  552. /**
  553. * Renders the given template source using the current layoutStrategy
  554. */
  555. def layout(source: TemplateSource, context: RenderContext): Unit = {
  556. val template = load(source)
  557. layout(template, context)
  558. }
  559. // Layout as markup methods
  560. //-------------------------------------------------------------------------
  561. /**
  562. * Renders the given template URI returning the output
  563. */
  564. def layoutAsNodes(
  565. uri: String,
  566. attributes: Map[String, Any] = Map(),
  567. extraBindings: Iterable[Binding] = Nil): NodeSeq = {
  568. val template = load(uri, extraBindings)
  569. layoutAsNodes(uri, template, attributes)
  570. }
  571. /**
  572. * Renders the given template returning the output
  573. */
  574. def layoutAsNodes(
  575. uri: String,
  576. template: Template,
  577. attributes: Map[String, Any]): NodeSeq = {
  578. // TODO there is a much better way of doing this by adding native NodeSeq
  579. // support into the generated templates - especially for Scaml!
  580. // for now lets do it a crappy way...
  581. val buffer = new StringWriter()
  582. val out = new PrintWriter(buffer)
  583. val context = createRenderContext(uri, out)
  584. for ((key, value) <- attributes) {
  585. context.attributes(key) = value
  586. }
  587. //layout(template, context)
  588. context.captureNodeSeq(template)
  589. }
  590. // can't use multiple methods with default arguments so lets manually expand them here...
  591. def layoutAsNodes(uri: String, template: Template): NodeSeq = layoutAsNodes(uri, template, Map[String, Any]())
  592. /**
  593. * Factory method used by the layout helper methods that should be overloaded by template engine implementations
  594. * if they wish to customize the render context implementation
  595. */
  596. protected def createRenderContext(uri: String, out: PrintWriter): RenderContext = new DefaultRenderContext(uri, this, out)
  597. private def loadPrecompiledEntry(
  598. source: TemplateSource,
  599. extraBindings: Iterable[Binding]) = {
  600. source.engine = this
  601. val className = source.className
  602. val template = loadCompiledTemplate(className, allowCaching)
  603. template.source = source
  604. if (allowCaching && allowReload && resourceLoader.exists(source.uri)) {
  605. // Even though the template was pre-compiled, it may go or is stale
  606. // We still need to parse the template to figure out it's dependencies..
  607. val code = generateScala(source, extraBindings)
  608. val entry = CacheEntry(template, code.dependencies, lastModified(template.getClass))
  609. if (entry.isStale) {
  610. // Throw an exception since we should not load stale pre-compiled classes.
  611. throw new StaleCacheEntryException(source)
  612. }
  613. // Yay the template is not stale. Lets use it.
  614. entry
  615. } else {
  616. // If we are not going to be cache reloading.. then we
  617. // don't need to do the extra work.
  618. CacheEntry(template, Set(), 0)
  619. }
  620. }
  621. private def compileAndLoadEntry(
  622. source: TemplateSource,
  623. extraBindings: Iterable[Binding]) = {
  624. val (template, dependencies) = compileAndLoad(source, extraBindings, 0)
  625. CacheEntry(template, dependencies, System.currentTimeMillis)
  626. }
  627. private def cache(source: TemplateSource, ce: CacheEntry): Template = {
  628. if (allowCaching) {
  629. templateCache += (source.uri -> ce)
  630. }
  631. val answer = ce.template
  632. debug("Loaded uri: " + source.uri + " template: " + answer)
  633. answer
  634. }
  635. /**
  636. * Returns the source file of the template URI
  637. */
  638. protected def sourceFileName(uri: String) = {
  639. // Write the source code to file..
  640. // to avoid paths like foo/bar/C:/whatnot on windows lets mangle the ':' character
  641. new File(sourceDirectory, uri.replace(':', '_') + ".scala")
  642. }
  643. protected def classFileName(uri: String) = {
  644. // Write the source code to file..
  645. // to avoid paths like foo/bar/C:/whatnot on windows lets mangle the ':' character
  646. new File(sourceDirectory, uri.replace(':', '_') + ".scala")
  647. }
  648. protected val sourceMapLog = Log(getClass, "SourceMap")
  649. private def compileAndLoad(
  650. source: TemplateSource,
  651. extraBindings: Iterable[Binding],
  652. attempt: Int): (Template, Set[String]) = {
  653. source.engine = this
  654. var code: Code = null
  655. try {
  656. val uri = source.uri
  657. // Can we use a pipeline to process the request?
  658. pipeline(source) match {
  659. case Some(p) =>
  660. val template = new PipelineTemplate(p, source.text)
  661. template.source = source
  662. return (template, Set(uri))
  663. case None =>
  664. }
  665. if (!compilerInstalled) {
  666. throw new ResourceNotFoundException(
  667. "Scala compiler not on the classpath. You must either add it to the classpath or precompile all the templates")
  668. }
  669. val g = generator(source)
  670. // Generate the scala source code from the template
  671. code = g.generate(this, source, bindings ++ extraBindings)
  672. val sourceFile = sourceFileName(uri)
  673. sourceFile.getParentFile.mkdirs
  674. IOUtil.writeBinaryFile(sourceFile, code.source.getBytes("UTF-8"))
  675. // Compile the generated scala code
  676. compiler.compile(sourceFile)
  677. // Write the source map information to the class file
  678. val sourceMap = buildSourceMap(g.stratumName, uri, sourceFile, code.positions)
  679. sourceMapLog.debug("installing:" + sourceMap)
  680. storeSourceMap(new File(bytecodeDirectory, code.className.replace('.', '/') + ".class"), sourceMap)
  681. storeSourceMap(new File(bytecodeDirectory, code.className.replace('.', '/') + "$.class"), sourceMap)
  682. // Load the compiled class and instantiate the template object
  683. val template = loadCompiledTemplate(code.className)
  684. template.source = source
  685. (template, code.dependencies)
  686. } catch {
  687. // TODO: figure out why we sometimes get these InstantiationException errors that
  688. // go away if you redo
  689. case e: InstantiationException =>
  690. if (attempt == 0) {
  691. compileAndLoad(source, extraBindings, 1)
  692. } else {
  693. throw new TemplateException(e.getMessage, e)
  694. }
  695. case e: CompilerException =>
  696. // Translate the scala error location info
  697. // to the template locations..
  698. def template_pos(pos: Position) = {
  699. pos match {
  700. case p: OffsetPosition => {
  701. val filtered = code.positions.filterKeys(code.positions.ordering.compare(_, p) <= 0)
  702. if (filtered.isEmpty) {
  703. null
  704. } else {
  705. val (key, value) = filtered.last
  706. // TODO: handle the case where the line is different too.
  707. val colChange = pos.column - key.column
  708. if (colChange >= 0) {
  709. OffsetPosition(value.source, value.offset + colChange)
  710. } else {
  711. pos
  712. }
  713. }
  714. }
  715. case _ => null
  716. }
  717. }
  718. var newmessage = "Compilation failed:\n"
  719. val errors = e.errors.map {
  720. (olderror) =>
  721. val uri = source.uri
  722. val pos = template_pos(olderror.pos)
  723. if (pos == null) {
  724. newmessage += ":" + olderror.pos + " " + olderror.message + "\n"
  725. newmessage += olderror.pos.longString + "\n"
  726. olderror
  727. } else {
  728. newmessage += uri + ":" + pos + " " + olderror.message + "\n"
  729. newmessage += pos.longString + "\n"
  730. // TODO should we pass the source?
  731. CompilerError(uri, olderror.message, pos, olderror)
  732. }
  733. }
  734. error(e)
  735. if (e.errors.isEmpty) {
  736. throw e
  737. } else {
  738. throw new CompilerException(newmessage, errors)
  739. }
  740. case e: InvalidSyntaxException =>
  741. e.source = source
  742. throw e
  743. case e: TemplateException => throw e
  744. case e: ResourceNotFoundException => throw e
  745. case e: Throwable => throw new TemplateException(e.getMessage, e)
  746. }
  747. }
  748. /**
  749. * Gets a pipeline to use for the give uri string by looking up the uri's extension
  750. * in the the pipelines map.
  751. */
  752. protected def pipeline(source: TemplateSource): Option[List[Filter]] = {
  753. //sort the extensions so we match the longest first.
  754. pipelines.keys.toList.sortWith {
  755. case (x, y) => if (x.length == y.length) x.compareTo(y) < 0 else x.length > y.length
  756. }.withFilter(ext => source.uri.endsWith("." + ext)).flatMap { ext =>
  757. pipelines.get(ext)
  758. }.headOption
  759. }
  760. /**
  761. * Gets the code generator to use for the give uri string by looking up the uri's extension
  762. * in the the codeGenerators map.
  763. */
  764. protected def generator(source: TemplateSource): CodeGenerator = {
  765. extension(source) match {
  766. case Some(ext) =>
  767. generatorForExtension(ext)
  768. case None =>
  769. throw new TemplateException("Template file extension missing. Cannot determine which template processor to use.")
  770. }
  771. }
  772. /**
  773. * Extracts the extension from the source's uri though derived engines could override this behaviour to
  774. * auto default missing extensions or performing custom mappings etc.
  775. */
  776. protected def extension(source: TemplateSource): Option[String] = source.templateType
  777. /**
  778. * Returns the code generator for the given file extension
  779. */
  780. protected def generatorForExtension(extension: String) = codeGenerators.get(extension) match {
  781. case None =>
  782. val extensions = pipelines.keySet.toList ::: codeGenerators.keySet.toList
  783. throw new TemplateException("Not a template file extension (" + extensions.mkString(" | ") + "), you requested: " + extension);
  784. case Some(generator) => generator
  785. }
  786. private def loadCompiledTemplate(
  787. className: String,
  788. from_cache: Boolean = true): Template = {
  789. val cl = if (from_cache) {
  790. new URLClassLoader(Array(bytecodeDirectory.toURI.toURL), classLoader)
  791. } else {
  792. classLoader
  793. }
  794. val clazz = try {
  795. cl.loadClass(className)
  796. } catch {
  797. case e: ClassNotFoundException =>
  798. if (packagePrefix == "") {
  799. throw e
  800. } else {
  801. // Try without the package prefix.
  802. cl.loadClass(className.stripPrefix(packagePrefix).stripPrefix("."))
  803. }
  804. }
  805. clazz.asInstanceOf[Class[Template]].getConstructor().newInstance()
  806. }
  807. /**
  808. * Figures out the modification time of the class.
  809. */
  810. private def lastModified(clazz: Class[_]): Long = {
  811. val codeSource = clazz.getProtectionDomain.getCodeSource
  812. if (codeSource != null && codeSource.getLocation.getProtocol == "file") {
  813. val location = new File(codeSource.getLocation.getPath)
  814. if (location.isDirectory) {
  815. val classFile = new File(location, clazz.getName.replace('.', '/') + ".class")
  816. if (classFile.exists) {
  817. return classFile.lastModified
  818. }
  819. } else {
  820. // class is inside an archive.. just use the modification time of the jar
  821. return location.lastModified
  822. }
  823. }
  824. // Bail out
  825. 0
  826. }
  827. protected def buildSourceMap(
  828. stratumName: String,
  829. uri: String,
  830. scalaFile: File,
  831. positions: TreeMap[OffsetPosition, OffsetPosition]) = {
  832. val shortName = uri.split("/").last
  833. val longName = uri.stripPrefix("/")
  834. val stratum: SourceMapStratum = new SourceMapStratum(stratumName)
  835. val fileId = stratum.addFile(shortName, longName)
  836. // build a map of input-line -> List( output-line )
  837. var smap = new TreeMap[Int, List[Int]]()
  838. positions.foreach {
  839. case (out, in) =>
  840. val outs = out.line :: smap.getOrElse(in.line, Nil)
  841. smap += in.line -> outs
  842. }
  843. // sort the output lines..
  844. smap = smap.transform { (x, y) => y.sortWith(_ < _) }
  845. smap.foreach {
  846. case (in, outs) =>
  847. outs.foreach {
  848. out =>
  849. stratum.addLine(in, fileId, 1, out, 1)
  850. }
  851. }
  852. stratum.optimize
  853. val sourceMap: SourceMap = new SourceMap
  854. sourceMap.setOutputFileName(scalaFile.getName)
  855. sourceMap.addStratum(stratum, true)
  856. sourceMap.toString
  857. }
  858. protected def storeSourceMap(classFile: File, sourceMap: String) = {
  859. SourceMapInstaller.store(classFile, sourceMap)
  860. }
  861. /**
  862. * Creates a [[org.fusesource.scalate.TemplateSource]] from a URI
  863. */
  864. protected def uriToSource(uri: String) = TemplateSource.fromUri(uri, resourceLoader)
  865. }