PageRenderTime 84ms CodeModel.GetById 10ms app.highlight 65ms RepoModel.GetById 1ms app.codeStats 1ms

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