/*
* Copyright 2010 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 {
package rest {
import net.liftweb.json._
import net.liftweb.common._
import net.liftweb.util.Props
import scala.xml.{Elem, Node, Text}
/**
* Mix this trait into a class to provide a lost of REST helper methods
*/
trait RestHelper extends LiftRules.DispatchPF {
import JsonAST._
/**
* Will the request accept a JSON response? Yes if
* the Accept header contains "application/json" or
* the Accept header is missing or contains "star/star" and the
* suffix is "json". Override this method to provide your
* own logic
*/
protected def jsonResponse_?(in: Req): Boolean = {
val accept = in.headers("accept")
accept.find(_.toLowerCase.indexOf("application/json") >= 0).isDefined ||
((in.path.suffix equalsIgnoreCase "json") &&
(accept.isEmpty ||
accept.find(_.toLowerCase.indexOf("*/*") >= 0).isDefined)) ||
suplimentalJsonResponse_?(in)
}
/**
* If there are additional custom rules (e.g., looking at query parameters)
* you can override this method which is consulted if the other rules
* in jsonResponse_? fail
*/
protected def suplimentalJsonResponse_?(in: Req): Boolean = false
/**
* Will the request accept an XML response? Yes if
* the Accept header contains "text/xml" or
* the Accept header is missing or contains "star/star" and the
* suffix is "xml". Override this method to provide your
* own logic
*/
protected def xmlResponse_?(in: Req): Boolean = {
val accept = in.headers("accept")
accept.find(_.toLowerCase.indexOf("text/xml") >= 0).isDefined ||
((in.path.suffix equalsIgnoreCase "xml") &&
(accept.isEmpty ||
accept.find(_.toLowerCase.indexOf("*/*") >= 0).isDefined)) ||
suplimentalXmlResponse_?(in)
}
/**
* If there are additional custom rules (e.g., looking at query parameters)
* you can override this method which is consulted if the other rules
* in xmlResponse_? fail
*/
protected def suplimentalXmlResponse_?(in: Req): Boolean = false
/**
* A trait that defines the TestReq extractor. Is
* the request something that expects JSON, XML in the response.
* Subclass this trait to change the behavior
*/
protected trait TestReq {
/**
* Test to see if the request is expecting JSON, XML in the response.
* The path and a Tuple2 representing RequestType and the Req
* instance are extracted.
*/
def unapply(r: Req): Option[(List[String], (RequestType, Req))] =
if (testResponse_?(r))
Some(r.path.partPath -> (r.requestType -> r)) else None
def testResponse_?(r: Req): Boolean
}
protected trait XmlTest {
def testResponse_?(r: Req): Boolean = xmlResponse_?(r)
}
protected trait JsonTest {
def testResponse_?(r: Req): Boolean = jsonResponse_?(r)
}
/**
* The stable identifier for JsonReq. You can use it
* as an extractor.
*/
protected lazy val JsonReq = new TestReq with JsonTest
/**
* The stable identifier for XmlReq. You can use it
* as an extractor.
*/
protected lazy val XmlReq = new TestReq with XmlTest
/**
* A trait that defines the TestGet extractor. Is
* the request a GET and something that expects JSON or XML in the response.
* Subclass this trait to change the behavior
*/
protected trait TestGet {
/**
* Test to see if the request is a GET and expecting JSON in the response.
* The path and the Req instance are extracted.
*/
def unapply(r: Req): Option[(List[String], Req)] =
if (r.get_? && testResponse_?(r))
Some(r.path.partPath -> r) else None
def testResponse_?(r: Req): Boolean
}
/**
* The stable identifier for JsonGet. You can use it
* as an extractor.
*/
protected lazy val JsonGet = new TestGet with JsonTest
/**
* The stable identifier for XmlGet. You can use it
* as an extractor.
*/
protected lazy val XmlGet = new TestGet with XmlTest
/**
* A trait that defines the TestDelete extractor. Is
* the request a DELETE and something that expects
* JSON or XML in the response.
* Subclass this trait to change the behavior
*/
protected trait TestDelete {
/**
* Test to see if the request is a DELETE and
* expecting JSON or XML in the response.
* The path and the Req instance are extracted.
*/
def unapply(r: Req): Option[(List[String], Req)] =
if (r.requestType.delete_? && testResponse_?(r))
Some(r.path.partPath -> r) else None
def testResponse_?(r: Req): Boolean
}
/**
* The stable identifier for JsonDelete. You can use it
* as an extractor.
*/
protected lazy val JsonDelete = new TestDelete with JsonTest
/**
* The stable identifier for XmlDelete. You can use it
* as an extractor.
*/
protected lazy val XmlDelete = new TestDelete with XmlTest
/**
* A trait that defines the TestPost extractor. Is
* the request a POST, has JSON or XML data in the post body
* and something that expects JSON or XML in the response.
* Subclass this trait to change the behavior
*/
protected trait TestPost[T] {
/**
* Test to see if the request is a POST, has JSON data in the
* body and expecting JSON in the response.
* The path, JSON Data and the Req instance are extracted.
*/
def unapply(r: Req): Option[(List[String], (T, Req))] =
if (r.post_? && testResponse_?(r))
body(r).toOption.map(t => (r.path.partPath -> (t -> r)))
else None
def testResponse_?(r: Req): Boolean
def body(r: Req): Box[T]
}
/**
* a trait that extracts the JSON body from a request It is
* composed with a TestXXX to get the correct thing for the extractor
*/
protected trait JsonBody {
def body(r: Req): Box[JValue] = r.json
}
/**
* a trait that extracts the XML body from a request It is
* composed with a TestXXX to get the correct thing for the extractor
*/
protected trait XmlBody {
def body(r: Req): Box[Elem] = r.xml
}
/**
* An extractor that tests the request to see if it's a GET and
* if it is, the path and the request are extracted. It can
* be used as:<br/>
* <pre>case "api" :: id :: _ Get req => ...</pre><br/>
* or<br/>
* <pre>case Get("api" :: id :: _, req) => ...</pre><br/> *
*/
protected object Get {
def unapply(r: Req): Option[(List[String], Req)] =
if (r.get_?) Some(r.path.partPath -> r) else None
}
/**
* An extractor that tests the request to see if it's a POST and
* if it is, the path and the request are extracted. It can
* be used as:<br/>
* <pre>case "api" :: id :: _ Post req => ...</pre><br/>
* or<br/>
* <pre>case Post("api" :: id :: _, req) => ...</pre><br/>
*/
protected object Post {
def unapply(r: Req): Option[(List[String], Req)] =
if (r.post_?) Some(r.path.partPath -> r) else None
}
/**
* An extractor that tests the request to see if it's a PUT and
* if it is, the path and the request are extracted. It can
* be used as:<br/>
* <pre>case "api" :: id :: _ Put req => ...</pre><br/>
* or<br/>
* <pre>case Put("api" :: id :: _, req) => ...</pre><br/>
*/
protected object Put {
def unapply(r: Req): Option[(List[String], Req)] =
if (r.put_?) Some(r.path.partPath -> r) else None
}
/**
* An extractor that tests the request to see if it's a DELETE and
* if it is, the path and the request are extracted. It can
* be used as:<br/>
* <pre>case "api" :: id :: _ Delete req => ...</pre><br/>
* or<br/>
* <pre>case Delete("api" :: id :: _, req) => ...</pre><br/>
*/
protected object Delete {
def unapply(r: Req): Option[(List[String], Req)] =
if (r.requestType.delete_?) Some(r.path.partPath -> r) else None
}
/**
* A function that chooses JSON or XML based on the request..
* Use with serveType
*/
implicit def jxSel(req: Req): Box[JsonXmlSelect] =
if (jsonResponse_?(req)) Full(JsonSelect)
else if (xmlResponse_?(req)) Full(XmlSelect)
else None
/**
* Serve a given request by determining the request type, computing
* the response and then converting the response to the given
* type (e.g., JSON or XML).<br/><br/>
* @param selection -- a function that determines the response type
* based on the Req.
* @parama pf -- a PartialFunction that converts the request to a
* response type (e.g., a case class that contains the response).
* @param cvt -- a function that converts from the response type
* to a the appropriate LiftResponse based on the selected response
* type.
*/
protected def serveType[T, SelectType](selection: Req => Box[SelectType])
(pf: PartialFunction[Req, Box[T]])
(implicit cvt: PartialFunction[(SelectType, T, Req), LiftResponse]): Unit = {
serve(new PartialFunction[Req, () => Box[LiftResponse]] {
def isDefinedAt(r: Req): Boolean =
selection(r).isDefined && pf.isDefinedAt(r)
def apply(r: Req): () => Box[LiftResponse] =
() => {
pf(r) match {
case Full(resp) =>
val selType = selection(r).open_! // Full because pass isDefinedAt
if (cvt.isDefinedAt((selType, resp, r)))
Full(cvt((selType, resp, r)))
else emptyToResp(ParamFailure("Unabled to convert the message",
Empty, Empty, 500))
case e: EmptyBox => emptyToResp(e)
}
}
}
)
}
/**
* Serve a request returning either JSON or XML.
*
* @parama pf -- a Partial Function that converts the request into
* an intermediate response.
* @param cvt -- convert the intermediate response to a LiftResponse
* based on the request being for XML or JSON. If T is JsonXmlAble,
* there are built-in converters. Further, you can return auto(thing)
* and that will invoke built-in converters as well. The built-in
* converters use Lift JSON's Extraction.decompose to convert the object
* into JSON and then Xml.toXml() to convert to XML.
*/
protected def serveJx[T](pf: PartialFunction[Req, Box[T]])
(implicit cvt: JxCvtPF[T]): Unit =
serveType(jxSel)(pf)(cvt)
protected type JxCvtPF[T] =
PartialFunction[(JsonXmlSelect, T, Req), LiftResponse]
/**
* Serve a request returning either JSON or XML.
*
* @parama pf -- a Partial Function that converts the request into
* Any (note that the response must be convertable into
* JSON vis Lift JSON Extraction.decompose
*/
protected def serveJxa(pf: PartialFunction[Req, Box[Any]]): Unit =
serveType(jxSel)(pf)(new PartialFunction[(JsonXmlSelect,
Any, Req), LiftResponse] {
def isDefinedAt(p: (JsonXmlSelect, Any, Req)) =
convertAutoJsonXmlAble.isDefinedAt((p._1, AutoJsonXmlAble(p._2), p._3))
def apply(p: (JsonXmlSelect, Any, Req)) =
convertAutoJsonXmlAble.apply((p._1, AutoJsonXmlAble(p._2), p._3))
})
/**
* Return the implicit Formats instance for JSON conversion
*/
protected implicit def formats: Formats = net.liftweb.json.DefaultFormats
/**
* The default way to convert a JsonXmlAble into JSON or XML
*/
protected implicit lazy val convertJsonXmlAble:
PartialFunction[(JsonXmlSelect, JsonXmlAble, Req), LiftResponse] = {
case (JsonSelect, obj, _) => Extraction.decompose(obj)
case (XmlSelect, obj, _) =>
Xml.toXml(Extraction.decompose(obj)).toList match {
case x :: _ => x
case _ => Text("")
}
}
/**
* The class that wraps anything for auto conversion to JSON or XML
*/
protected final case class AutoJsonXmlAble(obj: Any)
/**
* wrap anything for autoconversion to JSON or XML
*/
protected def auto(in: Any): Box[AutoJsonXmlAble] =
Full(new AutoJsonXmlAble(in))
/**
* Wrap a Box of anything for autoconversion to JSON or XML
*/
protected def auto(in: Box[Any]): Box[AutoJsonXmlAble] =
in.map(obj => new AutoJsonXmlAble(obj))
/**
* An implicit conversion that converts AutoJsonXmlAble into
* JSON or XML
*/
protected implicit lazy val convertAutoJsonXmlAble:
PartialFunction[(JsonXmlSelect, AutoJsonXmlAble, Req), LiftResponse] = {
case (JsonSelect, AutoJsonXmlAble(obj), _) =>
Extraction.decompose(obj)
case (XmlSelect, AutoJsonXmlAble(obj), _) =>
Xml.toXml(Extraction.decompose(obj)).toList match {
case x :: _ => x
case _ => Text("")
}
}
/**
* The stable identifier for JsonPost. You can use it
* as an extractor.
*/
protected lazy val JsonPost = new TestPost[JValue] with JsonTest with JsonBody
/**
* The stable identifier for XmlPost. You can use it
* as an extractor.
*/
protected lazy val XmlPost = new TestPost[Elem] with XmlTest with XmlBody
/**
* A trait that defines the TestPut extractor. Is
* the request a PUT, has JSON or XML data in the put body
* and something that expects JSON or XML in the response.
* Subclass this trait to change the behavior
*/
protected trait TestPut[T] {
/**
* Test to see if the request is a PUT, has JSON or XML data in the
* body and expecting JSON or XML in the response.
* The path, Data and the Req instance are extracted.
*/
def unapply(r: Req): Option[(List[String], (T, Req))] =
if (r.put_? && testResponse_?(r))
body(r).toOption.map(b => (r.path.partPath -> (b -> r))) else None
def testResponse_?(r: Req): Boolean
def body(r: Req): Box[T]
}
/**
* The stable identifier for JsonPut. You can use it
* as an extractor.
*/
protected lazy val JsonPut = new TestPut[JValue] with JsonTest with JsonBody
/**
* The stable identifier for XmlPut. You can use it
* as an extractor.
*/
protected lazy val XmlPut = new TestPut[Elem] with XmlTest with XmlBody
/**
* Extract a Pair using the same syntax that you use to make a Pair
*/
protected object -> {
def unapply[A, B](s: (A, B)): Option[(A, B)] = Some(s._1 -> s._2)
}
@volatile private var _dispatch: List[LiftRules.DispatchPF] = Nil
private lazy val nonDevDispatch = _dispatch.reverse
private def dispatch: List[LiftRules.DispatchPF] =
if (Props.devMode) _dispatch.reverse else nonDevDispatch
/**
* Is the Rest helper defined for a given request
*/
def isDefinedAt(in: Req) = dispatch.find(_.isDefinedAt(in)).isDefined
/**
* Apply the Rest helper
*/
def apply(in: Req): () => Box[LiftResponse] =
dispatch.find(_.isDefinedAt(in)).get.apply(in)
/**
* Add request handlers
*/
protected def serve(handler: PartialFunction[Req, () => Box[LiftResponse]]):
Unit = _dispatch ::= handler
/**
* Turn T into the return type expected by DispatchPF as long
* as we can convert T to a LiftResponse.
*/
protected implicit def thingToResp[T](in: T)(implicit c: T => LiftResponse):
() => Box[LiftResponse] = () => Full(c(in))
/**
* Turn a Box[T] into the return type expected by
* DispatchPF. Note that this method will return
* messages from Failure() and return codes and messages
* from ParamFailure[Int[(msg, _, _, code)
*/
protected implicit def boxToResp[T](in: Box[T])
(implicit c: T => LiftResponse): () => Box[LiftResponse] =
in match {
case Full(v) => () => Full(c(v))
case e: EmptyBox => () => emptyToResp(e)
}
protected def emptyToResp(eb: EmptyBox): Box[LiftResponse] =
eb match {
case ParamFailure(msg, _, _, code: Int) =>
Full(InMemoryResponse(msg.getBytes("UTF-8"),
("Content-Type" ->
"text/plain; charset=utf-8") ::
Nil, Nil, code))
case Failure(msg, _, _) =>
Full(NotFoundResponse(msg))
case _ => Empty
}
/**
* Turn an Option[T] into the return type expected by
* DispatchPF.
*/
protected implicit def optionToResp[T](in: Option[T])
(implicit c: T => LiftResponse): () => Box[LiftResponse] =
in match {
case Some(v) => () => Full(c(v))
case _ => () => Empty
}
/**
* Turn a () => Box[T] into the return type expected by
* DispatchPF. Note that this method will return
* messages from Failure() and return codes and messages
* from ParamFailure[Int[(msg, _, _, code)
*/
protected implicit def boxFuncToResp[T](in: () => Box[T])
(implicit c: T => LiftResponse): () => Box[LiftResponse] =
() => {
in() match {
case ParamFailure(msg, _, _, code: Int) =>
Full(InMemoryResponse(msg.getBytes("UTF-8"),
("Content-Type" ->
"text/plain; charset=utf-8") ::
Nil, Nil, code))
case Failure(msg, _, _) =>
Full(NotFoundResponse(msg))
case Full(v) => Full(c(v))
case _ => Empty
}
}
/**
* Turn an Option[T] into the return type expected by
* DispatchPF.
*/
protected implicit def optionFuncToResp[T](in: () => Option[T])
(implicit c: T => LiftResponse): () => Box[LiftResponse] =
() =>
in() match {
case Some(v) => Full(c(v))
case _ => Empty
}
/**
* Convert a Node to an XmlResponse
*/
protected implicit def nodeToResp(in: scala.xml.Node): LiftResponse =
XmlResponse(in)
/**
* Convert a JValue to a LiftResponse
*/
implicit def jsonToResp(in: JsonAST.JValue): LiftResponse =
JsonResponse(in)
/**
* Convert a JsExp to a LiftResponse
*/
implicit def jsExpToResp(in: js.JsExp): LiftResponse =
JsonResponse(in)
}
/**
* A trait that can be mixed into an class (probably a case class)
* so that the class can be converted automatically into JSON or XML
*/
trait JsonXmlAble
/**
* This trait is part of the ADT that allows the choice between
*/
sealed trait JsonXmlSelect
/**
* The Type for JSON
*/
final case object JsonSelect extends JsonXmlSelect
/**
* The type for XML
*/
final case object XmlSelect extends JsonXmlSelect
}
}
}