org.fusesource.scalate.TemplateEngine.scala Maven / Gradle / Ivy
/*
* Copyright (c) 2009 Matthew Hildebrand
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package org.fusesource.scalate
import filter._
import layout.{NullLayoutStrategy, LayoutStrategy}
import scaml.ScamlCodeGenerator
import ssp.SspCodeGenerator
import support._
import util._
import scala.util.parsing.input.{OffsetPosition, Position}
import scala.collection.mutable.HashMap
import scala.collection.immutable.TreeMap
import scala.util.control.Exception
import scala.compat.Platform
import java.net.URLClassLoader
import java.io.{StringWriter, PrintWriter, FileWriter, File}
import xml.NodeSeq
/**
* A TemplateEngine is used to compile and load Scalate templates.
* The TemplateEngine takes care of setting up the Scala compiler
* and caching compiled templates for quicker subsequent loads
* of a requested template.
*
* @author Hiram Chirino
*/
class TemplateEngine extends Logging {
private case class CacheEntry(template: Template, dependencies: Set[String], timestamp: Long) {
def isStale() = dependencies.exists {
resourceLoader.lastModified(_) > timestamp
}
}
/**
* Set to false if you don't want the template engine to ever cache any of the compiled templates.
*/
var allowCaching = true
/**
* Set to false if you don't want the template engine to see if a previously compiled template needs
* to be reloaded due to it being updated.
*/
var allowReload = true
/**
* Whether a custom classpath should be combined with the deduced classpath
*/
var combinedClassPath = false
/**
* Sets the import statements used in each generated template class
*/
var importStatements: List[String] = List("import _root_.scala.collection.JavaConversions._")
/**
*
*/
var resourceLoader: ResourceLoader = new FileResourceLoader
var codeGenerators: Map[String, CodeGenerator] = Map("ssp" -> new SspCodeGenerator, "scaml" -> new ScamlCodeGenerator)
var filters: Map[String, Filter] = Map()
private val attempt = Exception.ignoring(classOf[Throwable])
// Attempt to load all the built in filters.. Some may not load do to missing classpath
// dependencies.
attempt( filters += "plain" -> PlainFilter )
attempt( filters += "javascript"-> JavascriptFilter )
attempt( filters += "escaped"->EscapedFilter )
attempt( filters += "markdown"->MarkdownFilter )
var layoutStrategy: LayoutStrategy = NullLayoutStrategy
lazy val compiler = new ScalaCompiler(bytecodeDirectory, classpath, combinedClassPath)
def sourceDirectory = new File(workingDirectory, "src")
def bytecodeDirectory = new File(workingDirectory, "classes")
var classpath: String = null
private var _workingDirectory: File = null
def workingDirectory: File = {
// Use a temp working directory if none is configured.
if( _workingDirectory == null ) {
val value = System.getProperty("scalate.workdir", "")
if (value != null && value.length > 0) {
_workingDirectory = new File(value)
_workingDirectory.mkdirs
}
else {
_workingDirectory = new File(new File(System.getProperty("java.io.tmpdir")), "_scalate");
}
}
_workingDirectory
}
def workingDirectory_=(value:File) = {
this._workingDirectory = value
}
var classLoader = this.getClass.getClassLoader
/**
* Hy default lets bind the context so we get to reuse its methods in a template
*/
var bindings = Binding("context", classOf[RenderContext].getName, true, None, "val", false) :: Nil
private val templateCache = new HashMap[String, CacheEntry]
/**
* Compiles the given SSP template text and returns the template
*/
def compileSsp(text: String, extraBindings:List[Binding] = Nil):Template = {
compileText("ssp", text, extraBindings)
}
/**
* Compiles the given SSP template text and returns the template
*/
def compileScaml(text: String, extraBindings:List[Binding] = Nil):Template = {
compileText("scaml", text, extraBindings)
}
/**
* Compiles the given text using the given extension (such as ssp or scaml for example to denote what parser to use)
* and return the template
*/
def compileText(extension: String, text: String, extraBindings:List[Binding] = Nil):Template = {
val file = File.createTempFile("scalate", "." + extension)
val writer = new FileWriter(file)
writer.write(text)
writer.close()
compile(file.getAbsolutePath, extraBindings)
}
/**
* Compiles a template file/URI without placing it in the template cache. Useful for temporary
* templates or dynamically created template
*/
def compile(uri: String, extraBindings:List[Binding] = Nil):Template = {
compileAndLoad(uri, extraBindings, 0)._1
}
/**
* Generates the Scala code for a template. Useful for generating scala code that
* will then be compiled into the application as part of a build process.
*/
def generateScala(uri: String, extraBindings:List[Binding] = Nil) = {
generator(uri).generate(this, uri, bindings ::: extraBindings)
}
/**
* Compiles and then caches the specified template. If the template
* was previously cached, the previously compiled template instance
* is returned. The cache entry in invalidated and then template
* is re-compiled if the template file has been updated since
* it was last compiled.
*/
def load(uri: String, extraBindings:List[Binding]= Nil): Template = {
templateCache.synchronized {
// Determine whether to build/rebuild the template, load existing .class files from the file system,
// or reuse an existing template that we've already loaded
templateCache.get(uri) match {
// Not in the cache..
case None =>
try {
// Try to load a pre-compiled template from the classpath
cache(uri, loadPrecompiledEntry(uri, extraBindings))
} catch {
case _: Throwable =>
// It was not pre-compiled... compile and load it.
cache(uri, compileAndLoadEntry(uri, extraBindings))
}
// It was in the cache..
case Some(entry) =>
// check for staleness
if (allowReload && entry.isStale)
// re-compile it
cache(uri, compileAndLoadEntry(uri, extraBindings))
else
// Cache entry is valid
entry.template
}
}
}
/**
* Compiles and then caches the specified template. If the template
* was previously cached, the previously compiled template instance
* is returned. The cache entry in invalidated and then template
* is re-compiled if the template file has been updated since
* it was last compiled.
*/
def load(file: File, extraBindings: List[Binding]): Template = {
load(file.getPath, extraBindings)
}
/**
* Compiles and then caches the specified template. If the template
* was previously cached, the previously compiled template instance
* is returned. The cache entry in invalidated and then template
* is re-compiled if the template file has been updated since
* it was last compiled.
*/
def load(file: File): Template = {
load(file.getPath, Nil)
}
/**
* Returns true if the URI can be loaded as a template
*/
def canLoad(uri: String, extraBindings:List[Binding]= Nil): Boolean = {
try {
load(uri, extraBindings) != null
} catch {
case e: ResourceNotFoundException => false
}
}
/**
* Invalidates any cached Templates
*/
def invalidateCachedTemplates() = {
templateCache.synchronized {
templateCache.clear
}
}
// Layout as text methods
//-------------------------------------------------------------------------
/**
* Renders the given template URI using the current layoutStrategy
*/
def layout(uri: String, context: RenderContext, extraBindings:List[Binding]): Unit = {
val template = load(uri, extraBindings)
layout(template, context)
}
/**
* Renders the given template using the current layoutStrategy
*/
def layout(template: Template, context: RenderContext): Unit = {
RenderContext.update(context)
layoutStrategy.layout(template, context)
}
/**
* Renders the given template URI returning the output
*/
def layout(uri: String, attributes: Map[String,Any] = Map(), extraBindings:List[Binding] = Nil): String = {
val template = load(uri, extraBindings)
layout(template, attributes)
}
/**
* Renders the given template returning the output
*/
def layout(template: Template, attributes: Map[String,Any]): String = {
val buffer = new StringWriter()
val out = new PrintWriter(buffer)
val context = new DefaultRenderContext(this, out)
for ((key, value) <- attributes) {
context.attributes(key) = value
}
layout(template, context)
buffer.toString
}
// can't use multiple methods with default arguments so lets manually expand them here...
def layout(uri: String, context: RenderContext): Unit = layout(uri, context, Nil)
def layout(template: Template): String = layout(template, Map[String,Any]())
// Layout as markup methods
//-------------------------------------------------------------------------
/**
* Renders the given template URI returning the output
*/
def layoutAsNodes(uri: String, attributes: Map[String,Any] = Map(), extraBindings:List[Binding] = Nil): NodeSeq = {
val template = load(uri, extraBindings)
layoutAsNodes(template, attributes)
}
/**
* Renders the given template returning the output
*/
def layoutAsNodes(template: Template, attributes: Map[String,Any]): NodeSeq = {
// TODO there is a much better way of doing this by adding native NodeSeq
// support into the generated templates - especially for Scaml!
// for now lets do it a crappy way...
val buffer = new StringWriter()
val out = new PrintWriter(buffer)
val context = new DefaultRenderContext(this, out)
for ((key, value) <- attributes) {
context.attributes(key) = value
}
//layout(template, context)
context.captureNodeSeq(template)
}
// can't use multiple methods with default arguments so lets manually expand them here...
def layoutAsNodes(template: Template): NodeSeq = layoutAsNodes(template, Map[String,Any]())
private def loadPrecompiledEntry(uri:String, extraBindings:List[Binding]) = {
val className = generator(uri).className(uri)
val template = loadCompiledTemplate(className);
if( allowCaching && allowReload) {
// Even though the template was pre-compiled, it may go or is stale
// We still need to parse the template to figure out it's dependencies..
val code = generateScala(uri, extraBindings);
val entry = CacheEntry(template, code.dependencies, lastModified(template.getClass))
if( entry.isStale ) {
// Throw an exception since we should not load stale pre-compiled classes.
throw new StaleCacheEntryException(uri)
}
// Yay the template is not stale. Lets use it.
entry
} else {
// If we are not going to be cache reloading.. then we
// don't need to do the extra work.
CacheEntry(template, Set(), 0)
}
}
private def compileAndLoadEntry(uri:String, extraBindings:List[Binding]) = {
val (template, dependencies) = compileAndLoad(uri, extraBindings, 0)
CacheEntry(template, dependencies, Platform.currentTime)
}
private def cache(uri:String, ce:CacheEntry) :Template = {
if( allowCaching ) {
templateCache += (uri -> ce)
}
ce.template
}
/**
* Returns the source file of the template URI
*/
protected def sourceFileName(uri: String) = {
// Write the source code to file..
// to avoid paths like foo/bar/C:/whatnot on windows lets mangle the ':' character
new File(sourceDirectory, uri.replace(':', '_') + ".scala")
}
protected def classFileName(uri: String) = {
// Write the source code to file..
// to avoid paths like foo/bar/C:/whatnot on windows lets mangle the ':' character
new File(sourceDirectory, uri.replace(':', '_') + ".scala")
}
protected val sourceMapLog = Logging(getClass, "SourceMap")
private def compileAndLoad(uri: String, extraBindings: List[Binding], attempt: Int): (Template, Set[String]) = {
var code: Code = null
try {
// Generate the scala source code from the template
val g = generator(uri);
code = g.generate(this, uri, bindings ::: extraBindings)
val sourceFile = sourceFileName(uri)
sourceFile.getParentFile.mkdirs
IOUtil.writeBinaryFile(sourceFile, code.source.getBytes("UTF-8"))
// Compile the generated scala code
compiler.compile(sourceFile)
// Write the source map information to the class file
val sourceMap = buildSourceMap(g.stratumName, uri, sourceFile, code.positions)
sourceMapLog.debug("installing:" + sourceMap)
storeSourceMap(new File(bytecodeDirectory, code.className.replace('.', '/')+".class"), sourceMap)
storeSourceMap(new File(bytecodeDirectory, code.className.replace('.', '/')+"$.class"), sourceMap)
// Load the compiled class and instantiate the template object
val template = loadCompiledTemplate(code.className)
(template, code.dependencies)
} catch {
// TODO: figure out why we sometimes get these InstantiationException errors that
// go away if you redo
case e: InstantiationException =>
if (attempt == 0) {
compileAndLoad(uri, extraBindings, 1)
} else {
throw new TemplateException(e.getMessage, e)
}
case e:CompilerException=>
// Translate the scala error location info
// to the template locations..
def template_pos(pos:Position) = {
pos match {
case p:OffsetPosition => {
val filtered = code.positions.filterKeys( code.positions.ordering.compare(_,p) <= 0 )
if( filtered.isEmpty ) {
null
} else {
val (key,value) = filtered.last
// TODO: handle the case where the line is different too.
val colChange = pos.column - key.column
OffsetPosition(value.source, value.offset+colChange)
}
}
case _=> null
}
}
var newmessage = "Compilation failed:\n"
val errors = e.errors.map {
(olderror) =>
val pos = template_pos(olderror.pos)
if( pos==null ) {
newmessage += ":"+olderror.pos+" "+olderror.message+"\n"
newmessage += olderror.pos.longString+"\n"
olderror
} else {
newmessage += uri+":"+pos+" "+olderror.message+"\n"
newmessage += pos.longString+"\n"
CompilerError(uri, olderror.message, pos, olderror)
}
}
error(e)
throw new CompilerException(newmessage, errors)
case e: InvalidSyntaxException =>
e.template = uri
throw e
case e: TemplateException => throw e
case e: Throwable => throw new TemplateException(e.getMessage, e)
}
}
/**
* Gets the code generator to use for the give uri string by looking up the uri's extension
* in the the codeGenerators map.
*/
private def generator(uri: String): CodeGenerator = {
extension(uri) match {
case Some(ext)=>
generatorForExtension(ext)
case None=>
throw new TemplateException("Template file extension missing. Cannot determine which template processor to use.")
}
}
private def extension(uri: String): Option[String] = {
val t = uri.split("\\.")
if (t.length < 2) {
None
} else {
Some(t.last)
}
}
/**
* Returns the code generator for the given file extension
*/
private def generatorForExtension(extension: String) = codeGenerators.get(extension) match {
case None => throw new TemplateException("Not a template file extension (" + codeGenerators.keysIterator.mkString("|") + "), you requested: " + extension);
case Some(generator) => generator
}
private def loadCompiledTemplate(className:String) = {
val cl = new URLClassLoader(Array(bytecodeDirectory.toURI.toURL), classLoader)
val clazz = cl.loadClass(className)
clazz.asInstanceOf[Class[Template]].newInstance
}
/**
* Figures out the modification time of the class.
*/
private def lastModified(clazz:Class[_]):Long = {
val codeSource = clazz.getProtectionDomain.getCodeSource;
if( codeSource !=null && codeSource.getLocation.getProtocol == "file") {
val location = new File(codeSource.getLocation.getPath)
if( location.isDirectory ) {
val classFile = new File(location, clazz.getName.replace('.', '/')+".class")
if( classFile.exists ) {
return classFile.lastModified
}
} else {
// class is inside an archive.. just use the modification time of the jar
return location.lastModified
}
}
// Bail out
return 0
}
protected def buildSourceMap(stratumName:String, uri:String, scalaFile:File, positions:TreeMap[OffsetPosition,OffsetPosition]) = {
val shortName = uri.split("/").last
val longName = uri.stripPrefix("/")
val stratum: SourceMapStratum = new SourceMapStratum(stratumName)
val fileId = stratum.addFile(shortName, longName)
// build a map of input-line -> List( output-line )
var smap = new TreeMap[Int,List[Int]]()
positions.foreach {
case (out,in)=>
var outs = out.line :: smap.getOrElse(in.line, Nil)
smap += in.line -> outs
}
// sort the output lines..
smap = smap.transform { (x,y)=> y.sortWith(_<_) }
smap.foreach{
case (in, outs)=>
outs.foreach {
out=>
stratum.addLine(in, fileId, 1, out, 1)
}
}
stratum.optimize
var sourceMap: SourceMap = new SourceMap
sourceMap.setOutputFileName(scalaFile.getName)
sourceMap.addStratum(stratum, true)
sourceMap.toString
}
protected def storeSourceMap(classFile:File, sourceMap:String) = {
SourceMapInstaller.store(classFile, sourceMap)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy