Skip to content

Commit

Permalink
Closes #1094. Closes #1086. Improved handling of statelessness includ…
Browse files Browse the repository at this point in the history
…ing a snippet trait that handles a given snippet in stateless mode and the ability to inspect cookies and user agents to determine if a request should be treated as stateless
  • Loading branch information
dpp committed Aug 23, 2011
1 parent cbe9037 commit 23bfc86
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 22 deletions.
27 changes: 26 additions & 1 deletion web/webkit/src/main/scala/net/liftweb/http/LiftRules.scala
Expand Up @@ -64,6 +64,12 @@ object LiftRulesMocker {
() => devTestLiftRulesInstance.box.openOr( LiftRules.prodInstance)
}

/**
* The data structure that contains information to determine if the
* request should be treated as a stateful or stateless request
*/
final case class StatelessReqTest(path: List[String], httpReq: HTTPRequest)

/**
* The Lift configuration singleton
*/
Expand All @@ -87,6 +93,13 @@ object LiftRules extends LiftRulesMocker {
*/
type StatelessTestPF = PartialFunction[List[String], Boolean]


/**
* The test between the path of a request, the HTTP request, and whether that path
* should result in stateless servicing of that path
*/
type StatelessReqTestPF = PartialFunction[StatelessReqTest, Boolean]

type RewritePF = PartialFunction[RewriteRequest, RewriteResponse]
type SnippetPF = PartialFunction[List[String], NodeSeq => NodeSeq]
type LiftTagPF = PartialFunction[(String, Elem, MetaData, NodeSeq, String), NodeSeq]
Expand Down Expand Up @@ -345,10 +358,22 @@ class LiftRules() extends Factory with FormVendor with LazyLoggable {
* Certain paths within your application can be marked as stateless
* and if there is access to Lift's stateful facilities (setting
* SessionVars, updating function tables, etc.) the developer will
* receive a notice and the operation will not complete
* receive a notice and the operation will not complete. This test
* has been deprecated in favor of statelessReqTest which also passes
* the HTTPRequest instance for testing of the user agent and other stuff.
*/
@deprecated("Use statelessReqTest")
val statelessTest = RulesSeq[StatelessTestPF]


/**
* Certain paths and requests within your application can be marked as stateless
* and if there is access to Lift's stateful facilities (setting
* SessionVars, updating function tables, etc.) the developer will
* receive a notice and the operation will not complete.
*/
val statelessReqTest = RulesSeq[StatelessReqTestPF]

val statelessSession: FactoryMaker[Req => LiftSession with StatelessSession] =
new FactoryMaker((req: Req) => new LiftSession(req.contextPath,
Helpers.nextFuncName,
Expand Down
18 changes: 18 additions & 0 deletions web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala
Expand Up @@ -1556,6 +1556,11 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
S.locateSnippet(snippet).map(_(kids)) openOr {
val (cls, method) = splitColonPair(snippet)
(locateAndCacheSnippet(cls)) match {
// deal with a stateless request when a snippet has
// different behavior in stateless mode
case Full(inst: StatelessBehavior) if !stateful_? =>
inst.behavior(method)(kids)

case Full(inst: StatefulSnippet) if !stateful_? =>
reportSnippetError(page, snippetName,
LiftRules.SnippetFailures.StateInStateless,
Expand Down Expand Up @@ -1685,6 +1690,12 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,
wholeTag)
}
} catch {
case ExclosedSnippetFailure(e) =>
reportSnippetError(page, snippetName,
e.snippetFailure,
e.buildStackTrace,
wholeTag)

case e: SnippetFailureException =>
reportSnippetError(page, snippetName,
e.snippetFailure,
Expand Down Expand Up @@ -1725,6 +1736,13 @@ class LiftSession(private[http] val _contextPath: String, val uniqueId: String,

}

private object ExclosedSnippetFailure {
def unapply(e: Throwable): Option[SnippetFailureException] = e.getCause match {
case null => None
case e: SnippetFailureException => Some(e)
case _ => None
}
}

/**
* Apply HTML specific corrections such as adding the context path etc.
Expand Down
24 changes: 17 additions & 7 deletions web/webkit/src/main/scala/net/liftweb/http/Req.scala
Expand Up @@ -356,9 +356,10 @@ object Req {
}

def apply(original: Req, rewrite: List[LiftRules.RewritePF]): Req =
this.apply(original, rewrite, Nil)
this.apply(original, rewrite, Nil, Nil)

def apply(original: Req, rewrite: List[LiftRules.RewritePF], statelessTest: List[LiftRules.StatelessTestPF]): Req = {
def apply(original: Req, rewrite: List[LiftRules.RewritePF], statelessTest: List[LiftRules.StatelessTestPF],
otherStatelessTest: List[LiftRules.StatelessReqTestPF]): Req = {

def processRewrite(path: ParsePath, params: Map[String, String]): RewriteResponse =
NamedPF.applyBox(RewriteRequest(path, original.requestType, original.request), rewrite) match {
Expand All @@ -369,7 +370,11 @@ object Req {

val rewritten = processRewrite(original.path, Map.empty)

val stateless = NamedPF.applyBox(rewritten.path.wholePath, statelessTest)
val wholePath = rewritten.path.wholePath


val stateless = NamedPF.applyBox(StatelessReqTest(wholePath, original.request), otherStatelessTest) or
NamedPF.applyBox(wholePath, statelessTest)

new Req(rewritten.path, original.contextPath,
original.requestType, original.contentType, original.request,
Expand All @@ -378,10 +383,12 @@ object Req {
original.paramCalculator, original.addlParams ++ rewritten.params)
}

def apply(request: HTTPRequest, rewrite: List[LiftRules.RewritePF], nanoStart: Long): Req = this.apply(request, rewrite, Nil, nanoStart)
def apply(request: HTTPRequest, rewrite: List[LiftRules.RewritePF], nanoStart: Long): Req =
this.apply(request, rewrite, Nil, Nil, nanoStart)


def apply(request: HTTPRequest, rewrite: List[LiftRules.RewritePF], statelessTest: List[LiftRules.StatelessTestPF], nanoStart: Long): Req = {
def apply(request: HTTPRequest, rewrite: List[LiftRules.RewritePF], statelessTest: List[LiftRules.StatelessTestPF],
otherStatelessTest: List[LiftRules.StatelessReqTestPF], nanoStart: Long): Req = {
val reqType = RequestType(request)
val contextPath = LiftRules.calculateContextPath() openOr request.contextPath
val turi = if (request.uri.length >= contextPath.length) request.uri.substring(contextPath.length) else ""
Expand Down Expand Up @@ -488,7 +495,10 @@ object Req {
paramCalcInfo.thunk
}

val stateless = NamedPF.applyBox(rewritten.path.wholePath, statelessTest)
val wholePath = rewritten.path.wholePath

val stateless = NamedPF.applyBox(StatelessReqTest(wholePath, request), otherStatelessTest) or
NamedPF.applyBox(wholePath, statelessTest)

new Req(rewritten.path, contextPath, reqType,
contentType, request, nanoStart,
Expand All @@ -497,7 +507,7 @@ object Req {

private def fixURI(uri: String) = uri indexOf ";jsessionid" match {
case -1 => uri
case x@_ => uri substring (0, x)
case x@_ => uri.substring(0, x)
}

/**
Expand Down
15 changes: 9 additions & 6 deletions web/webkit/src/main/scala/net/liftweb/http/S.scala
Expand Up @@ -1528,7 +1528,8 @@ for {
!old.path.partPath.isEmpty &&
(old.request ne null))
Req(old, S.sessionRewriter.map(_.rewrite) :::
LiftRules.statefulRewrite.toList, LiftRules.statelessTest.toList)
LiftRules.statefulRewrite.toList, LiftRules.statelessTest.toList,
LiftRules.statelessReqTest.toList)
else old
}

Expand Down Expand Up @@ -2164,7 +2165,7 @@ for {
*/
def functionMap: Map[String, AFuncHolder] = __functionMap.box.openOr(Map())

private def testFunctionMap[T](f: T): T =
private def testFunctionMap[T](f: => T): T =
session match {
case Full(s) if s.stateful_? => f
case _ => throw new StateInStatelessException(
Expand All @@ -2175,10 +2176,11 @@ for {
* Clears the function map. potentially very destuctive... use at your own risk!
*/
def clearFunctionMap {
if (__functionMap.box.map(_.size).openOr(0) > 0) { // Issue #1037
testFunctionMap {
__functionMap.box.foreach(ignore => __functionMap.set(Map()))
}
if (__functionMap.box.map(_.size).openOr(0) > 0) {
// Issue #1037
testFunctionMap {
__functionMap.box.foreach(ignore => __functionMap.set(Map()))
}
}
}

Expand Down Expand Up @@ -2626,6 +2628,7 @@ for {
else {
val req = Req(httpRequest, LiftRules.statelessRewrite.toList,
LiftRules.statelessTest.toList,
LiftRules.statelessReqTest.toList,
System.nanoTime)

CurrentReq.doWith(req) {
Expand Down
30 changes: 29 additions & 1 deletion web/webkit/src/main/scala/net/liftweb/http/StatefulSnippet.scala
Expand Up @@ -21,7 +21,7 @@ import net.liftweb.common._
import net.liftweb._
import util._
import Helpers._
import scala.xml.{NodeSeq, Elem}
import xml.{Text, NodeSeq, Elem}

/**
* The same StatefulSnippet instance is used across a given page rendering.
Expand Down Expand Up @@ -167,3 +167,31 @@ trait DispatchSnippet {
def dispatch: DispatchIt
}

/**
* Mix this snippet into any snippet. If the snippet is invoked in response to a
* stateless request, then the behavior method is called with the method name of
* the snippet (usually render, but there may be others if you specify a method
* after the snippet name: MySnippet.dothing).
*/
trait StatelessBehavior {
/**
* Given the method name, return the transformation for the method
*/
def behavior(methodName: String): NodeSeq => NodeSeq
}

/**
* A simpler way to define behavior if the snippet is invoked. Just implement the behavior() method
* and all methods for the snippet will use that behavior.
*/
trait DefaultStatelessBehavior extends StatelessBehavior {
def behavior(): NodeSeq => NodeSeq
def behavior(methodName: String): NodeSeq => NodeSeq = behavior()
}

/**
* A "default" implementation of StatelessBehavior. Just ignore everything and return a zero-length Text.
*/
trait BlankStatelessBehavior extends StatelessBehavior {
def behavior(methodName: String): NodeSeq => NodeSeq = ignore => Text("")
}
Expand Up @@ -969,14 +969,14 @@ object JsRules {
* The default duration for displaying FadeOut and FadeIn
* messages.
*/
@deprecated
//@deprecated
@volatile var prefadeDuration: Helpers.TimeSpan = 5 seconds

/**
* The default fade time for fading FadeOut and FadeIn
* messages.
*/
@deprecated
//@deprecated
@volatile var fadeTime: Helpers.TimeSpan = 1 second
}

Expand Up @@ -58,7 +58,9 @@ trait HTTPProvider {
}

val newReq = Req(req, LiftRules.statelessRewrite.toList,
LiftRules.statelessTest.toList, System.nanoTime)
LiftRules.statelessTest.toList,
LiftRules.statelessReqTest.toList,
System.nanoTime)

CurrentReq.doWith(newReq) {
URLRewriter.doWith(url =>
Expand Down
3 changes: 2 additions & 1 deletion web/webkit/src/main/scala/net/liftweb/mockweb/MockWeb.scala
Expand Up @@ -97,7 +97,8 @@ object MockWeb {
if(liftRulesEnabled) {
// Apply stateless rewrites
Req(req, LiftRules.statelessRewrite.toList,
LiftRules.statelessTest.toList, System.nanoTime)
LiftRules.statelessTest.toList,
LiftRules.statelessReqTest.toList, System.nanoTime)
} else {
Req(req, Nil, System.nanoTime)
}
Expand Down
45 changes: 42 additions & 3 deletions web/webkit/src/main/scala/net/liftweb/sitemap/Loc.scala
Expand Up @@ -187,8 +187,8 @@ trait Loc[T] {
* the page into stateless mode)
*/
def stateless_? : Boolean =
if (Props.devMode) calcStateless()
else _frozenStateless
if (Props.devMode) (calcStateless() || reqCalcStateless())
else (_frozenStateless || reqCalcStateless())

/**
* A lazy val used to track statelessness for non-dev mode.
Expand All @@ -200,11 +200,37 @@ trait Loc[T] {
* The method to calculate if this Loc is stateless. By default
* looks for the Loc.Stateless Param
*/
protected def calcStateless() = allParams.find {
protected def calcStateless(): Boolean = allParams.find {
case Loc.Stateless => true
case _ => false
}.isDefined

/**
* Find the stateless calculation Loc params
*/
protected def findStatelessCalc: (Box[Loc.CalcStateless], Box[Loc.CalcParamStateless[T]]) = (allParams.collect {
case v @ Loc.CalcStateless(_) => v
}.headOption,
allParams.collect {
case v @ Loc.CalcParamStateless(_) => v
}.headOption)

/**
* The cached Loc params
*/
protected lazy val _foundStatelessCalc: (Box[Loc.CalcStateless], Box[Loc.CalcParamStateless[T]]) = findStatelessCalc

protected def foundStatelessCalc: (Box[Loc.CalcStateless], Box[Loc.CalcParamStateless[T]]) =
if (Props.devMode) findStatelessCalc else _foundStatelessCalc

/**
* run the stateless calculation
*/
protected def reqCalcStateless(): Boolean = {
val (np, param) = foundStatelessCalc
(np.map(_.f()) or param.map(_.f(currentValue))) openOr false
}

lazy val calcSnippets: SnippetTest = {
def buildPF(in: Loc.Snippet): PartialFunction[String, NodeSeq => NodeSeq] = {
new PartialFunction[String, NodeSeq => NodeSeq] {
Expand Down Expand Up @@ -674,6 +700,8 @@ object Loc {
*/
case object Stateless extends AnyLocParam



/**
* The Loc does not represent a menu itself, but is the parent menu for
* children (implies HideIfNoKids)
Expand All @@ -685,6 +713,17 @@ object Loc {
*/
trait UserLocParam[-T] extends LocParam[T]

/**
* A function that calculates the statelessness of the Loc for the given request
*/
case class CalcStateless(f: () => Boolean) extends AnyLocParam

/**
* A function that calculates the statelessness of the Loc for the given request
* with the parameterized type passed into the function
*/
case class CalcParamStateless[-T](f: Box[T] => Boolean) extends LocParam[T]

/**
* A subclass of LocSnippets with a built in dispatch method (no need to
* implement isDefinedAt or apply... just
Expand Down

0 comments on commit 23bfc86

Please sign in to comment.