All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fusesource.scalate.RenderContext.scala Maven / Gradle / Ivy

There is a newer version: 1.5.3-scala_2.8.2
Show newest version
/**
 * Copyright (C) 2009-2011 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.fusesource.scalate

import filter.FilterRequest
import introspector.Introspector
import support.RenderHelper
import util._
import util.Strings.isEmpty
import util.IOUtil._

import java.io.File
import java.text.{DateFormat, NumberFormat}
import java.util.{Locale, Date}
import xml.{Node, PCData, NodeSeq, NodeBuffer}
import collection.mutable.{ListMap, LinkedHashSet, ListBuffer, HashMap}

object RenderContext {
  val threadLocal = new ThreadLocal[RenderContext]

  def capture(body: => Unit) = apply().capture(body)

  def captureNodeSeq(body: => Unit) = apply().captureNodeSeq(body)

  def apply(): RenderContext = threadLocal.get

  def using[T](that: RenderContext)(func: =>T):T = {
    val previous = threadLocal.get
    try {
      threadLocal.set(that)
      func
    } finally {
      if( previous!=null ) {
        threadLocal.set(previous)
      } else {
        threadLocal.remove
      }
    }
  }

}

/**
 * Provides helper methods for rendering templates and values and for working with attributes.
 *
 * @see DefaultRenderContext
 * @see org.fusesource.scalate.servlet.ServletRenderContext
 */
trait RenderContext {
  /**
   * Default string used to output null values
   */
  var nullString = ""

  /**
   * Default string used to output None values
   */
  var noneString = ""

  /**
   * Whether or not markup sensitive characters for HTML/XML elements like & > < are escaped or not
   */
  var escapeMarkup = true

  /**
   * Whether we should escape CSS in CDATA sections when using the CSS filter
   */
  var wrapCssInCData = true

  var currentTemplate: String = _

  var viewPrefixes = List("")
  var viewPostfixes = engine.codeGenerators.keysIterator.map(x => "." + x).toList

  def engine: TemplateEngine

  /**
   * Renders the provided value and inserts it into the final rendered document without sanitizing the value.
   */
  def <<(value: Any): Unit

  /**
   * Renders the provided value, sanitizes any XML special characters and inserts
   * it into the final rendered document.
   */
  def <<<(value: Any): Unit

  /**
   * Returns the request URI
   */
  def requestUri: String

  /**
   * Returns the Resource of the request
   */
  def requestResource: Option[Resource]

  /**
   * Returns the file for the given request resource
   */
  def requestFile: Option[File]


  /**
   * Returns a local link to the given file which should be within the [sourceDirectories]
   */
  def uri(file: File): Option[String] = {
    for (s <- engine.sourceDirectories) {
      if (Files.isDescendant(s, file)) {
        return Some(uri("/" + Files.relativeUri(s, file)))
      }
    }
    None
  }


  /**
   * Allows conversion of an absolute URL starting with "/" to be converted using the prefix of a web application
   */
  def uri(u: String): String = u

  /**
   * Loads the given template or file as a String for inclusion into the current page.
   *
   * Useful if you want to include some client side template, particularly with a .jade extension
   */
  def load(uri: String): String = engine.resourceLoader.load(uri)

  /**
   * Access the attributes available in this context
   */
  def attributes: AttributeMap

  /**
   * Sorted list of attribute keys
   */
  def attributeKeys = attributes.keySet.toList.sortWith(_ < _)

  /**
   * Returns the attribute of the given type or a [[org.fussesource.scalate.NoValueSetException]] exception is thrown
   */
  def attribute[T](name: String): T =
    attributeOrElse(name, throw new NoValueSetException(name))


  /**
   * Returns the attribute of the given name and type or the default value if it is not available
   */
  def attributeOrElse[T](name: String, defaultValue: => T): T = {
    attributes.get(name)
            .getOrElse(defaultValue)
            .asInstanceOf[T]
  }

  def setAttribute(name: String, value: Option[Any]) {
    value match {
      case Some(v) => attributes(name) = v
      case None => attributes.remove(name)
    }
  }

  /**
   * Captured the body of the function call then sets it to the given attribute value
   */
  def captureAttribute(name: String)(body: => Unit): Unit = {
    val v = capture(body)
    attributes(name) = v
  }



  /**
   * Captured the body of the function call then append it to the given attribute value
   */
  def captureAttributeAppend(name: String)(body: => Unit): Unit = {
    val text = capture(body)
    val v = attributes.get(name) match {
      case Some(t) => t + text
      case _ => text
    }
    attributes(name) = v
  }

  /**
   * Creates an instance of the given given type using dependency injection to inject the necessary values into
   * the object
   */
  def inject[T](implicit manifest: Manifest[T]): T = {
    val clazz = manifest.erasure
    Objects.tryInstantiate(clazz, List(this)) match {
      case Some(t) => t.asInstanceOf[T]
      case _ => throw new NoInjectionException(clazz)
    }
  }
  /////////////////////////////////////////////////////////////////////
  //
  // Rendering API
  //
  //////////////////////////////////x///////////////////////////////////
  def value(any: Any, shouldSanitize: Boolean = escapeMarkup): Any = {
    def sanitize(text: String): Any = if (shouldSanitize) {Unescaped(RenderHelper.sanitize(text))} else {text}

    any match {
      case u: Unit => ""
      case null => sanitize(nullString)
      case None => sanitize(noneString)
      case Some(a) => value(a, shouldSanitize)
      case Unescaped(text) => text
      case f: Function0[_] => value(f(), shouldSanitize)
      case v: String => sanitize(v)
      case v: Date => sanitize(dateFormat.format(v))
      case n: Double if n.isNaN => "NaN"
      case n: Float if n.isNaN => "NaN"
      case v: Double => sanitize(numberFormat.format(v))
      case v: Float => sanitize(numberFormat.format(v))
      case v: Number => sanitize(numberFormat.format(v))
      case f: FilterRequest => {
        // NOTE assume a filter does the correct sanitizing
        var rc = filter(f.filter, f.content.toString)
        rc
      }
      // No need to sanitize nodes as they are already sanitized
      case s: NodeBuffer =>
        // No need to sanitize nodes as they are already sanitized
        (s.foldLeft(new StringBuilder) {
          (rc, x) => x match {
            case cd: PCData => rc.append(cd.data)
            case _ => rc.append(x)
          }
        }).toString

      case n: Node => n.toString

      case x: Traversable[Any] =>
        x.map(value(_, shouldSanitize)).mkString("")

      // TODO for any should we use the renderView?
      case v: Any => sanitize(v.toString)
    }
  }

  def valueEscaped(any: Any) = value(any, true)

  def valueUnescaped(any: Any) = value(any, false)

  /**
   * Ensures that the string value of the parameter is not markup escaped
   */
  def unescape(v: Any): Unit = this << value(v, false)

  /**
   * Ensures that the string value of the parameter is always markup escaped
   */
  def escape(v: Any): Unit = this << value(v, true)

  def filter(name: String, content: String): String = {
    val context = this
    engine.filter(name) match {
      case None => throw new NoSuchFilterException(name)
      case Some(f) => f.filter(context, content)
    }
  }

  def include(path: String): Unit = include(path, false)
  def include(path: String, layout: Boolean): Unit = include(path, layout, Nil)

  /**
   * Includes the given template path
   *
   * @param layout if true then applying the layout the included template
   */
  def include(path: String, layout: Boolean, extraBindings: Traversable[Binding]): Unit = {
    val uri = resolveUri(path)

    withUri(uri) {
      val template = engine.load(uri, extraBindings)
      if (layout) {
        engine.layout(template, this);
      }
      else {
        template.render(this);
      }
    }
  }

  protected def blankString: String = ""

  /**
   * Renders a collection of model objects with an optional separator
   */
  def collection(objects: Traversable[AnyRef], viewName: String = "index", separator: => Any = blankString): Unit = {
    var first = true
    for (model <- objects) {
      if (first) {
        first = false
      }
      else {
        this << separator
      }
      view(model, viewName)
    }
  }

  /**
   * Renders the view of the given model object, looking for the view in
   * packageName/className.viewName.ext
   */
  def view(model: AnyRef, viewName: String = "index"): Unit = {
    if (model == null) {
      throw new NullPointerException("No model object given!")
    }

    val classSearchList = new ListBuffer[Class[_]]()

    def buildClassList(clazz: Class[_]): Unit = {
      if (clazz != null && clazz != classOf[Object] && !classSearchList.contains(clazz)) {
        classSearchList.append(clazz);
        buildClassList(clazz.getSuperclass)
        for (i <- clazz.getInterfaces) {
          buildClassList(i)
        }
      }
    }

    def viewForClass(clazz: Class[_]): String = {
      for (prefix <- viewPrefixes; postfix <- viewPostfixes) {
        val path = clazz.getName.replace('.', '/') + "." + viewName + postfix
        val fullPath = if (isEmpty(prefix)) {"/" + path} else {"/" + prefix + "/" + path}
        if (engine.resourceLoader.exists(fullPath)) {
          return fullPath
        }
      }
      null
    }

    def searchForView(): String = {
      for (i <- classSearchList) {
        val rc = viewForClass(i)
        if (rc != null) {
          return rc;
        }
      }
      null
    }

    buildClassList(model.getClass)
    val templateUri = searchForView()

    if (templateUri == null) {
      throw new NoSuchViewException(model, viewName)
    } else {
      using(model) {
        include(templateUri)
      }
    }
  }

  /**
   * Allows a symbol to be used with arguments to the   { @link render } or  { @link layout } method such as
   * render("foo.ssp", 'foo -> 123, 'bar -> 456)   {...}
   */
  implicit def toStringPair(entry: (Symbol, Any)): (String, Any) = (entry._1.name, entry._2)

  /**
   * Renders the given template with optional attributes
   */
  def render(path: String, attributeMap: Map[String, Any] = Map()): Unit = {
    // TODO should we call engine.layout() instead??

    val uri = resolveUri(path)
    val context = this

    withAttributes(attributeMap) {
      withUri(uri) {
        engine.load(uri).render(context);
      }
    }
  }

  /**
   * Renders the given template with optional attributes passing the body block as the *body* attribute
   * so that it can be layered out using the template.
   */
  def layout(path: String, attrMap: Map[String, Any] = Map())(body: => Unit): Unit = {
    val bodyText = capture(body)
    render(path, attrMap + ("body" -> bodyText))
  }

  /**
   * Uses the new sets of attributes for the given block, then replace them all
   * (and remove any newly defined attributes)
   */
  def withAttributes[T](attrMap: Map[String, Any])(block: => T): T = {
    val oldValues = new HashMap[String, Any]

    // lets replace attributes, saving the old values
    for ((key, value) <- attrMap) {
      val oldValue = attributes.get(key)
      if (oldValue.isDefined) {
        oldValues.put(key, oldValue.get)
      }
      attributes(key) = value
    }

    val answer = block

    // restore old values
    for (key <- attrMap.keysIterator) {
      val oldValue = oldValues.get(key)
      if (removeOldAttributes || oldValue.isDefined) {
        setAttribute(key, oldValue)
      }
    }

    answer
  }

  /**
   * Should we remove attributes from the context after we've rendered a child request?
   */
  protected def removeOldAttributes = true

  def withUri[T](uri: String)(block: => T): T = {
    val original = currentTemplate
    try {
      currentTemplate = uri

      // lets keep track of the templates
      attributes("scalateTemplates") = uri :: attributeOrElse[List[String]]("scalateTemplates", List()).distinct

      block
    } finally {
      currentTemplate = original
    }
  }


