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

net.liftmodules.ng.Angular.scala Maven / Gradle / Ivy

package net.liftmodules.ng

import java.util.concurrent.{ ConcurrentMap, ConcurrentHashMap }

import scala.collection.mutable
import scala.xml.{Elem, NodeSeq}

import net.liftweb.actor.LAFuture
import net.liftweb.common._
import net.liftweb.json.{DefaultFormats, Formats, JsonParser}
import net.liftweb.http. { LiftRules, DispatchSnippet, ResourceServer, S, RequestVar, SessionVar }
import net.liftweb.http.js.JE._
import net.liftweb.http.js.JsCmds._
import net.liftweb.http.js.{JsExp, JsCmd, JsObj}
import net.liftweb.util.Props
import net.liftweb.util.Props.RunModes
import net.liftweb.util.StringHelpers._

/**
 * Primary lift-ng module
 */
object Angular extends DispatchSnippet with AngularProperties with LiftNgJsHelpers with Loggable {

  private [ng] var futuresDefault:Boolean = true
  private [ng] var appSelectorDefault:String = "[ng-app]"
  private [ng] var includeJsScript:Boolean = true
  private [ng] var includeAngularJs:Boolean = false
  private [ng] var additionalAngularJsModules:Seq[String] = Seq()
  private [ng] var includeAngularCspCss = false
  private [ng] var retryAjaxInOrder = false
  private [ng] def rand = "NG"+randomString(18)

  /**
    * Init function to be called in Boot
    * @param futures true to include {{{net.liftweb.actor.LAFuture}}} support (and hence add a comet to your page), false otherwise
    * @param appSelector the CSS selector to find your app in the page
    * @param includeJsScript true to include the prerequisite liftproxy.js file, false if you plan to include it yourself.
    * @param includeAngularJs true to include angular.js and modules found in angularjs webjar.
    * @param additionalAngularJsModules list of additional angularjs modules to include in the page.
    * @param includeAngularCspCss true to include angular-csp.css found in angularjs webjar.
    * @param retryAjaxInOrder true to preserve the order of ajax service calls even in the event of server communication failures.
    */
  def init(
    futures:Boolean = true,
    appSelector:String = "[ng-app]",
    includeJsScript:Boolean = true,
    includeAngularJs:Boolean = false,
    additionalAngularJsModules:List[String] = List(),
    includeAngularCspCss:Boolean = false,
    retryAjaxInOrder:Boolean = false
  ):Unit = {
    LiftRules.snippetDispatch.append {
      case "Angular" => this
      case "i18n" => AngularI18n
    }
    
    LiftRules.addToPackages("net.liftmodules.ng")

    ResourceServer.allow {
      case "net" :: "liftmodules" :: "ng" :: "js" :: _ => true
    }

    futuresDefault = futures
    appSelectorDefault = appSelector
    this.includeJsScript = includeJsScript

    AngularI18nRest.init()

    this.includeAngularJs = includeAngularJs
    this.includeAngularCspCss = includeAngularCspCss
    if(includeAngularJs || includeAngularCspCss) {
      this.additionalAngularJsModules = additionalAngularJsModules
      AngularJsRest.init()
    }

    this.retryAjaxInOrder = retryAjaxInOrder

    // TODO Remove limitation
    require(
      !retryAjaxInOrder || !BuildInfo.liftEdition.startsWith("3"),
      "retryAjaxInOrder is not currently supported in Lift 3.x"
    )
  }

  private def bool(s:String, default:Boolean):Boolean = {
    val truthy = List("true", "yes", "on")
    val falsey = List("false", "no", "off")

    if(default) !falsey.find(_.equalsIgnoreCase(s)).isDefined
    else truthy.find(_.equalsIgnoreCase(s)).isDefined
  }
  
  private object AngularModules extends RequestVar[mutable.HashSet[Module]](mutable.HashSet.empty)

  /**
   * Set to true when render is called so we know to stop saving things up to put in the head.
   */
  private object HeadRendered extends RequestVar[Boolean](false)
  
  /** Implementation of dispatch to allow us to add ourselves as a snippet */
  override def dispatch = {
    case "bind" => bind
    case _ => { _ => render }
  }

  private val liftproxySrc =
    "/classpath/net/liftmodules/ng/js/liftproxy-"+BuildInfo.version + (Props.mode match {
      case RunModes.Development => ".js"
      case _ => ".min.js"
    })

  private def classFromName(name:String):Option[Class[_]] = {
    try Some(Class.forName(name)) catch {
      case e:Exception => None
    }
  }
  /**
   * Sets up a two-way scope variable binding by dropping in two binding actors as the last Elem/Node children
   * in the passed NodeSeq
   * @param xhtml
   * @return
   */
  def bind(xhtml:NodeSeq):NodeSeq = {
    val t = S.attr("type").map(_.trim)
    val ts = S.attr("types").map(_.split(',').map(_.trim).toList).openOr(Seq())
    val types = t.map(_ +: ts).openOr(ts)

    def cometClass(name:String) = LiftRules.buildPackage("comet."+name)
      .map(clazz => classFromName(clazz))
      .find(_.isDefined)
      .map(_.get)

    def isToClient(clazz:Class[_])      = classOf[BindingToClient] isAssignableFrom clazz
    def isToServer(clazz:Class[_])      = classOf[BindingToServer] isAssignableFrom clazz
    def isSessionScoped(clazz:Class[_]) = classOf[SessionScope] isAssignableFrom clazz

    val divs = types.map { bType =>
      val clazz = cometClass(bType)

      def cometUnnamed = {
        val cometUnnamed = "comet?type=" + bType
        
} def cometNamed = { val cometNamed = "comet?type=" + bType + "&randomname=true"
} def ajax(theClass:Class[_], inSession:Boolean) = { def newBinder = theClass.newInstance.asInstanceOf[NgModelBinder[NgModel]] val binder = if(inSession) getToServerBinder(bType).openOr(addToServerBinder(bType, newBinder)) else newBinder binder.fixedRender.openOrThrowException("lift-ng has a bug in it. Please report it at https://github.com/joescii/lift-ng") } clazz.map { c => val toClient = isToClient(c) val toServer = isToServer(c) val session = isSessionScoped(c) if(session) { // We need to render the named comet first. Otherwise using CometListener does not work. // This is because the unnamed comet sends the messages up via the named comet. // Hence it will get a create message but have no named comet actor to use. if (toClient && toServer) cometNamed ++ cometUnnamed else if (toClient) cometUnnamed else ajax(c, session) } else { if(toClient) cometNamed else ajax(c, session) } }.getOrElse(NodeSeq.Empty) }.reduceOption(_ ++ _) divs match { case Some(ds) => xhtml.toList match { case (n:Elem) :: tail => n.copy(child = n.child ++ ds) :: tail case _ => xhtml // I don't think this case can actually happen... } case None => { logger.warn("Angular.bind utilized without 'type' or 'types' specified") xhtml } } } private object ToServerBinders extends SessionVar[ConcurrentMap[String, NgModelBinder[NgModel]]](new ConcurrentHashMap()) private def addToServerBinder(theType:String, b:NgModelBinder[NgModel]):NgModelBinder[NgModel] = { ToServerBinders.get.put(theType, b) b } def getToServerBinder(theType:String):Box[NgModelBinder[NgModel]] = Option(ToServerBinders.get.get(theType)) /** * Renders all the modules that have been added to the RequestVar. */ def render: NodeSeq = { // We should only call this once from the tag. Calling it again indicates a programming error. require(!HeadRendered.is, "render has already been called once") HeadRendered.set(true) val liftproxy = if(includeJsScript) else NodeSeq.Empty val jsModule = Script(JsRaw( "var net_liftmodules_ng=net_liftmodules_ng||{};" + "net_liftmodules_ng.ajax=function(){"+ajaxFn+".apply(this,arguments)};" + "net_liftmodules_ng.retryAjaxInOrder="+retryAjaxInOrder+";" + "net_liftmodules_ng.enhancedAjax="+enhancedAjax+";" + "net_liftmodules_ng.version=\"" + BuildInfo.version + "\";" + "net_liftmodules_ng.jsPath=\"" + liftproxySrc +"\";" )) val modules = Script(AngularModules.is.map(_.cmd).reduceOption(_ & _).getOrElse(Noop)) val includeFutures = S.attr("futures").map(bool(_, futuresDefault)).openOr(futuresDefault) val futureActor = if(includeFutures)
else NodeSeq.Empty angularCspCss ++ angularModules ++ liftproxy ++ jsModule ++ modules ++ futureActor } private def angularModules:NodeSeq = if(includeAngularJs) { val ms = S.attr("additional-angularjs-modules").map(_.split(',').map(_.trim).toSeq).openOr(additionalAngularJsModules) val notDev = Props.mode != RunModes.Development val min = S.attr("min").map(bool(_, notDev)).openOr(notDev) ("" +: ms).map { m => val name = if(m == "") "angular" else "angular-"+m val id = name+"_js" val src = AngularJsRest.path + AngularJsRest.version + "/" + name + (if(min) ".min" else "") + ".js" }.foldLeft(NodeSeq.Empty)(_ ++ _) } else NodeSeq.Empty private lazy val angularCspCss:NodeSeq = if(!includeAngularCspCss) NodeSeq.Empty else /** * Registers the module with the RequestVar so that it may be rendered in base.html. */ def renderIfNotAlreadyDefined(module: Module): NodeSeq = { if (HeadRendered.is) { if (AngularModules.is.contains(module)) { // module already added elsewhere. normal case. don't render it again. NodeSeq.Empty } else { // module not rendered already in head or elsewhere. render it now, and keep it so we can deduplicate it later AngularModules.is += module Script(module.cmd) } } else { // New module and head render hasn't been called. Store it for head render. AngularModules.is += module NodeSeq.Empty } } private [ng] def plumbFuture[T <: Any](f: LAFuture[Box[T]], id:String)(implicit formats:Formats) = { S.session map { s => f foreach { box => val json = S.initIfUninitted(s)(box map stringify) s.sendCometActorMessage("LiftNgFutureActor", Empty, ReturnData(id, json)) }} f } object angular { def module(moduleName: String) = new Module(moduleName) } /** * Builder for Angular modules. * * @param dependencies other modules whose services and scopes this module depends upon. * NOTE: factories may add additional module dependencies to this as they're defined. */ class Module(private[Angular] val name: String, dependencies: Set[String] = Set.empty) { require(name.nonEmpty) private val factories = Map.newBuilder[String, Factory] def factory(serviceName: String, factory: Factory): Module = { factories += serviceName -> factory this } private[Angular] def cmd: JsCmd = { val finalFactories = factories.result() val allDependencies: List[Str] = finalFactories .values .foldLeft(Set.newBuilder[String] ++= dependencies)(_ ++= _.moduleDependencies) .result() .map(Str)(collection.breakOut) val moduleDeclaration = Call("angular.module", name, JsArray(allDependencies)) finalFactories.foldLeft(moduleDeclaration) { case (module, (factName, factory)) => Call(JsVar(module.toJsCmd, "factory").toJsCmd, factName, factory.toGenerator) } } override def hashCode(): Int = name.hashCode override def equals(obj: Any): Boolean = obj != null && obj.isInstanceOf[Module] && { val otherModule = obj.asInstanceOf[Module] otherModule.name == name } } /** * A factory builder that can create a javascript object full of ajax calls. */ def jsObjFactory() = new JsObjFactory() /** * Creates a generator function() {} to be used within an angular.module.factory(name, ...) call. */ trait Factory { private[Angular] def moduleDependencies: Set[String] = Set.empty[String] private[Angular] def toGenerator: AnonFunc } /** * This exists to wrap the functions passed to `defModelToAny`/`jsonCall` and handle the type contravariance * appropriately. Users of lift-ng needn't even know it exists. * See http://stackoverflow.com/questions/31907701/why-does-this-scala-function-compile-when-the-argument-does-not-conform-to-the-t/31907828#answer-31907813 */ case class ModelFnBox[M](f: M => Box[Any]) /** * This exists to wrap the functions passed to `defModelToFutureAny`/`future` and handle the type contravariance * appropriately. Users of lift-ng needn't even know it exists. * See http://stackoverflow.com/questions/31907701/why-does-this-scala-function-compile-when-the-argument-does-not-conform-to-the-t/31907828#answer-31907813 */ case class ModelFnFuture[M, R](f: M => LAFuture[Box[R]]) /** * Transparently wraps `NgModel => Box[Any]` functions appropriately for `defModelToAny`/`jsonCall` */ implicit def wrapModelFnBox[M](f: M => Box[Any]) = ModelFnBox(f) /** * Transparently wraps `NgModel => LAFuture[Box[Any]]` functions appropriately for `defModelToFutureAny` */ implicit def wrapModelFnFuture[M, R](f: M => LAFuture[Box[R]]) = ModelFnFuture(f) /** * Produces a javascript object with ajax functions as keys. e.g. * {{{ * function(dependencies) { * get: function() { doAjaxStuff(); } * post: function(string) { doAjaxStuff(); } * } * }}} */ class JsObjFactory extends Factory { /** * name -> function */ private val functions = mutable.HashMap.empty[String, AjaxFunctionGenerator] override private[Angular] def moduleDependencies = functions.values.foldLeft(Set.newBuilder[String])(_ ++= _.moduleDependencies).result() private val promiseMapper = DefaultApiSuccessMapper /** * Registers a no-arg javascript function in this service's javascript object that returns a \$q promise. * This function is an enhancement over `jsonCall` in that it allows the caller to provide an implicit `Formats` * for dictating the JSON serializer. *

* Future plan is to include a macro named `defs` which will choose the appropriate def*() function based on * the type of the argument. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ def defAny (functionName: String, func: => Box[Any]) (implicit formats:Formats = DefaultFormats) : JsObjFactory = registerFunction(functionName, AjaxNoArgToJsonFunctionGenerator(Unit => promiseMapper.toPromise(func))) /** * Registers a no-arg javascript function in this service's javascript object that returns a \$q promise. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ // @deprecated(message = "", since = "0.7.0") def jsonCall (functionName: String, func: => Box[AnyRef]) : JsObjFactory = defAny(functionName, func) /** * Registers a javascript function in this service's javascript object that takes a String and returns a \$q promise. * This function is an enhancement over `jsonCall` in that it allows the caller to provide an implicit `Formats` * for dictating the JSON serializer. *

* Future plan is to include a macro named `defs` which will choose the appropriate def*() function based on * the type of the argument. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ def defStringToAny (functionName: String, func: String => Box[Any]) (implicit formats:Formats = DefaultFormats) : JsObjFactory = registerFunction(functionName, AjaxStringToJsonFunctionGenerator(func.andThen(promiseMapper.toPromise))) /** * Registers a javascript function in this service's javascript object that takes a String and returns a \$q promise. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ // @deprecated(message = "", since = "0.7.0") def jsonCall (functionName: String, func: String => Box[AnyRef]) : JsObjFactory = defStringToAny(functionName, func) /** * Registers a javascript function in this service's javascript object that takes an NgModel object and returns a * \$q promise. * This function is an enhancement over `jsonCall` in that it allows the caller to provide an implicit `Formats` * for dictating the JSON serializer. *

* Future plan is to include a macro named `defs` which will choose the appropriate def*() function based on * the type of the argument. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ def defModelToAny[Model <: NgModel] (functionName: String, func: ModelFnBox[Model]) (implicit mf:Manifest[Model], formats:Formats = DefaultFormats) : JsObjFactory = registerFunction(functionName, AjaxJsonToJsonFunctionGenerator(func.f.andThen(promiseMapper.toPromise))) /** * Registers a javascript function in this service's javascript object that takes an NgModel object and returns a * \$q promise. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ // @deprecated(message = "", since = "0.7.0") def jsonCall[Model <: NgModel] (functionName: String, func: ModelFnBox[Model]) (implicit mf:Manifest[Model]) : JsObjFactory = defModelToAny(functionName, func) /** * Registers a no-arg javascript function in this service's javascript object that returns a \$q promise. * This function is an enhancement over `future` in that it allows the caller to provide an implicit `Formats` * for dictating the JSON serializer. *

* Future plan is to include a macro named `defs` which will choose the appropriate def*() function based on * the type of the argument. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ def defFutureAny[T <: Any] (functionName: String, func: => LAFuture[Box[T]]) (implicit formats:Formats = DefaultFormats) : JsObjFactory = registerFunction(functionName, NoArgFutureFunctionGenerator(Unit => func)) /** * Registers a no-arg javascript function in this service's javascript object that returns a \$q promise. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ // @deprecated(message = "", since = "0.7.0") def future[T <: Any] (functionName: String, func: => LAFuture[Box[T]]) : JsObjFactory = defFutureAny(functionName, func) /** * Registers a javascript function in this service's javascript object that takes a String and returns a \$q promise. * This function is an enhancement over `future` in that it allows the caller to provide an implicit `Formats` * for dictating the JSON serializer. *

* Future plan is to include a macro named `defs` which will choose the appropriate def*() function based on * the type of the argument. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ def defStringToFutureAny[T <: Any] (functionName: String, func: String => LAFuture[Box[T]]) (implicit formats:Formats = DefaultFormats) : JsObjFactory = registerFunction(functionName, StringFutureFunctionGenerator(func)) /** * Registers a javascript function in this service's javascript object that takes a String and returns a \$q promise. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ // @deprecated(message = "", since = "0.7.0") def future[T <: Any] (functionName: String, func: String => LAFuture[Box[T]]) : JsObjFactory = defStringToFutureAny(functionName, func) /** * Registers a javascript function in this service's javascript object that takes an NgModel object and returns a * \$q promise. * This function is an enhancement over `future` in that it allows the caller to provide an implicit `Formats` * for dictating the JSON serializer. *

* Future plan is to include a macro named `defs` which will choose the appropriate def*() function based on * the type of the argument. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ def defModelToFutureAny[Model <: NgModel, T <: Any] (functionName: String, func: ModelFnFuture[Model, T]) (implicit mf:Manifest[Model], formats:Formats = DefaultFormats) : JsObjFactory = registerFunction(functionName, JsonFutureFunctionGenerator(func.f)) /** * Registers a javascript function in this service's javascript object that takes an NgModel object and returns a * \$q promise. * * @param functionName name of the function to be made available on the service/factory * @param func produces the result of the ajax call. Failure, Full(DefaultResponse(false)), and some other logical * failures will be mapped to promise.reject(). See promiseMapper. */ // @deprecated(message = "", since = "0.7.0") def future[Model <: NgModel, T <: Any] (functionName: String, func: Model => LAFuture[Box[T]]) (implicit mf:Manifest[Model]) : JsObjFactory = defModelToFutureAny(functionName, func) // TODO: Rather than functions, let's put the values directly on the factory // /** // * Registers a no-arg javascript function in this service's javascript object that returns a String value. // * Use this to provide string values which are known at page load time and do not change. // * // * @param functionName name of the function to be made available on the service/factory // * @param value value to be returned on invocation of this function in the client. // */ // def valAny // (functionName: String, value:Any) // (implicit formats:Formats = DefaultFormats) // : JsObjFactory = // registerFunction(functionName, FromAnyFunctionGenerator(value)) /** * Registers a no-arg javascript function in this service's javascript object that returns a String value. * Use this to provide string values which are known at page load time and do not change. * * @param functionName name of the function to be made available on the service/factory * @param value value to be returned on invocation of this function in the client. */ // @deprecated(message = "", since = "0.7.0") def string (functionName: String, value:String) (implicit formats:Formats = DefaultFormats) : JsObjFactory = registerFunction(functionName, FromAnyFunctionGenerator(value)) /** * Registers a no-arg javascript function in this service's javascript object that returns an AnyVal value. * Use this to provide primitive values which are known at page load time and do not change. * * @param functionName name of the function to be made available on the service/factory * @param value value to be returned on invocation of this function in the client. */ // @deprecated(message = "", since = "0.7.0") def anyVal (functionName: String, value:AnyVal) (implicit formats:Formats = DefaultFormats) : JsObjFactory = registerFunction(functionName, FromAnyFunctionGenerator(value)) /** * Registers a no-arg javascript function in this service's javascript object that returns a json object. * Use this to provide objects which are known at page load time and do not change. * * @param functionName name of the function to be made available on the service/factory * @param value value to be returned on invocation of this function in the client. */ // @deprecated(message = "", since = "0.7.0") def json (functionName: String, value:AnyRef) (implicit formats:Formats = DefaultFormats) : JsObjFactory = registerFunction(functionName, FromAnyFunctionGenerator(value)) /** * Adds the ajax function factory and its dependencies to the factory. */ private def registerFunction(functionName: String, generator: AjaxFunctionGenerator): JsObjFactory = { require(functionName.nonEmpty) functions += functionName -> generator this } private[Angular] def toGenerator: AnonFunc = { val serviceDependencies = functions.values.foldLeft(Set.newBuilder[String])(_ ++= _.serviceDependencies).result() AnonFunc(serviceDependencies.mkString(","), JsReturn(JsObj(functions.mapValues(_.toAnonFunc).toSeq: _*))) } } /** * Maps an api result to a Promise object that will be used to fulfill the javascript promise object. */ object DefaultApiSuccessMapper extends PromiseMapper { // private implicit val formats = DefaultFormats + new LAFutureSerializer def toPromise(box: Box[Any])(implicit formats:Formats): Promise = { box match { case Full(jsExp: JsExp) => Resolve(Some(jsExp)) // prefer using a case class instead case Full(serializable: AnyRef) => Resolve(Some(JsRaw(stringify(serializable)))) case Full(other) => Resolve(Some(JsRaw(other.toString))) case Full(Unit) | Empty => Resolve() case Failure(msg, _, _) => Reject(msg) } } } /** * Maps the response passed into the ajax calls into something that can be passed into promise.resolve(data) or * promise.reject(reason). */ trait PromiseMapper { def toPromise(box: Box[Any])(implicit formats:Formats): Promise } /** * Used to resolve or reject a javascript angular \$q promise. */ sealed trait Promise case class Resolve(data: Option[JsExp] = None, futureId: Option[String] = None) extends Promise case class Reject(reason: String = "server error") extends Promise object Promise { def apply(success: Boolean): Promise = if (success) Resolve(None) else Reject() } protected case class AjaxNoArgToJsonFunctionGenerator(jsFunc: Unit => Promise) extends LiftAjaxFunctionGenerator { def toAnonFunc = AnonFunc(JsReturn(Call("liftProxy.request", liftPostData))) private def liftPostData = SHtmlExtensions.ajaxJsonPost((id) => promiseToJson(tryPromise((), jsFunc))) } protected case class AjaxStringToJsonFunctionGenerator(stringToPromise: (String) => Promise)(implicit formats:Formats) extends LiftAjaxFunctionGenerator { private val ParamName = "str" def toAnonFunc = AnonFunc(ParamName, JsReturn(Call("liftProxy.request", liftPostData))) private def liftPostData = SHtmlExtensions.ajaxJsonPost(JsVar(ParamName), jsonFunc) private def jsonFunc: String => JsObj = { val jsonToPromise = (json: String) => JsonParser.parse(json).extractOpt[RequestString] match { case Some(RequestString(data)) => tryPromise(data, stringToPromise) case None => Reject(invalidJson(json)) } jsonToPromise andThen promiseToJson } } protected case class AjaxJsonToJsonFunctionGenerator[Model <: NgModel](modelToPromise: Model => Promise)(implicit mf:Manifest[Model], formats:Formats) extends LiftAjaxFunctionGenerator { private val ParamName = "json" def toAnonFunc = AnonFunc(ParamName, JsReturn(Call("liftProxy.request", liftPostData))) private def liftPostData: JsExp = SHtmlExtensions.ajaxJsonPost(JsVar(ParamName), jsonFunc) private def jsonFunc: String => JsObj = { val jsonToPromise = (json: String) => Json.slash(JsonParser.parse(json), ("data")).extractOpt[Model] match { case Some(model) => tryPromise(model, modelToPromise) case None => Reject(invalidJson(json)) } jsonToPromise andThen promiseToJson } } protected abstract class FutureFunctionGenerator extends LiftAjaxFunctionGenerator { protected def jsonFunc[T <: Any](jsonToFuture: (String) => NgFuture[T])(implicit formats:Formats): String => JsObj = { val futureToJsObj = (f:NgFuture[T]) => if(f._1.isSatisfied) promiseToJson(DefaultApiSuccessMapper.toPromise(f._1.get)) else promiseToJson(Resolve(None, Some(f._2))) jsonToFuture andThen futureToJsObj } protected def reject[T <: Any](json:String):NgFuture[T] = { val f = new LAFuture[Box[T]] f.satisfy(Failure(invalidJson(json))) (f, FutureIdNA) } } protected case class NoArgFutureFunctionGenerator[T <: Any](func: Unit => LAFuture[Box[T]])(implicit formats:Formats) extends FutureFunctionGenerator { def toAnonFunc = AnonFunc(JsReturn(Call("liftProxy.request", liftPostData))) private def liftPostData = SHtmlExtensions.ajaxJsonPost(jsonFunc(jsonToFuture)) val jsonToFuture:(String) => NgFuture[T] = json => { val id = rand (Angular.plumbFuture(tryFuture((), func), id), id) } } protected case class StringFutureFunctionGenerator[T <: Any](func: String => LAFuture[Box[T]])(implicit formats:Formats) extends FutureFunctionGenerator { private val ParamName = "str" def toAnonFunc = AnonFunc(ParamName, JsReturn(Call("liftProxy.request", liftPostData))) private def liftPostData = SHtmlExtensions.ajaxJsonPost(JsVar(ParamName), jsonFunc(jsonToFuture)) def jsonToFuture:(String) => NgFuture[T] = json => JsonParser.parse(json).extractOpt[RequestString] match { case Some(RequestString(data)) => { val id = rand (Angular.plumbFuture(tryFuture(data, func), id), id) } case _ => reject[T](json) } } protected case class JsonFutureFunctionGenerator[Model <: NgModel, T <: Any](func: Model => LAFuture[Box[T]])(implicit mf:Manifest[Model], formats:Formats) extends FutureFunctionGenerator { private val ParamName = "json" def toAnonFunc = AnonFunc(ParamName, JsReturn(Call("liftProxy.request", liftPostData))) private def liftPostData = SHtmlExtensions.ajaxJsonPost(JsVar(ParamName), jsonFunc(jsonToFuture)) def jsonToFuture:(String) => NgFuture[T] = json => { val dataOpt = Json.slash(JsonParser.parse(json), ("data")).extractOpt[Model] val id = rand val fOpt = for { data <- dataOpt } yield { (Angular.plumbFuture(tryFuture(data, func), id), id) } fOpt.openOr(reject[T](json)) } } protected case class ToStringFunctionGenerator(s:String)(implicit formats:Formats) extends LiftAjaxFunctionGenerator { def toAnonFunc = AnonFunc(JsReturn(s)) } protected case class ToJsonFunctionGenerator(obj:AnyRef)(implicit formats:Formats) extends LiftAjaxFunctionGenerator { def toAnonFunc = AnonFunc(JsReturn(JsRaw(stringify(obj)))) } protected case class FromAnyFunctionGenerator(obj:Any)(implicit formats:Formats) extends LiftAjaxFunctionGenerator { def toAnonFunc = AnonFunc(JsReturn(JsRaw(stringify(obj)))) } trait AjaxFunctionGenerator { def moduleDependencies: Set[String] def serviceDependencies: Set[String] def toAnonFunc: AnonFunc } trait LiftAjaxFunctionGenerator extends AjaxFunctionGenerator { def moduleDependencies: Set[String] = Set("lift-ng") def serviceDependencies: Set[String] = Set("liftProxy") private val SuccessField = "success" private val FutureField = "futureId" protected def tryPromise[A](a:A, f: A => Promise):Promise = try { f(a) } catch { case e:Exception => logger.warn("Uncaught exception while processing ajax function", e) Reject(e.getMessage) } protected def tryFuture[A, T <: Any](a:A, f: A => LAFuture[Box[T]]):LAFuture[Box[T]] = try { f(a) } catch { case e:Exception => logger.warn("Uncaught exception while processing ajax function", e) val future = new LAFuture[Box[T]] future.satisfy(Failure(e.getMessage)) future } protected def promiseToJson(promise: Promise): JsObj = { promise match { case Resolve(Some(jsExp), _) => JsObj(SuccessField -> JsTrue, "data" -> jsExp) case Resolve(None, futureId) => futureId.map( id => JsObj(SuccessField -> JsTrue, FutureField -> id) ).getOrElse(JsObj(SuccessField -> JsTrue)) case Reject(reason) => JsObj(SuccessField -> JsFalse, "msg" -> reason) } } protected def invalidJson(json:String) = { logger.warn("Received invalid JSON from the client => "+json) "invalid json" } } /** * A model to be sent from angularjs as json, to lift deserialized into this class. */ trait NgModel // These case classes encapsulate the incoming request. We used to have an id field, which was the impetus // for having explicit classes. Now they're just a hold over/placeholder in case we need anything like this in // the future. Consider refactoring and removing these... case class RequestData[Model <: NgModel : Manifest](data:Model) case class RequestString(data:String) case class ReturnData(id:FutureId, json:Box[String]) type FutureId = String val FutureIdNA:FutureId = "" type NgFuture[T <: Any] = (LAFuture[Box[T]], FutureId) }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy