
net.liftweb.http.CometActor.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2007-2011 WorldWide Conferencing, LLC
*
* 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 net.liftweb
package http
import net.liftweb.common._
import net.liftweb.actor._
import net.liftweb.util.Helpers._
import net.liftweb.util._
import net.liftweb.json._
import scala.xml.{NodeSeq, Text, Elem, Node, Group, Null, PrefixedAttribute, UnprefixedAttribute}
import scala.collection.mutable.ListBuffer
import net.liftweb.http.js._
import JsCmds._
import JE._
import java.util.Locale
trait DeltaTrait {
def toJs: JsCmd
}
trait CometState[DeltaType <: DeltaTrait,
MyType <: CometState[DeltaType, MyType]] {
self: MyType =>
def -(other: MyType): Seq[DeltaType]
def render: NodeSeq
}
trait CometStateWithUpdate[UpdateType, DeltaType <: DeltaTrait,
MyType <: CometStateWithUpdate[UpdateType,
DeltaType, MyType]]
extends CometState[DeltaType, MyType] {
self: MyType =>
def process(in: UpdateType): MyType
}
trait StatefulComet extends CometActor {
type Delta <: DeltaTrait
type State <: CometState[Delta, State]
/**
* Test the parameter to see if it's an updated state object
*/
def testState(in: Any): Box[State]
/**
* Return the empty state object
*/
def emptyState: State
/**
* The current state objects
*/
protected var state: State = emptyState
/**
* If there's some ThreadLocal variable that needs to be set up
* before processing the state deltas, set it up here.
*/
protected def setupLocalState[T](f: => T): T = f
private[http] override val _lowPriority = {
val pf: PartialFunction[Any, Unit] = {
case v if testState(v).isDefined =>
testState(v).foreach {
ns =>
if (ns ne state) {
val diff = ns - state
state = ns
partialUpdate(setupLocalState {
diff.map(_.toJs).foldLeft(Noop)(_ & _)
})
}
}
}
pf orElse super._lowPriority
}
/**
* The Render method
*/
def render = state.render
}
object CurrentCometActor extends ThreadGlobal[LiftCometActor]
object AddAListener {
def apply(who: SimpleActor[Any]) = new AddAListener(who, {
case _ => true
})
}
/**
* This is a message class for use with ListenerManager and CometListener
* instances. The use of the shouldUpdate function is deprecated, and
* should instead be handled by the message processing partial functions
* on the CometListener instances themselves.
*
* @see CometListener
* @see ListenerManager
*/
case class AddAListener(who: SimpleActor[Any], shouldUpdate: PartialFunction[Any, Boolean])
/**
* This is a message class for use with ListenerManager and CometListener
* instances.
*
* @see CometListener
* @see ListenerManager
*/
case class RemoveAListener(who: SimpleActor[Any])
object ListenerManager {
type ActorTest = (SimpleActor[Any], PartialFunction[Any, Boolean])
}
/**
* This trait manages a set of Actors in a publish/subscribe pattern. When you extend your Actor with
* this trait, you automatically get handling for sending messages out to all subscribed Actors. Simply
* override the high-, medium-, or lowPriority handlers to do your message processing. When you want to update
* all subscribers, just call the updateListeners method. The createUpdate method is used to generate
* the message that you want sent to all subscribers.
*
* Note that the AddAListener and RemoveAListener messages (for subscription control) are processed
* after any highPriority or mediumPriority messages are processed, so take care to avoid overly
* broad matches in those handlers that might consume internal messages.
*
* For example, you could write a simple service to provide clock ticks using the following code:
*
*
* case object Tick
*
* object Ticker extends ListenerManager with LiftActor {
* import net.liftweb.util.ActorPing
*
* // Set up the initial tick
* ActorPing.schedule(this, Tick, 1000L)
*
* // This is a placeholder, since we're only interested
* // in Ticks
* def createUpdate = "Registered"
*
* override def mediumPriority = {
* case Tick => {
* updateListeners(Tick)
* ActorPing.schedule(this, Tick, 1000L)
* }
* }
* }
*
*
* A client CometActor could look like:
*
*
* class CometClock extends CometListener {
* val registerWith = Ticker
*
* ... handling code ...
* }
*
*
* @see CometListener
*
*/
trait ListenerManager {
self: SimpleActor[Any] =>
import ListenerManager._
/**
* This is the list of all registered actors
*/
private var listeners: List[ActorTest] = Nil
protected def messageHandler: PartialFunction[Any, Unit] =
highPriority orElse mediumPriority orElse
listenerService orElse lowPriority
protected def listenerService: PartialFunction[Any, Unit] = {
case AddAListener(who, shouldUpdate) =>
val pair = (who, shouldUpdate)
listeners ::= pair
updateIfPassesTest(createUpdate)(pair)
case RemoveAListener(who) =>
listeners = listeners.filter(_._1 ne who)
if (listeners.isEmpty) {
onListenersListEmptied()
}
}
/**
* Called after RemoveAListener-message is processed and no more listeners exist.
* Default does nothing.
*/
protected def onListenersListEmptied() {
}
/**
* Update the listeners with the message generated by createUpdate
*/
protected def updateListeners() {
val update = updateIfPassesTest(createUpdate) _
listeners foreach update
}
/**
* Send a message we create to all of the listeners. Note that with this
* invocation the createUpdate method is not used.
*/
protected def sendListenersMessage(msg: Any) {
listeners foreach (_._1 ! msg)
}
@deprecated("Use sendListenersMessage instead.", "2.6")
protected def updateListeners(msg: Any) { sendListenersMessage(msg) }
/**
* This method provides legacy functionality for filtering messages
* before sending to each registered actor. It is deprecated in
* favor of doing the filtering in the registered Actor's
* message handling partial functions instead.
*/
@deprecated("Accept/reject logic should be done in the partial function that handles the message.", "2.4")
protected def updateIfPassesTest(update: Any)(info: ActorTest) {
info match {
case (who, test) => if (test.isDefinedAt(update) && test(update)) who ! update
}
}
/**
* This method is called when the updateListeners()
method
* needs a message to send to subscribed Actors. In particular, createUpdate
* is used to create the first message that a newly subscribed CometListener
* will receive.
*/
protected def createUpdate: Any
/**
* Override this method to process high priority messages. Note:
* you must not process messages with a wildcard (match all), since
* this will intercept the messages used for subscription control.
*/
protected def highPriority: PartialFunction[Any, Unit] = Map.empty
/**
* Override this method to process medium priority messages. See
* the highPriority method for an important note on wildcard
* processing.
*
* @see #highPriority
*/
protected def mediumPriority: PartialFunction[Any, Unit] = Map.empty
/**
* Override this method to process low priority messages.
*/
protected def lowPriority: PartialFunction[Any, Unit] = Map.empty
}
/**
* This is a legacy trait, left over from Lift's
* Scala 2.7 support. You should use or migrate to
* CometListener instead.
*
* @see CometListener
*/
@deprecated("Use the CometListener trait instead.", "2.4")
trait CometListenee extends CometListener {
self: CometActor =>
}
/**
* A LiftActorJ with ListenerManager. Subclass this class
* to get a Java-usable LiftActorJ with ListenerManager
*/
abstract class LiftActorJWithListenerManager extends LiftActorJ with ListenerManager {
protected override def messageHandler: PartialFunction[Any, Unit] =
highPriority orElse mediumPriority orElse
listenerService orElse lowPriority orElse _messageHandler
}
/**
* This trait adds functionality to automatically register with a given
* Actor using AddAListener and RemoveAListener control messages. The most
* typical usage would be to register with an instance of ListenerManager.
* You will need to provide a def/val for the registerWith
member
* to control which Actor to connect to.
*
* See ListenerManager for a complete example.
*
* @see ListenerManager
*/
trait CometListener extends CometActor {
self: CometActor =>
/**
* This controls which Actor to register with for updates. Typically
* this will be an instance of ListenerActor, although you can provide
* your own subscription handling on top of any SimpleActor.
*/
protected def registerWith: SimpleActor[Any]
/**
* Override this in order to selectively update listeners based on the given message.
* This method has been deprecated because it's executed in a separate context from
* the session's context. This causes problems. Accept/reject logic should be done
* in the partial function that handles the message.
*/
@deprecated("Accept/reject logic should be done in the partial function that handles the message.", "2.4")
protected def shouldUpdate: PartialFunction[Any, Boolean] = {
case _ => true
}
abstract override protected def localSetup() {
registerWith ! AddAListener(this, shouldUpdate)
super.localSetup()
}
abstract override protected def localShutdown() {
registerWith ! RemoveAListener(this)
super.localShutdown()
}
}
trait LiftCometActor extends TypedActor[Any, Any] with ForwardableActor[Any, Any] with Dependent {
def uniqueId: String
private[http] def callInitCometActor(theSession: LiftSession,
theType: Box[String],
name: Box[String],
defaultHtml: NodeSeq,
attributes: Map[String, String]) {
initCometActor(theSession, theType, name, defaultHtml, attributes)
}
/**
* Override in sub-class to customise timeout for the render()-method for the specific comet
*/
def cometRenderTimeout = LiftRules.cometRenderTimeout
/**
* Override in sub-class to customise timeout for AJAX-requests to the comet-component for the specific comet
*/
def cometProcessingTimeout = LiftRules.cometProcessingTimeout
/**
* This is to react to comet-requests timing out.
* When the timeout specified in {@link LiftRules#cometProcessingTimeout} occurs one may override
* this to send a message to the user informing of the timeout.
*
* Do NOT manipulate actor-state here. If you want to manipulate state, send the actor a new message.
*
* Typical example would be:
*
* override def cometTimeoutHandler(): JsCmd = {
* Alert("Timeout processing comet-request, timeout is: " + cometProcessingTimeout + "ms")
* }
*
*/
def cometProcessingTimeoutHandler(): JsCmd = Noop
/**
* This is to react to comet-actors timing out while initial rendering, calls to render().
* When the timeout specified in {@link LiftRules#cometRenderTimeout} occurs one may override
* this to customise the output.
*
* Do NOT manipulate actor-state here. If you want to manipulate state, send the actor a new message.
*
* Typical example would be:
*
* override def renderTimeoutHandler(): Box[NodeSeq] = {
* Full(<div>Comet {this.getClass} timed out, timeout is {cometRenderTimeout}ms</div>)
* }
*
*/
def cometRenderTimeoutHandler(): Box[NodeSeq] = Empty
protected def initCometActor(theSession: LiftSession,
theType: Box[String],
name: Box[String],
defaultHtml: NodeSeq,
attributes: Map[String, String]): Unit
def jsonCall: JsonCall
def theType: Box[String]
def name: Box[String]
def hasOuter: Boolean
def buildSpan(time: Long, xml: NodeSeq): NodeSeq
def parentTag: Elem
/**
* Poke the CometActor and cause it to do a partial update Noop which
* will have the effect of causing the component to redisplay any
* Wiring elements on the component.
* This method is Actor-safe and may be called from any thread, not
* just the Actor's message handler thread.
*/
def poke(): Unit = {}
/**
* Is this CometActor going to capture the initial Req
* object? If yes, override this method and return true
* and override captureInitialReq to capture the Req. Why
* have to explicitly ask for the Req? In order to send Req
* instances across threads, the Req objects must be snapshotted
* which is the process of reading the POST or PUT body from the
* HTTP request stream. We don't want to do this unless we
* have to, so by default the Req is not snapshotted/sent. But
* if you want it, you can have it.
*/
def sendInitialReq_? : Boolean = false
/**
* If the predicate cell changes, the Dependent will be notified
*/
def predicateChanged(which: Cell[_]): Unit = {
poke()
}
/**
* The locale for the session that created the CometActor
*/
def cometActorLocale: Locale = _myLocale
private var _myLocale = Locale.getDefault()
private[http] def setCometActorLocale(loc: Locale) {
_myLocale = loc
}
}
/**
* Subclass from this class if you're in Java-land
* and want a CometActor
*/
abstract class CometActorJ extends LiftActorJ with CometActor {
override def lowPriority = _messageHandler
}
/**
* Subclass from this class if you want a CometActorJ with
* CometListeners
*/
abstract class CometActorJWithCometListener extends CometActorJ with CometListener {
override def lowPriority = _messageHandler
}
/**
* Takes care of the plumbing for building Comet-based Web Apps
*/
trait CometActor extends LiftActor with LiftCometActor with BindHelpers {
private val logger = Logger(classOf[CometActor])
val uniqueId = Helpers.nextFuncName
private var spanId = uniqueId
private var lastRenderTime = Helpers.nextNum
/**
* If we're going to cache the last rendering, here's the
* private cache
*/
private[this] var _realLastRendering: RenderOut = _
/**
* The last rendering (cached or not)
*/
private def lastRendering: RenderOut =
if (dontCacheRendering) {
val ret = (render ++ jsonInCode): RenderOut
theSession.updateFunctionMap(S.functionMap, uniqueId, lastRenderTime)
ret
} else {
_realLastRendering
}
/**
* set the last rendering... ignore if we're not caching
*/
private def lastRendering_=(last: RenderOut) {
if (!dontCacheRendering) {
_realLastRendering = last
}
}
private var wasLastFullRender = false
@transient
private var listeners: List[(ListenerId, AnswerRender => Unit)] = Nil
private var askingWho: Box[LiftCometActor] = Empty
private var whosAsking: Box[LiftCometActor] = Empty
private var answerWith: Box[Any => Any] = Empty
private var deltas: List[Delta] = Nil
private var jsonHandlerChain: PartialFunction[Any, JsCmd] = Map.empty
private val notices = new ListBuffer[(NoticeType.Value, NodeSeq, Box[String])]
private var lastListenTime = millis
private var _theSession: LiftSession = _
def theSession = _theSession
@volatile private var _defaultHtml: NodeSeq = _
@deprecated("Use defaultHtml", "2.3")
def defaultXml = _defaultHtml
/**
* The template that was passed to this component during comet
* initializations
*/
def defaultHtml: NodeSeq = _defaultHtml
private var _name: Box[String] = Empty
/**
* The optional name of this CometActors
*/
def name: Box[String] = _name
private var _theType: Box[String] = Empty
/**
* The optional type of this CometActor
*/
def theType: Box[String] = _theType
private var _attributes: Map[String, String] = Map.empty
def attributes = _attributes
/**
* The lifespan of this component. By default CometActors
* will last for the entire session that they were created in,
* even if the CometActor is not currently visible. You can
* set the lifespan of the CometActor. If the CometActor
* isn't visible on any page for some period after its lifespan
* the CometActor will be shut down.
*/
def lifespan: Box[TimeSpan] = Empty
private var _running = true
private var _shutDownAt = millis
/**
* Is the CometActor running?
*/
protected def running = _running
/**
* It's seriously suboptimal to override this method. Instead
* use localSetup()
*/
protected def initCometActor(theSession: LiftSession,
theType: Box[String],
name: Box[String],
defaultHtml: NodeSeq,
attributes: Map[String, String]) {
if (!dontCacheRendering) {
lastRendering = RenderOut(Full(defaultHtml),
Empty, Empty, Empty, false)
}
this._theType = theType
this._theSession = theSession
this._defaultHtml = defaultHtml
this._name = name
this._attributes = attributes
}
def defaultPrefix: Box[String] = Empty
private lazy val _defaultPrefix: String = (defaultPrefix or _name) openOr "comet"
/**
* Set to 'true' if we should run "render" on every page load
*/
protected def devMode = false
def hasOuter = true
def parentTag =
/**
* Return the list of ListenerIds of all long poll agents that
* are waiting for this CometActor to change its state. This method
* is useful for detecting presence.
*/
protected def cometListeners: List[ListenerId] = listeners.map(_._1)
/**
* This method will be called when there's a change in the long poll
* listeners. The method does nothing, but allows you to get a granular
* sense of how many browsers care about this CometActor. Note that
* this method should not block for any material time and if there's
* any processing to do, use Scheduler.schedule or send a message to this
* CometActor. Do not change the Actor's state from this method.
*/
protected def listenerTransition(): Unit = {}
private def _handleJson(in: Any): JsCmd =
if (jsonHandlerChain.isDefinedAt(in))
jsonHandlerChain(in)
else handleJson(in)
/**
* Prepends the handler to the Json Handlers. Should only be used
* during instantiation
*
* @param h -- the PartialFunction that can handle a JSON request
*/
def appendJsonHandler(h: PartialFunction[Any, JsCmd]) {
jsonHandlerChain = h orElse jsonHandlerChain
}
@deprecated("Use receiveJson and deal in JValues instead of Anys.", "2.6")
def handleJson(in: Any): JsCmd = Noop
/**
* If there's actor-specific JSON behavior on failure to make the JSON
* call, include the JavaScript here.
*/
def onJsonError: Box[JsCmd] = Empty
@deprecated("Use jsonSend and deal in JValues instead of Anys.", "2.6")
lazy val (jsonCall, jsonInCode) = S.buildJsonFunc(Full(_defaultPrefix), onJsonError, _handleJson)
/**
* Override this method to deal with JSON sent from the browser via the sendJson function. This
* is based on the Lift JSON package rather than the handleJson stuff based on the older util.JsonParser. This
* is the preferred mechanism. If you use the jsonSend call, you will get a JObject(JField("command", cmd), JField("param", params))
*/
def receiveJson: PartialFunction[JsonAST.JValue, JsCmd] = Map()
/**
* The JavaScript call that you use to send the data to the server. For example:
* <button onclick={jsonSend("Hello", JsRaw("Dude".encJs))}>Click</button>
*/
def jsonSend: JsonCall = _sendJson
/**
* The call that packages up the JSON and tosses it to the server. If you set autoIncludeJsonCode to true,
* then this will be included in the stuff sent to the server.
*/
def jsonToIncludeInCode: JsCmd = _jsonToIncludeCode
private lazy val (_sendJson, _jsonToIncludeCode) = S.createJsonFunc(Full(_defaultPrefix), onJsonError, receiveJson _)
/**
* Set this method to true to have the Json call code included in the Comet output
*/
def autoIncludeJsonCode: Boolean = false
/**
* Creates the span element acting as the real estate for comet rendering.
*/
def buildSpan(time: Long, xml: NodeSeq): NodeSeq = {
Elem(parentTag.prefix, parentTag.label, parentTag.attributes,
parentTag.scope, Group(xml)) %
new UnprefixedAttribute("id",
Text(spanId),
if (time > 0L) {
new PrefixedAttribute("lift", "when",
time.toString,
Null)
} else {
Null
})
}
/**
* How to report an error that occurs during message dispatch
*/
protected def reportError(msg: String, exception: Exception) {
logger.error(msg, exception)
}
protected override def messageHandler = {
val what = composeFunction
val myPf: PartialFunction[Any, Unit] = new PartialFunction[Any, Unit] {
def apply(in: Any): Unit =
CurrentCometActor.doWith(CometActor.this) {
S.initIfUninitted(theSession) {
RenderVersion.doWith(uniqueId) {
S.functionLifespan(true) {
try {
what.apply(in)
} catch {
case e if exceptionHandler.isDefinedAt(e) => exceptionHandler(e)
case e: Exception => reportError("Message dispatch for " + in, e)
}
if (S.functionMap.size > 0) {
theSession.updateFunctionMap(S.functionMap,
uniqueId, lastRenderTime)
S.clearFunctionMap
}
}
}
}
}
def isDefinedAt(in: Any): Boolean =
CurrentCometActor.doWith(CometActor.this) {
S.initIfUninitted(theSession) {
RenderVersion.doWith(uniqueId) {
S.functionLifespan(true) {
try {
what.isDefinedAt(in)
} catch {
case e if exceptionHandler.isDefinedAt(e) => exceptionHandler(e); false
case e: Exception => reportError("Message test for " + in, e); false
}
}
}
}
}
}
myPf
}
/**
* A part of the CometActor's screen real estate that is not
* updated by default with reRender(). This block of HTML is
* useful for the editor part of a Comet-based control where
* the data is JSON and updated with partialUpdates.
*/
def fixedRender: Box[NodeSeq] = Empty
/**
* Calculate fixedRender and capture the postpage javascript
*/
protected def calcFixedRender: Box[NodeSeq] =
fixedRender.map(ns => theSession.postPageJavaScript() match {
case Nil => ns
case xs => {
ns ++ Script(xs)
}
})
/**
* We have to cache fixedRender and only change it if
* the template changes or we get a reRender(true)
*/
private def internalFixedRender: Box[NodeSeq] =
if (!cacheFixedRender) {
calcFixedRender
} else {
cachedFixedRender.get
}
private val cachedFixedRender: FatLazy[Box[NodeSeq]] = FatLazy(calcFixedRender)
/**
* By default, we do not cache the value of fixedRender. If it's
* expensive to recompute it each time there's a conversion
* of something to a RenderOut, override this method if you
* want to cache fixedRender.
*/
protected def cacheFixedRender = false
/**
* A helpful implicit conversion that takes a NodeSeq => NodeSeq
* (for example a CssSel) and converts it to a Box[NodeSeq] by
* applying the function to defaultHtml
*/
protected implicit def nodeSeqFuncToBoxNodeSeq(f: NodeSeq => NodeSeq):
Box[NodeSeq] = Full(f(defaultHtml))
/**
* By default, `CometActor` handles `RedirectShortcutException`, which is
* used to handle many types of redirects in Lift. If you override this
* `PartialFunction` to do your own exception handling and want redirects
* from e.g. `S.redirectTo` to continue working correctly, make sure you
* chain back to this implementation.
*/
override def exceptionHandler : PartialFunction[Throwable, Unit] = {
case ResponseShortcutException(_, Full(redirectUri), _) =>
partialUpdate(RedirectTo(redirectUri))
case other if super.exceptionHandler.isDefinedAt(other) =>
super.exceptionHandler(other)
}
/**
* Handle messages sent to this Actor before the
*/
def highPriority: PartialFunction[Any, Unit] = Map.empty
def lowPriority: PartialFunction[Any, Unit] = Map.empty
def mediumPriority: PartialFunction[Any, Unit] = Map.empty
private[http] def _lowPriority: PartialFunction[Any, Unit] = {
case s => logger.debug("CometActor " + this + " got unexpected message " + s)
}
private lazy val _mediumPriority: PartialFunction[Any, Unit] = {
case l@Unlisten(seq) => {
lastListenTime = millis
askingWho match {
case Full(who) => forwardMessageTo(l, who) // forward l
case _ => listeners = listeners.filter(_._1 != seq)
}
listenerTransition()
}
case l@Listen(when, seqId, toDo) => {
lastListenTime = millis
askingWho match {
case Full(who) => forwardMessageTo(l, who) // who forward l
case _ =>
if (when < lastRenderTime) {
toDo(AnswerRender(new XmlOrJsCmd(spanId, lastRendering,
buildSpan _, notices toList),
whosAsking openOr this, lastRenderTime, wasLastFullRender))
clearNotices
} else {
deltas.filter(_.when > when) match {
case Nil => listeners = (seqId, toDo) :: listeners
case all@(hd :: xs) =>
toDo(AnswerRender(new XmlOrJsCmd(spanId, Empty, Empty,
Full(all.reverse.foldLeft(Noop)(_ & _.js)), Empty, buildSpan, false, notices toList),
whosAsking openOr this, hd.when, false))
clearNotices
}
}
}
listenerTransition()
}
case PerformSetupComet2(initialReq) => {
// this ! RelinkToActorWatcher
localSetup()
captureInitialReq(initialReq)
performReRender(true)
}
/**
* Update the defaultHtml... sent in dev mode
*/
case UpdateDefaultXml(xml) => {
val redo = xml != _defaultHtml
_defaultHtml = xml
if (redo) {
performReRender(false)
}
}
case AskRender =>
askingWho match {
case Full(who) => forwardMessageTo(AskRender, who) // forward AskRender
case _ => {
if (!deltas.isEmpty || devMode)
try {
performReRender(false)
} catch {
case e if exceptionHandler.isDefinedAt(e) => exceptionHandler(e)
case e: Exception => reportError("Failed performReRender", e)
}
reply(AnswerRender(new XmlOrJsCmd(spanId, lastRendering,
buildSpan _, notices toList),
whosAsking openOr this, lastRenderTime, true))
clearNotices
}
}
case ActionMessageSet(msgs, req) =>
S.doCometParams(req.params) {
S.jsToAppend() match {
case Nil =>
case js => partialUpdate(js)
}
val computed: List[Any] =
msgs.flatMap {
f => try {
List(f())
} catch {
case e if exceptionHandler.isDefinedAt(e) => exceptionHandler(e); Nil
case e: Exception => reportError("Ajax function dispatch", e); Nil
}
}
reply(computed ::: List(S.noticesToJsCmd))
}
case AskQuestion(what, who, otherlisteners) => {
this.spanId = who.uniqueId
this.listeners = otherlisteners ::: this.listeners
startQuestion(what)
whosAsking = Full(who)
this.reRender(true)
listenerTransition()
}
case AnswerQuestion(what, otherListeners) =>
askingWho.foreach {
ah => {
reply("A null message to release the actor from its send and await reply... do not delete this message")
// askingWho.unlink(self)
ah ! ShutDown
this.listeners = this.listeners ::: otherListeners
this.askingWho = Empty
val aw = answerWith
answerWith = Empty
aw.foreach(_(what))
performReRender(true)
listenerTransition()
}
}
case ShutdownIfPastLifespan =>
for {
ls <- lifespan if listeners.isEmpty && (lastListenTime + ls.millis) < millis
} {
this ! ShutDown
}
case ReRender(all) => performReRender(all)
case Error(id, node) => notices += ((NoticeType.Error, node, id))
case Warning(id, node) => notices += ((NoticeType.Warning, node, id))
case Notice(id, node) => notices += ((NoticeType.Notice, node, id))
case ClearNotices => clearNotices
case ShutDown =>
logger.info("The CometActor " + this + " Received Shutdown")
askingWho.foreach(_ ! ShutDown)
theSession.removeCometActor(this)
_localShutdown()
case PartialUpdateMsg(cmdF) => {
val cmd: JsCmd = cmdF.apply
val time = Helpers.nextNum
val delta = JsDelta(time, cmd)
theSession.updateFunctionMap(S.functionMap, uniqueId, time)
S.clearFunctionMap
val m = millis
deltas = (delta :: deltas).filter(d => (m - d.timestamp) < 120000L)
if (!listeners.isEmpty) {
val postPage = theSession.postPageJavaScript()
val rendered =
AnswerRender(new XmlOrJsCmd(spanId, Empty, Empty,
Full(cmd & postPage),
Empty, buildSpan, false,
notices toList),
whosAsking openOr this, time, false)
clearNotices
listeners.foreach(_._2(rendered))
listeners = Nil
listenerTransition()
}
}
}
/**
* It's the main method to override, to define what is rendered by the CometActor
*
* There are implicit conversions for a bunch of stuff to
* RenderOut (including NodeSeq). Thus, if you don't declare the return
* turn to be something other than RenderOut and return something that's
* coercible into RenderOut, the compiler "does the right thing"(tm) for you.
*
* There are implicit conversions for NodeSeq, so you can return a pile of
* XML right here. There's an implicit conversion for NodeSeq => NodeSeq,
* so you can return a function (e.g., a CssBindFunc) that will convert
* the defaultHtml to the correct output. There's an implicit conversion
* from JsCmd, so you can return a pile of JavaScript that'll be shipped
* to the browser.
* Note that the render method will be called each time a new browser tab
* is opened to the comet component or the comet component is otherwise
* accessed during a full page load (this is true if a partialUpdate
* has occurred.) You may want to look at the fixedRender method which is
* only called once and sets up a stable rendering state.
*/
def render: RenderOut
/**
* Cause the entire component to be reRendered and pushed out
* to any listeners. This method will cause the entire component
* to be rendered which can result in a huge blob of JavaScript to
* be sent to the client. It's a much better practice to use
* partialUpdate for non-trivial CometActor components.
*
* @param sendAll -- Should the fixed part of the CometActor be
* rendered.
*/
def reRender(sendAll: Boolean) {
this ! ReRender(sendAll)
}
/**
* Cause the entire component to be reRendered and pushed out
* to any listeners. This method will cause the entire component
* to be rendered which can result in a huge blob of JavaScript to
* be sent to the client. It's a much better practice to use
* partialUpdate for non-trivial CometActor components.
*/
def reRender() {
reRender(false)
}
/**
* Set this method to true if you want to avoid caching the
* rendering. This trades space for time.
*/
protected def dontCacheRendering: Boolean = false
/**
* Clear the common dependencies for Wiring. This
* method will clearPostPageJavaScriptForThisPage() and
* unregisterFromAllDependencies(). The combination
* will result in a clean slate for Wiring during a redraw.
* You can change the behavior of the wiring dependency management
* by overriding this method
*/
protected def clearWiringDependencies() {
if (!manualWiringDependencyManagement) {
theSession.clearPostPageJavaScriptForThisPage()
unregisterFromAllDependencies()
}
}
/**
* By default, Lift deals with managing wiring dependencies.
* This means on each full render (a full render will
* happen on reRender() or on a page load if there have been
* partial updates.) You may want to manually deal with
* wiring dependencies. If you do, override this method
* and return true
*/
protected def manualWiringDependencyManagement = false
private def performReRender(sendAll: Boolean) {
lastRenderTime = Helpers.nextNum
if (sendAll) {
cachedFixedRender.reset
}
if (sendAll || !cacheFixedRender) {
clearWiringDependencies()
}
wasLastFullRender = sendAll & hasOuter
deltas = Nil
if (!dontCacheRendering) {
lastRendering = render ++ jsonInCode
}
theSession.updateFunctionMap(S.functionMap, uniqueId, lastRenderTime)
val rendered: AnswerRender =
AnswerRender(new XmlOrJsCmd(spanId, lastRendering, buildSpan _, notices toList),
this, lastRenderTime, sendAll)
clearNotices
listeners.foreach(_._2(rendered))
listeners = Nil
}
def unWatch = partialUpdate(Call("liftComet.lift_unlistWatch", uniqueId))
/**
* Poke the CometActor and cause it to do a partial update Noop which
* will have the effect of causing the component to redisplay any
* Wiring elements on the component.
* This method is Actor-safe and may be called from any thread, not
* just the Actor's message handler thread.
*/
override def poke(): Unit = {
if (running) {
partialUpdate(Noop)
}
}
/**
* Perform a partial update of the comet component based
* on the JsCmd. This means that the JsCmd will be sent to
* all of the currently listening browser tabs. This is the
* preferred method over reRender to update the component
*/
protected def partialUpdate(cmd: => JsCmd) {
this ! PartialUpdateMsg(() => cmd)
}
protected def startQuestion(what: Any) {
}
/**
* This method will be called after the Actor has started. Do any setup here.
* DO NOT do initialization in the constructor or in initCometActor... do it here.
*/
protected def localSetup(): Unit = {
}
/**
* Comet Actors live outside the HTTP request/response cycle.
* However, it may be useful to know what Request led to the
* creation of the CometActor. You can override this method
* and capture the initial Req object. Note that keeping a reference
* to the Req may lead to memory retention issues if the Req contains
* large message bodies, etc. It's optimal to capture the path
* or capture any request parameters that you care about rather
* the keeping the whole Req reference.
*/
protected def captureInitialReq(initialReq: Box[Req]) {
}
private def _localShutdown() {
localShutdown()
clearNotices
listeners = Nil
askingWho = Empty
whosAsking = Empty
deltas = Nil
jsonHandlerChain = Map.empty
_running = false
_shutDownAt = millis
}
/**
* This method will be called as part of the shut-down of the actor. Release any resources here.
*/
protected def localShutdown(): Unit = {
}
/**
* Compose the Message Handler function. By default,
* composes highPriority orElse mediumPriority orElse internalHandler orElse
* lowPriority orElse internalHandler. But you can change how
* the handler works if doing stuff in highPriority, mediumPriority and
* lowPriority is not enough.
*/
protected def composeFunction: PartialFunction[Any, Unit] = composeFunction_i
private def composeFunction_i: PartialFunction[Any, Unit] = {
// if we're no longer running don't pass messages to the other handlers
// just pass them to our handlers
if (!_running && (millis - 20000L) > _shutDownAt)
_mediumPriority orElse _lowPriority
else
highPriority orElse mediumPriority orElse
_mediumPriority orElse lowPriority orElse _lowPriority
}
/**
* A helper for binding which uses the defaultHtml property.
*/
def bind(prefix: String, vals: BindParam*): NodeSeq = bind(prefix, _defaultHtml, vals: _*)
/**
* A helper for binding which uses the defaultHtml property and the
* default prefix.
*/
def bind(vals: BindParam*): NodeSeq = bind(_defaultPrefix, vals: _*)
/**
* Ask another CometActor a question. That other CometActor will
* take over the screen real estate until the question is answered.
*/
protected def ask(who: LiftCometActor, what: Any)(answerWith: Any => Unit) {
who.callInitCometActor(theSession, Full(who.uniqueId), name, defaultHtml, attributes)
theSession.addCometActor(who)
// who.link(this)
who ! PerformSetupComet2(Empty)
askingWho = Full(who)
this.answerWith = Full(answerWith)
who ! AskQuestion(what, this, listeners)
// this ! AskRender
}
protected def answer(answer: Any) {
whosAsking.foreach(_ !? AnswerQuestion(answer, listeners))
whosAsking = Empty
performReRender(false)
}
/**
* Convert a NodeSeq => NodeSeq to a RenderOut. The render method
* returns a RenderOut. This method implicitly (in Scala) or explicitly
* (in Java) will convert a NodeSeq => NodeSeq to a RenderOut. This
* is helpful if you use Lift's CSS Selector Transforms to define
* rendering.
*/
protected implicit def nsToNsFuncToRenderOut(f: NodeSeq => NodeSeq) =
new RenderOut((Box !! defaultHtml).map(f), internalFixedRender, if (autoIncludeJsonCode) Full(jsonToIncludeInCode & S.jsToAppend())
else {
S.jsToAppend match {
case Nil => Empty
case x :: Nil => Full(x)
case xs => Full(xs.reduceLeft(_ & _))
}
}, Empty, false)
/**
* Convert a Seq[Node] (the superclass of NodeSeq) to a RenderOut.
* The render method
* returns a RenderOut. This method implicitly (in Scala) or explicitly
* (in Java) will convert a NodeSeq to a RenderOut. This
* is helpful if you return a NodeSeq from your render method.
*/
protected implicit def arrayToRenderOut(in: Seq[Node]): RenderOut = new RenderOut(Full(in: NodeSeq), internalFixedRender, if (autoIncludeJsonCode) Full(jsonToIncludeInCode & S.jsToAppend())
else {
S.jsToAppend match {
case Nil => Empty
case x :: Nil => Full(x)
case xs => Full(xs.reduceLeft(_ & _))
}
}, Empty, false)
protected implicit def jsToXmlOrJsCmd(in: JsCmd): RenderOut = new RenderOut(Empty, internalFixedRender, if (autoIncludeJsonCode) Full(in & jsonToIncludeInCode & S.jsToAppend()) else Full(in & S.jsToAppend()), Empty, false)
implicit def pairToPair(in: (String, Any)): (String, NodeSeq) = (in._1, Text(in._2 match {
case null => "null"
case s => s.toString
}))
implicit def nodeSeqToFull(in: NodeSeq): Box[NodeSeq] = Full(in)
implicit def elemToFull(in: Elem): Box[NodeSeq] = Full(in)
/**
* Similar with S.error
*/
def error(n: String) {
error(Text(n))
}
/**
* Similar with S.error
*/
def error(n: NodeSeq) {
notices += ((NoticeType.Error, n, Empty))
}
/**
* Similar with S.error
*/
def error(id: String, n: NodeSeq) {
notices += ((NoticeType.Error, n, Full(id)))
}
/**
* Similar with S.error
*/
def error(id: String, n: String) {
error(id, Text(n))
}
/**
* Similar with S.notice
*/
def notice(n: String) {
notice(Text(n))
}
/**
* Similar with S.notice
*/
def notice(n: NodeSeq) {
notices += ((NoticeType.Notice, n, Empty))
}
/**
* Similar with S.notice
*/
def notice(id: String, n: NodeSeq) {
notices += ((NoticeType.Notice, n, Full(id)))
}
/**
* Similar with S.notice
*/
def notice(id: String, n: String) {
notice(id, Text(n))
}
/**
* Similar with S.warning
*/
def warning(n: String) {
warning(Text(n))
}
/**
* Similar with S.warning
*/
def warning(n: NodeSeq) {
notices += ((NoticeType.Warning, n, Empty))
}
/**
* Similar with S.warning
*/
def warning(id: String, n: NodeSeq) {
notices += ((NoticeType.Warning, n, Full(id)))
}
/**
* Similar with S.warning
*/
def warning(id: String, n: String) {
warning(id, Text(n))
}
private def clearNotices {
notices clear
}
}
abstract class Delta(val when: Long) {
def js: JsCmd
val timestamp = millis
}
case class JsDelta(override val when: Long, js: JsCmd) extends Delta(when)
sealed abstract class CometMessage
/**
* Impersonates the actual comet response content
*/
private[http] class XmlOrJsCmd(val id: String,
_xml: Box[NodeSeq],
_fixedXhtml: Box[NodeSeq],
val javaScript: Box[JsCmd],
val destroy: Box[JsCmd],
spanFunc: (Long, NodeSeq) => NodeSeq,
ignoreHtmlOnJs: Boolean,
notices: List[(NoticeType.Value, NodeSeq, Box[String])]) {
def this(id: String, ro: RenderOut, spanFunc: (Long, NodeSeq) => NodeSeq, notices: List[(NoticeType.Value, NodeSeq, Box[String])]) =
this (id, ro.xhtml, ro.fixedXhtml, ro.script, ro.destroyScript, spanFunc, ro.ignoreHtmlOnJs, notices)
val xml = _xml.flatMap(content => S.session.map(s => s.processSurroundAndInclude("JS SetHTML id: " + id, content)))
val fixedXhtml = _fixedXhtml.flatMap(content => S.session.map(s => s.processSurroundAndInclude("JS SetHTML id: " + id, content)))
/**
* Returns the JsCmd that will be sent to client
*/
def toJavaScript(session: LiftSession, displayAll: Boolean): JsCmd = {
val updateJs =
(if (ignoreHtmlOnJs) Empty else xml, javaScript, displayAll) match {
case (Full(xml), Full(js), false) => LiftRules.jsArtifacts.setHtml(id, Helpers.stripHead(xml)) & JsCmds.JsTry(js, false)
case (Full(xml), _, false) => LiftRules.jsArtifacts.setHtml(id, Helpers.stripHead(xml))
case (Full(xml), Full(js), true) => LiftRules.jsArtifacts.setHtml(id + "_outer", (
spanFunc(0, Helpers.stripHead(xml)) ++ fixedXhtml.openOr(Text("")))) & JsCmds.JsTry(js, false)
case (Full(xml), _, true) => LiftRules.jsArtifacts.setHtml(id + "_outer", (
spanFunc(0, Helpers.stripHead(xml)) ++ fixedXhtml.openOr(Text(""))))
case (_, Full(js), _) => js
case _ => JsCmds.Noop
}
val fullUpdateJs =
LiftRules.cometUpdateExceptionHandler.vend.foldLeft(updateJs) { (commands, catchHandler) =>
JsCmds.Run(
"try{" +
commands.toJsCmd +
"}catch(e){" +
catchHandler.toJsCmd +
"}"
)
}
var ret: JsCmd = JsCmds.JsTry(JsCmds.Run("destroy_" + id + "();"), false) &
fullUpdateJs &
JsCmds.JsTry(JsCmds.Run("destroy_" + id + " = function() {" + (destroy.openOr(JsCmds.Noop).toJsCmd) + "};"), false)
S.appendNotices(notices)
ret = S.noticesToJsCmd & ret
ret
}
def inSpan: NodeSeq = xml.openOr(Text("")) ++ javaScript.map(s => Script(s)).openOr(Text(""))
def outSpan: NodeSeq = Script(Run("var destroy_" + id + " = function() {" + (destroy.openOr(JsCmds.Noop).toJsCmd) + "}")) ++
fixedXhtml.openOr(Text(""))
}
/**
* Update the comet XML on each page reload in dev mode
*/
case class UpdateDefaultXml(xml: NodeSeq) extends CometMessage
case class PartialUpdateMsg(cmd: () => JsCmd) extends CometMessage
case object AskRender extends CometMessage
case class AnswerRender(response: XmlOrJsCmd, who: LiftCometActor, when: Long, displayAll: Boolean) extends CometMessage
case class PerformSetupComet2(initialReq: Box[Req]) extends CometMessage
case object ShutdownIfPastLifespan extends CometMessage
case class AskQuestion(what: Any, who: LiftCometActor, listeners: List[(ListenerId, AnswerRender => Unit)]) extends CometMessage
case class AnswerQuestion(what: Any, listeners: List[(ListenerId, AnswerRender => Unit)]) extends CometMessage
case class Listen(when: Long, uniqueId: ListenerId, action: AnswerRender => Unit) extends CometMessage
case class Unlisten(uniqueId: ListenerId) extends CometMessage
case class ActionMessageSet(msg: List[() => Any], req: Req) extends CometMessage
case class ReRender(doAll: Boolean) extends CometMessage
case class ListenerId(id: Long)
case class Error(id: Box[String], msg: NodeSeq) extends CometMessage
case class Warning(id: Box[String], msg: NodeSeq) extends CometMessage
case class Notice(id: Box[String], msg: NodeSeq) extends CometMessage
case object ClearNotices extends CometMessage
object Error {
def apply(node: NodeSeq): Error = Error(Empty, node)
def apply(node: String): Error = Error(Empty, Text(node))
def apply(id: String, node: String): Error = Error(Full(id), Text(node))
def apply(id: String, node: NodeSeq): Error = Error(Full(id), node)
}
object Warning {
def apply(node: NodeSeq): Warning = Warning(Empty, node)
def apply(node: String): Warning = Warning(Empty, Text(node))
def apply(id: String, node: String): Warning = Warning(Full(id), Text(node))
def apply(id: String, node: NodeSeq): Warning = Warning(Full(id), node)
}
object Notice {
def apply(node: NodeSeq): Notice = Notice(Empty, node)
def apply(node: String): Notice = Notice(Empty, Text(node))
def apply(id: String, node: String): Notice = Notice(Full(id), Text(node))
def apply(id: String, node: NodeSeq): Notice = Notice(Full(id), node)
}
/**
* The RenderOut case class contains the rendering for the CometActor.
* Because of the implicit conversions, RenderOut can come from
*
* @param xhtml is the "normal" render body
* @param fixedXhtml is the "fixed" part of the body. This is ignored unless reRender(true)
* @param script is the script to be executed on render. This is where you want to put your script
* @param destroyScript is executed when the comet widget is redrawn ( e.g., if you register drag or mouse-over or some events, you unregister them here so the page doesn't leak resources.)
* @param ignoreHtmlOnJs -- if the reason for sending the render is a Comet update, ignore the xhtml part and just run the JS commands. This is useful in IE when you need to redraw the stuff inside ... just doing innerHtml on is broken in IE
*/
case class RenderOut(xhtml: Box[NodeSeq], fixedXhtml: Box[NodeSeq], script: Box[JsCmd], destroyScript: Box[JsCmd], ignoreHtmlOnJs: Boolean) {
def this(xhtml: NodeSeq) = this (Full(xhtml), Empty, Empty, Empty, false)
def this(xhtml: NodeSeq, js: JsCmd) = this (Full(xhtml), Empty, Full(js), Empty, false)
def this(xhtml: NodeSeq, js: JsCmd, destroy: JsCmd) = this (Full(xhtml), Empty, Full(js), Full(destroy), false)
def ++(cmd: JsCmd) =
RenderOut(xhtml, fixedXhtml, script.map(_ & cmd) or Full(cmd),
destroyScript, ignoreHtmlOnJs)
}
private[http] object Never extends Serializable
© 2015 - 2025 Weber Informatics LLC | Privacy Policy