  protected def resolveUri(path: String) = if (currentTemplate != null) {
    engine.resourceLoader.resolve(currentTemplate, path);
  } else {
    path
  }


  protected def using[T](model: AnyRef)(op: => T): T = {
    val original = attributes.get("it");
    try {
      attributes("it") = model
      op
    } finally {
      setAttribute("it", original)
    }
  }


  /**
   * Evaluates the specified body capturing any output written to this context
   * during the evaluation
   */
  def capture(body: => Unit): String

  /**
   * Evaluates the template capturing any output written to this page context during the body evaluation
   */
  def capture(template: Template): String

  /**
   * Captures the text of the body and then parses it as markup
   */
  def captureNodeSeq(body: => Unit): NodeSeq = XmlHelper.textToNodeSeq(capture(body))

  /**
   * Captures the text of the template rendering and then parses it as markup
   */
  def captureNodeSeq(template: Template): NodeSeq = XmlHelper.textToNodeSeq(capture(template))

  /*
    Note due to the implicit conversions being applied to => Unit only taking the last
    statement of the block as per this discussion:
    http://old.nabble.com/-scala--is-this-a-compiler-bug-or-just-a-surprising-language-quirk-%28or-newbie--lack-of-understanding-%3A%29-ts27917276.html

    then we can no longer support this approach which is a shame.

    So tags must take => Unit as a parameter - then either take Rendercontext as the first parameter block
    or use the RenderContext() to get the current active context for capturing.

    implicit def bodyToStringFunction(body: => Unit): () => String = {
      () => {
        println("capturing the body....")
        val answer = capture(body)
        println("captured body: " + answer)
        answer
      }
    }

    implicit def toBody(body: => Unit): Body = new Body(this, body)
  */


  /////////////////////////////////////////////////////////////////////
  //
  // introspection for dynamic templates or for archetype templates
  //
  /////////////////////////////////////////////////////////////////////
  def introspect(aType: Class[_]) = Introspector(aType)



  /////////////////////////////////////////////////////////////////////
  //
  // resource helpers/accessors
  //
  /////////////////////////////////////////////////////////////////////

  private var resourceBeanAttribute = "it"

  /**
   * Returns the JAXRS resource bean of the given type or a  [[org.fusesource.scalate.NoValueSetException]]
   * exception is thrown
   */
  def resource[T]: T = {
    attribute[T](resourceBeanAttribute)
  }

  /**
   * Returns the JAXRS resource bean of the given type or the default value if it is not available
   */
  def resourceOrElse[T](defaultValue: T): T = {
    attributeOrElse(resourceBeanAttribute, defaultValue)
  }




  /////////////////////////////////////////////////////////////////////
  //
  // custom object rendering
  //
  /////////////////////////////////////////////////////////////////////

  private var _numberFormat = new Lazy(NumberFormat.getNumberInstance(locale))
  private var _percentFormat = new Lazy(NumberFormat.getPercentInstance(locale))
  private var _dateFormat = new Lazy(DateFormat.getDateInstance(DateFormat.FULL, locale))

  /**
   * Returns the formatted string using the locale of the users request or the default locale if not available
   */
  def format(pattern: String, args: AnyRef*) = {
    String.format(locale, pattern, args: _*)
  }

  def percent(number: Number) = percentFormat.format(number)

  // Locale based formatters
  // shame we can't use 'lazy var' for this cruft...
  def numberFormat: NumberFormat = _numberFormat()

  def numberFormat_=(value: NumberFormat): Unit = _numberFormat(value)

  def percentFormat: NumberFormat = _percentFormat()

  def percentFormat_=(value: NumberFormat): Unit = _percentFormat(value)

  def dateFormat: DateFormat = _dateFormat()

  def dateFormat_=(value: DateFormat): Unit = _dateFormat(value)


  def locale: Locale = Locale.getDefault


  /**
   * Used to represent some text which does not need escaping
   */
  case class Unescaped(text: String) {
    override def toString = text
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy