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

jp.co.bizreach.play2handlebars.HandlebarsPlugin.scala Maven / Gradle / Ivy

There is a newer version: 0.4.3
Show newest version
package jp.co.bizreach.play2handlebars


import javax.inject._

import com.github.jknack.handlebars.Handlebars.SafeString
import com.github.jknack.handlebars.io.{ClassPathTemplateLoader, FileTemplateLoader}
import com.github.jknack.handlebars._
import play.api.inject.{Binding, Module, ApplicationLifecycle}
import play.api._
import play.twirl.api.{HtmlFormat, Html}

import scala.collection.concurrent.TrieMap
import scala.collection.JavaConverters._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

/**
 * Handlebars Module for Play 2.4 application
 */
class HandlebarsModule extends Module {
  def bindings(environment: Environment, configuration: Configuration): Seq[Binding[HandlebarsPlugin]] =
    Seq(
      bind[HandlebarsPlugin].toProvider[HandlebarsProvider].eagerly()
    )
}


/**
 * HandlebarsPlugin initializes handlebars.java configuration and keep the engine's singleton
 */
@Singleton
class HandlebarsProvider @Inject() (app: Application, lifecycle: ApplicationLifecycle) extends Provider[HandlebarsPlugin] {

  private lazy val logger = Logger(this.getClass)

  lazy val get: HandlebarsPlugin = new HandlebarsPlugin(app)

  /**
   * Shutdown the engine
   */
  lifecycle.addStopHook { () =>
    Future {
      logger.info("Handlebars plugin is shutting down")
    }
  }
}


class HandlebarsPlugin(app: Application) {

  private lazy val logger = Logger(this.getClass)

  private val confBasePath = "play2handlebars"

  trait Engine {
    val templates: TrieMap[String, Template]
    val rootPath: String
    val enableCache: Boolean
    val handlebars: Handlebars
  }

  lazy val engine = new Engine {
    val templates = TrieMap[String, Template]()
    val rootPath = app.configuration.getString(confBasePath + ".root").getOrElse("/app/views")
    val useClassPathLoader = app.configuration.getBoolean(confBasePath + ".useClassPathLoader").getOrElse(Play.isProd(app))
    val enableCache = app.configuration.getBoolean(confBasePath + ".enableCache").getOrElse(Play.isProd(app))
    val handlebars = new Handlebars(createLoader(useClassPathLoader, rootPath))

    /**
     * Override the default {{#each}} helper for java.lang.Iterable to recognize scala.Iterable
     */
    handlebars.registerHelper(EachHelper4S.NAME, EachHelper4S.INSTANCE)

    instantiateHelpers()
    
    def instantiateHelpers() = {
      val helperClasses = app.configuration.getStringList(confBasePath + ".helpers")
      val classloader = Thread.currentThread.getContextClassLoader

      def instantiateHelper(className: String) = {
        val helper = classloader.loadClass(className)
        handlebars.registerHelpers(helper)
        helper
      }

      helperClasses.foreach(list => list.asScala.foreach(clazz => instantiateHelper(clazz)))
    }
  }


  /**
   * Initiate the engine (Hmm ...)
   */
  new HBS(engine)
  logger.info("Handlebars plugin is started")


  private def createLoader(isClassPath:Boolean, rootPath:String) =
    if (isClassPath) new ClassPathTemplateLoader(rootPath)
    else new FileTemplateLoader(concatPath(System.getProperty("user.dir"), rootPath))


  private def concatPath(path1:String, path2:String) = {
    if (path2.startsWith("/")) path1 + path2
    else path1 + "/" + path2
  }
}


/**
 * This is just a small class to be used only for DI
 */
class HBS(private[HBS] val engine: HandlebarsPlugin#Engine) {
  HBS.injectMe(this)
}


/**
 *
 */
object HBS {

  private lazy val logger = Logger(this.getClass)

  /**
   * Object HBS has an instance of HBS which has the Handlebars engine.
   * Since Play's DI cannot combine with Scala's object, this "var" variable is used.
   */
  private[this] var hbs:Option[HBS] = None


  /**
   * Inject an instance of HBS to set the Handlebars engine. This can be called only once.
   */
  private[HBS] def injectMe(hbs:HBS): Unit = {
    this.hbs match {
      case Some(_) => logger.warn("HandlebarsPlugin is instantiated multiple times")
      case None =>
    }
    this.hbs = Some(hbs)
  }


  /**
   * Get the engine. If this is not injected, throws an exception.
   */
  def engine: HandlebarsPlugin#Engine = {
    hbs.map(_.engine).getOrElse(throw new IllegalStateException("HandlebarsPlugin is not installed"))
  }

  
  /**
   * Just a shorthand for Tuple2 attribute values
   * @param templatePath
   * @param attributes
   * @return
   */
  def apply(templatePath: String, attributes: (String, Any)*): Html = {
    generate(templatePath, attributes.toMap)
  }


  /**
   *
   * @param templatePath
   * @param attributes
   * @return
   */
  def withProduct(templatePath: String, attributes: Product, charset: String = "utf-8"): Html = {
    generate(templatePath, attributes, charset)
  }


  /**
   *
   * @param templatePath
   * @param attributes
   * @return
   */
  def any(templatePath: String, attributes: Any, charset: String = "utf-8"): Html = {
    generate(templatePath, attributes, charset)
  }

  /**
   *
   * @param templatePath
   * @param charset
   * @param attributes
   * @return
   */
  def apply(templatePath: String, attributes: Map[String, Any] = Map.empty, charset: String = "utf-8"): Html = {
    generate(templatePath, attributes.toMap, charset)
  }


  /**
   *
   * @param templatePath
   * @param attributes
   * @param charset
   * @return
   */
  private def generate(templatePath: String, attributes: Any = Map.empty, charset: String = "utf-8"): Html = {
    def compile = engine.handlebars.compile(templatePath)

    // Get the cache from the cache or just compile.
    val template =
      if (engine.enableCache)
        engine.templates.getOrElseUpdate(templatePath, { compile })
      else
        compile

    // Add several resolvers for scala
    val resolvers = ValueResolver.VALUE_RESOLVERS ++
      Array(
        ScalaMapValueResolver,
        CaseClassValueResolver,
        JsonNodeValueResolver.INSTANCE)

    val context = Context
      .newBuilder(attributes)
      .resolver(resolvers:_*)
      .build()


    // Wrap as html. See play.api.http.Writable
    HtmlFormat.raw(template.apply(context))
  }


  /**
   * Shorthand for Handlebars.SafeString class instantiation
   */
  def safeString(safeString:Any): SafeString = new Handlebars.SafeString(safeString.toString)


  /**
   * Convert any case class to java map using reflection.
   *
   * CAUTION: this may be slow
   */
  def toJavaMap[A <: Product](product:A):java.util.Map[String, Any] =
    product.getClass.getDeclaredFields.map(_.getName).zip(product.productIterator.toList).toMap.asJava

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy