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

.circumflex-core.2.2.source-code.messages.scala Maven / Gradle / Ivy

There is a newer version: 2.5
Show newest version
package ru.circumflex
package core

import collection.{Iterator, Map}
import collection.JavaConversions._
import collection.mutable.HashMap
import java.util.{ResourceBundle, Locale, Properties, Enumeration => JEnum, Date}
import java.text.MessageFormat
import java.io._
import org.apache.commons.io.FilenameUtils

/*!# Messages API

Messages API offers you a convenient way to internationalize your application.

Generally, all strings which should be presented to user are stored in
separate `.properties`-files as suggested by [Java Internationalization][java-i18n].

Circumflex Messages API goes beyond this simple approach and offers
delegating resolving, messages grouping, parameters interpolation and formatting.

  [java-i18n]: http://java.sun.com/javase/technologies/core/basic/intl

The usage is pretty simple: you use the `msg` method of package object `ru.circumflex.core`
which returns an implementation of `MessageResolver` used to retrieve messages. This instance
is also referred to as _global messages resolver_. By default, the `PropertyFileResolver`
is used. You can set `cx.messages` configuration parameter to use your own
`MessageResolver` implementation as global resolver.

The `resolve` method is responsible for resolving a message by `key`.

Circumflex Messages API features very robust ranged resolving. The message is searched
using the range of keys, from the most specific to the most general ones: if the message
is not resolved with given key, then the key is truncated from the left side to
the first dot (`.`) and the message is searched again. For example, if you are looking
for a message with the key `com.myapp.model.Account.name.empty` (possibly while performing
domain model validation), then following keys will be used to lookup an appropriate
message (until first success):

    com.myapp.model.Account.name.empty
    myapp.model.Account.name.empty
    model.Account.name.empty
    Account.name.empty
    name.empty
    empty

You can use the methods of Scala `Map` to retrieve messages from resolver.
Default implementation also reports missing messages into Circumflex debug log.

The locale is taken from `cx.locale` context variable (see `Context` for more details).
If no such variable found in the context, then the platform's default locale is used.

Messages can also be formatted. We support both classic `MessageFormat` style
(you know, with `{0}`s in text and varargs) and parameters interpolation (key-value pairs
are passed as arguments to `fmt` method, each `{key}` in message is replaced by
corresponding value).

You can use `ResourceBundleMessageResolver` to resolve messages from Java `ResourceBundle`s.

The default implementation (the `msg` method in package `ru.circumflex.core`)
uses uses property files with base name `Messages` to lookup messages. You can override
the default implementation by setting `cx.messages` configuration parameter. Unlike Java
`ResourceBundle` it effectively caches property files and allows hot editing (cache is
based on last modified dates).

You can set `cx.messages.root` to point to different directory (for example, to your webapp
root) and `cx.messages.name` to change the default base name of property files.

If you need to search messages in different sources, you can use
`DelegatingMessageResolver`: it tries to resolve a message using specified
`resolvers` list, the first successively resolved message is returned.
*/
trait MessageResolver extends Map[String, String] {
  protected var _lastModified = new Date()
  def lastModified = _lastModified

  def -(key: String): Map[String, String] = this
  def +[B1 >: String](kv: (String, B1)): Map[String, B1] = this

  protected def resolve(key: String): Option[String]

  protected def resolveRange(key: String): Option[String] = resolve(key) orElse {
    if (!key.contains(".")) None
    else resolveRange(key.substring(key.indexOf(".") + 1))
  }

  def get(key: String): Option[String] = resolveRange(key) orElse {
    CX_LOG.debug("Message with key '" + key + "' is missing.")
    None
  }

  def locale: Locale = ctx.get("cx.locale") match {
    case Some(l: Locale) => l
    case Some(l: String) => new Locale(l)
    case _ => Locale.getDefault
  }

  def fmt(key: String, params: (String, Any)*): String =
    params.foldLeft(getOrElse(key, key)) { (result, p) =>
      result.replaceAll("\\{" + p._1 + "\\}", p._2.toString)
    }
  def format(key: String, params: AnyRef*): String =
    MessageFormat.format(getOrElse(key, key), params: _*)
}

class ResourceBundleMessageResolver(val bundleName: String) extends MessageResolver {
  protected def bundle = ResourceBundle.getBundle(
    bundleName, locale, Thread.currentThread.getContextClassLoader)
  def iterator: Iterator[(String, String)] = bundle.getKeys
      .map(k => (k -> bundle.getString(k)))
  protected def resolve(key: String): Option[String] =
    try { Some(bundle.getString(key)) } catch { case e: Exception => None }
}

class DelegatingMessageResolver(initialResolvers: MessageResolver*) extends MessageResolver {
  protected var _resolvers: Seq[MessageResolver] = initialResolvers
  def resolvers = _resolvers
  def addResolver(r: MessageResolver): this.type = {
    _resolvers ++= List(r)
    _lastModified = new Date()
    this
  }
  def iterator: Iterator[(String, String)] =
    resolvers.map(_.iterator).reduceLeft((a, b) => a ++ b)
  protected def resolve(key: String): Option[String] = {
    resolvers.foreach(r => r.get(key).map(msg => return Some(msg)))
    None
  }
}

class PropertyFileResolver extends MessageResolver {
  val propsRoot = new File(
    FilenameUtils.separatorsToSystem(
      cx.getOrElse("cx.messages.root", "src/main/resources").toString))
  val resourceName = cx.getOrElse("cx.messages.name", "Messages").toString
  protected val _cache = new HashMap[String, (Properties, Long)]

  protected def getFile(suffix: String) =
    new File(propsRoot, resourceName + suffix + ".properties")

  protected def getProps(suffix: String): Option[Properties] = {
    val f = getFile(suffix)
    _cache.get(suffix) match {
      case Some((props: Properties, lm: Long)) =>
        if (!f.isFile) {    // previously cached file does not exist anymore
          _cache.remove(suffix)
          _lastModified = new Date()
          getProps(suffix)
        } else {
          if (f.lastModified > lm) {  // cached file has been modified
            _lastModified = new Date()
            loadProps(f) match {
              case Some(p: Properties) =>
                _cache(suffix) = (p, f.lastModified)
                Some(p)
              case _ =>    // previously cached file does not exist anymore
                _cache.remove(suffix)
                getProps(suffix)
            }
          } else Some(props)      // not modified -- return cached
        }
      case _ => loadProps(f) map { p =>
        _cache(suffix) = (p, f.lastModified)
        p
      }
    }
  }

  protected def loadProps(file: File): Option[Properties] = {
    if (!file.isFile) None
    else {
      val is = new FileInputStream(file)
      val props = new Properties
      try {
        props.load(is)
      } finally {
        is.close()
      }
      Some(props)
    }
  }

  def fallbackSuffix(suffix: String): String = {
    val i = suffix.lastIndexOf("_")
    if (i == -1) ""
    else suffix.substring(0, i)
  }

  def localeSuffix = "_" + locale.toString

  def iterator: Iterator[(String, String)] = {
    var suffix = ""
    var result: Iterator[(String, String)] = iteratorInternal(suffix)
    localeSuffix.split("_").filter(_ != "").foreach { part =>
      suffix += "_" + part
      getProps(suffix) map { props => result ++= iteratorInternal(suffix) }
    }
    result
  }

  protected def iteratorInternal(suffix: String): Iterator[(String, String)] =
    getProps(suffix).map { props =>
      props.keys.asInstanceOf[JEnum[String]].map(k => k -> props.getProperty(k))
    }.getOrElse(Iterator.empty)

  protected def resolve(key: String): Option[String] = resolveInternal(key, localeSuffix)

  protected def resolveInternal(key: String, suffix: String): Option[String] =
    getProps(suffix).flatMap(props => any2option(props.getProperty(key))).orElse {
      if (suffix == "") None
      else resolveInternal(key, fallbackSuffix(suffix))
    }

}

case class Msg(key: String, params: (String, Any)*) {
  def param(key: String): Option[Any] = params.find(_._1 == key).map(_._2)
  def hasParam(key: String): Boolean = !params.find(_._1 == key).isEmpty
  override def toString: String = msg.fmt(key, params: _*)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy