第4章、REST

本章通过Lift的RestHelper特点来讨论REST Web服务的配方有关介绍,请查看Lift Wiki页面Simply Lift的第5章

本章的示例代码位于https://github.com/LiftCookbook/cookbook_rest

DRY网址

问题

您发现自己重复部分URL路径,您RestHelper不想重复自己(DRY)。

用于prefix您的RestHelper

package code.rest

import net.liftweb.http.rest.RestHelper
import net.liftweb.http.LiftRules

object IssuesService extends RestHelper {

  def init() : Unit = {
    LiftRules.statelessDispatch.append(IssuesService)
  }

  serve("issues" / "by-state" prefix {
    case "open" :: Nil XmlGet _ => <p>None open</p>
    case "closed" :: Nil XmlGet _ => <p>None closed</p>
    case "closed" :: Nil XmlDelete _ => <p>All deleted</p>
  })
}

该服务响应/ issues / by-state / open/ issue / by-state / closed的 URL,我们将常见部分作为一个prefix

将其连接到Boot.scala中:

import code.rest.IssuesService
IssuesService.init()

我们可以用cURL测试服务:

$ curl -H'Content-Type:application / xml'
    http:// localhost:8080 / issues / by-state / open
<?xml version="1.0" encoding="UTF-8"?>
<p>None open</p>

$ curl -X DELETE -H'Content-Type:application / xml'
    http:// localhost:8080 / issues / by-state / closed
<?xml version="1.0" encoding="UTF-8"?>
<p>
All deleted
</p>

讨论

您可以拥有多个serveRestHelper,这有助于您提供REST服务结构。

在这个例子中,我们已经任意决定返回XML,并使用XmlGet使用XML请求进行匹配XmlDeleteXML请求的测试需要text / xmlapplication / xml的内容类型,或以.xml结尾的路径请求这就是为什么cURL请求包含带-H标志的头文件如果我们没有列出,请求将不符合我们的任何模式,结果将是404响应。

也可以看看

“返回JSON”提供了接受和返回JSON的示例。

缺少文件后缀

问题

RestHelper希望文件名作为URL的一部分,但后缀(扩展名)缺失,您需要它。

访问req.path.suffix恢复后缀。

例如,当处理/download/123.png时,您可以重建 123.png

package code.rest

import net.liftweb.http.rest.RestHelper
import net.liftweb.http.LiftRules
import xml.Text

object Reunite extends RestHelper {

  private def reunite(name: String, suffix: String) =
    if (suffix.isEmpty) name else name+"."+suffix

  serve {
    case "download" :: file :: Nil Get req =>
      Text("You requested "+reunite(file, req.path.suffix))
  }

  def init() : Unit = {
    LiftRules.statelessDispatch.append(Reunite)
  }

}

我们在下载时进行匹配,而不是file直接使用该值,我们先通过该reunite函数传递它以附加后缀(如果有的话)。

使用像cURL这样的命令请求此URL会按预期显示文件名:

$ curl http://127.0.0.1:8080/download/123.png
 <?xml version="1.0" encoding="UTF-8"?>
You requested 123.png

讨论

当Lift解析请求时,它将请求分成组成部分(例如,将路径转换为a List[String])。这包括一些后缀的分离当您想要根据后缀更改行为时,这对于模式匹配是有利的,但在这种特殊情况下是一个障碍。

只有这些定义的后缀才能LiftRules.explicitlyParsedSuffixes从文件名中拆分。这包括许多常见的文件后缀(如.png.atom.json),还有一些您可能不太熟悉,比如.com

请注意,如果后缀不在explicitlyParsedSuffixes,则后缀将为空字符串,name(在上一个示例中)将是后缀仍然附加的文件名。

根据您的需要,您可以添加一个保护条件来检查文件后缀:

case "download" :: file :: Nil Get req if req.path.suffix == "png" =>
  Text("You requested PNG file called "+file)

或者不是简单地附加后缀,您可以借此机会进行一些计算,并决定要发回的内容。例如,如果客户端支持WebP图像格式,您可能更愿意发送:

package code.rest

import net.liftweb.http.rest.RestHelper
import net.liftweb.http.LiftRules
import xml.Text

object Reunite extends RestHelper  {

  def init() : Unit = {
    LiftRules.statelessDispatch.append(Reunite)
  }

  serve {
    case "negotiate" :: file :: Nil Get req =>
      val toSend =
        if (req.header("Accept").exists(_ == "image/webp")) file+".webp"
        else file+".png"

      Text("You requested "+file+", would send "+toSend)
  }

}

调用此服务将Accept在确定要发送的资源之前检查HTTP 头文件:

$ curl http:// localhost:8080 / negotiate / 123
<?xml version="1.0" encoding="UTF-8"?>
你要求123,会发123.png

$ curl http:// localhost:8080 / negotiate / 123 -H“Accept:image / webp”
<?xml version="1.0" encoding="UTF-8"?>
你要求123,会发123.webp

也可以看看

“电子邮件地址丢失.com”显示如何从中删除项目explicitlyParsedSuffixes

源代码HttpHelpers.scala包含explicitlyParsedSuffixes列表,这是从URL提升分析的后缀的默认列表。

从电子邮件地址丢失.com

当向REST服务提交电子邮件地址时,在您的REST服务可以处理请求之前,将删除结束于.com的域

修改LiftRules.explicitlyParsedSuffixes,以便Lift不会更改以.com结尾的URL 

Boot.scala中

import net.liftweb.util.Helpers
LiftRules.explicitlyParsedSuffixes = Helpers.knownSuffixes - "com"

讨论

默认情况下,Lift将从URL中删除文件后缀,以便轻松匹配后缀。需要匹配所有以.xml.pdf结尾的请求但是,.com也注册为这些后缀之一,但如果您的URL以电子邮件地址结尾,那么这样做是不方便的。

请注意,这不会影响网址中的电子邮件地址。例如,考虑以下REST服务:

package code.rest

import net.liftweb.http.rest.RestHelper
import net.liftweb.http.LiftRules
import xml.Text

object Suffix extends RestHelper {

  def init() : Unit = {
    LiftRules.statelessDispatch.append(Suffix)
  }

  serve {
    case "email" :: e :: "send" :: Nil Get req =>
      Text("In middle: "+e)

    case "email" :: e :: Nil Get req =>
      Text("At end: "+e)
  }

}

initBoot.scala调用此服务方法,我们可以提出请求并观察问题:

$ curl http:// localhost:8080/email/you@example.com/send
<?xml version="1.0" encoding="UTF-8"?>
在中间:you@example.com

$ curl http:// localhost:8080/email/you@example.com
<?xml version="1.0" encoding="UTF-8"?>
结束:你@的例子

.COM被当作一个文件后缀,这就是为什么从后缀的列表中删除的解决方案将解决此问题。

请注意,由于其他顶级域名(例如.uk.nl.gov)不在explicitlyParsedSuffixes,所以这些电子邮件地址保持不变。

也可以看看

“遗失文件后缀”更详细地描述了后缀处理。

无法匹配文件后缀

问题

您正在尝试匹配文件后缀(扩展名),但您的匹配失败。

确保您匹配的后缀包含在 LiftRules.explicitlyParsedSuffixes

举个例子,或许你想/ reports / URL 匹配任何以.csv结尾的东西

case Req("reports" :: name :: Nil, "csv", GetRequest) =>
  Text("Here's your CSV report for "+name)

您期待/reports/foo.csv生成“以下是您的foo的CSV报告”,但是您将获得404。

要解决此问题,"csv"请将其包含为Lift知道从URL分割的文件后缀。Boot.scala中,调用:

LiftRules.explicitlyParsedSuffixes += "csv"

该模式现在将匹配。

讨论

不添加"csv"explicitlyParsedSuffixes,示例URL会匹配:

case Req("reports" :: name :: Nil, "", GetRequest) =>
  Text("Here's your CSV report for "+name)

在这里我们匹配no后缀("")。在这种情况下,name将被设置为"foo.csv"这是因为Lift仅将文件后缀从URL的尾部分离出来才能注册的文件后缀explicitlyParsedSuffixes因为"csv"不是默认的注册后缀之一,"foo.csv"不会被拆分。这就是为什么"csv"Req模式匹配的后缀位置不符合请求,但在该位置的空字符串将会。

也可以看看

“遗失文件后缀”更详细地解释了Lift中的后缀删除。

在REST服务中接受二进制数据

问题

您要在RESTful服务中接受图像上传或其他二进制数据。

访问您的REST助手中的请求正文:

package code.rest

import net.liftweb.http.rest.RestHelper
import net.liftweb.http.LiftRules

object Upload extends RestHelper {

  def init() : Unit = {
    LiftRules.statelessDispatch.append(Upload)
  }

  serve {
    case "upload" :: Nil Post req =>
      for {
        bodyBytes <- req.body
      } yield <info>Received {bodyBytes.length} bytes</info>
  }

}

Boot.scala中将其连接到您的应用程序中

import code.rest.Upload
Upload.init()

使用像cURL这样的工具测试此服务:

$ curl -X POST --data-binary“@ dog.jpg”-H'Content-Type:image / jpg'
    http://127.0.0.1:8080/upload
<?xml version="1.0" encoding="UTF-8"?>
<info>收到1365418字节</info>

讨论

在这个例子中,二进制数据是通过req.body访问的,它返回a Box[Array[Byte]]我们把它变成一个Box[Elem]送回客户端。RestHelper反过来,这意味着XmlResponseLift处理。

请注意,Web容器(如Jetty和Tomcat)可能会限制上传的大小。你会认识到这种情况,例如错误java.lang.IllegalStateException: Form too large705784>200000检查容器的文档以更改这些限制。

要限制您接受的图像类型,您可以为匹配添加防护条件,但您可能会发现通过将逻辑移动到unapply对象上的方法来获得更多可读的代码例如,要限制上传到只是一个JPEG你可以说

serve {
  case "jpg" :: Nil Post JPeg(req) =>
    for {
      bodyBytes <- req.body
    } yield <info>Jpeg Received {bodyBytes.length} bytes</info>
  }

object JPeg {
  def unapply(req: Req): Option[Req] =
    req.contentType.filter(_ == "image/jpg").map(_ => req)
}

我们定义了一个调用的提取器JPegSome[Req]如果上传的内容类型是image / jpg,则返回否则,结果将是None这在REST模式匹配中用于JPeg(req)请注意,unapply需要返回Option[Req],因为这是预期的Post提取器。

也可以看看

Odersky 等人 ,(2008),“Scala编程”第24章详细讨论了提取器。

“文件上传”描述了基于表单的(多部分)文件上传

投放视频

问题

您要使用HTML5 video标签来投放视频。

使用a RestHelper返回StreamingResponse一个视频

一个简单的例子:

import net.liftweb.http.StreamingResponse
import net.liftweb.http.rest.RestHelper

object Streamer extends RestHelper {
 serve {
    case req@Req(("video" :: id :: Nil), _, _) =>
      val fis = new FileInputStream(new File(id))
      val size = file.length - 1
      val content_type = "Content-Type" -> "video/mp4" // replace with appropriate format

      val start = 0L
      val end = size

      val headers =
        ("Connection" -> "close") ::
        ("Transfer-Encoding" -> "chunked") ::
        content_type ::
        ("Content-Range" -> ("bytes " + start.toString + "-" + end.toString + "/" + file.length.toString)) ::
        Nil

      () => StreamingResponse(
        data = fis,
        onEnd = fis.close,
        size,
        headers,
        cookies = Nil,
        code = 206
    )
  }
}

讨论

上述示例不允许客户端在文件中跳过。Range头必须以促进这一解析。它指定客户端希望接收的文件的哪一部分。请求标头包含req@Req(urlParams, _, _)在上一个示例中的模式中。开始和结束可以这样提取:

val (start,end) =
  req.header("Range") match {
    case Full(r) => {
      (
         parseNumber(r.substring(r.indexOf("bytes=") + 6)),
         {
           if (r.endsWith("-"))
             file.length - 1
           else
             parseNumber(r.substring(r.indexOf("-") + 1))
         }
       )
    }
    case _ => (0L, file.length - 1)
  }

接下来,响应必须跳过客户端希望开始的视频中的位置。这是通过执行以下操作完成的:

val fis: FileInputStream = ... // Shown in the first example
fis.skip(start)

也可以看看

这个食谱基于Streaming Response wiki文章。

ObeseRabbit是展示此功能的小型应用程序。

“流内容”描述StreamingResponse和类似类型的响应,如OutputStreamResponseInMemoryResponse

返回JSON

问题

您要从REST调用返回JSON。

使用提升JSON域专用语言(DSL)。 例如:

package code.rest

import net.liftweb.http.rest.RestHelper
import net.liftweb.http.LiftRules
import net.liftweb.json.JsonAST._
import net.liftweb.json.JsonDSL._

object QuotationsAPI extends RestHelper {

  def init() : Unit = {
    LiftRules.statelessDispatch.append(QuotationsAPI)
  }

  serve {
    case "quotation" :: Nil JsonGet req =>
     ("text" -> "A beach house isn't just real estate. It's a state of mind.") ~
     ("by" -> "Douglas Adams") : JValue
  }

}

将其连接到Boot.scala中

import code.rest.QuotationsAPI
QuotationsAPI.init()

运行此示例生成:

$ curl -H 'Content-type: text/json' http://127.0.0.1:8080/quotation
{
 "text":"A beach house isn't just real estate. It's a state of mind.",
 "by":"Douglas Adams"
}

讨论

类型归属在JSON表达式(的端部: JValue)告诉表达预计类型的编译器 JValue这是允许DSL应用的必需条件。如果您正在调用定义为返回的函数,则不需要JValue

JSON DSL允许您创建嵌套的结构,列表和您期望的其他一切JSON。

除了DSL之外,您可以使用以下Extraction.decompose方法从案例类创建JSON 

import net.liftweb.json.Extraction
import net.liftweb.json.DefaultFormats

case class Quote(text: String, by: String)
val quote = Quote(
  "A beach house isn't just real estate. It's a state of mind.",
  "Douglas Adams")

implicit val formats = DefaultFormats
val json : JValue = Extraction decompose quote

这也会产生一个JValue,打印时会是:

{
 "text":"A beach house isn't just real estate. It's a state of mind.",
 "by":"Douglas Adams"
}

也可以看看

用于lift-json项目的README文件是使用JSON DSL的一个很好的例子。

Google Sitemap

问题

您想使用Lift的呈现功能制作Google Sitemap。

创建Google站点地图结构,并将其绑定到Lift中的任何模板。然后将其作为XML内容提供。

首先得有个sitemap.html在你的web应用程序包含有效XML的标记地图文件夹:

<?xml version="1.0" encoding="utf-8" ?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <url data-lift="SitemapContent.base">
        <loc></loc>
        <changefreq>每日</changefreq>
        <priority>1.0</priority>
        <lastmod></lastmod>
    </url>
    <url data-lift="SitemapContent.list">
        <loc></loc>
        <lastmod></lastmod>
    </url>
</urlset>

制作一个代码段来填补所需的差距:

package code.snippet

import org.joda.time.DateTime
import net.liftweb.util.CssSel
import net.liftweb.http.S
import net.liftweb.util.Helpers._

class SitemapContent {

  case class Post(url: String, date: DateTime)

  lazy val entries =
    Post("/welcome", new DateTime) :: Post("/about", new DateTime) :: Nil

  val siteLastUdated = new DateTime

  def base: CssSel =
    "loc *" #> "http://%s/".format(S.hostName) &
      "lastmod *" #> siteLastUpdated.toString("yyyy-MM-dd'T'HH:mm:ss.SSSZZ")

  def list: CssSel =
    "url *" #> entries.map(post =>
      "loc *" #> "http://%s%s".format(S.hostName, post.url) &
        "lastmod *" #> post.date.toString("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"))

}

这个例子是使用两页的罐头数据。

/ sitemap的REST服务中应用模板和代码段

package code.rest

import net.liftweb.http._
import net.liftweb.http.rest.RestHelper

object Sitemap extends RestHelper {
  serve {
    case "sitemap" :: Nil Get req =>
      XmlResponse(
        S.render(<lift:embed what="sitemap" />, req.request).head
      )
  }
}

将其连接到Boot.scala中的应用程序,并强制升级以XML格式处理/站点地图

LiftRules.statelessDispatch.append(code.rest.Sitemap)

LiftRules.htmlProperties.default.set({ request: Req =>
  request.path.partPath match {
    case "sitemap" :: Nil => OldHtmlProperties(request.userAgent)
    case _                => Html5Properties(request.userAgent)
  }
})

使用像cURL这样的工具测试此服务:

$ curl http://127.0.0.1:8080/sitemap

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>http://127.0.0.1/ </loc>
      <changefreq>daily </changefreq>
      <priority>1.0 </priority>
      <lastmod>2013-02-10T19:16:12.433 + 00:00 </lastmod>
  </url>
  <url>
      <loc>http://127.0.0.1/welcome </loc>
      <lastmod>2013-02-10T19:16:12.434 + 00:00 </lastmod>
  </url><url>
      <loc>http://127.0 .0.1 / about </loc>
      <lastmod>2013-02-10T19:16:12.434 + 00:00</lastmod>
  </url>
</urlset>

讨论

您可能会想知道为什么我们在使用常规的HTML模板和代码段时我们已经使用了REST。原因是我们需要XML而不是HTML输出。我们使用相同的机制,但是调用它并将其包装在一个XmlResponse

使用Lift的常规代码段机制来呈现此XML是很方便的。但是,尽管我们正在使用XML,但Lift将使用默认的HTML5解析器来解析sitemap.html解析器的行为遵循HTML5规范,这与您从XML解析器可能期望的行为不同。例如,HTML解析器移动无效位置中的识别HTML标签。为了避免这些情况,我们修改了Boot.scala以使用/ sitemap的XML解析器

S.render方法需要a NodeSeqHTTPRequest我们首先通过运行sitemap.html片段来提供; 第二个只是当前的请求。 XmlResponse需要一个Node而不是一个NodeSeq,这就是为什么我们称之为head响应中只有一个节点,这是我们需要的。

请注意,Google Sitemaps需要日期为ISO 8601格式。内置的java.text.SimpleDateFormatJava 7之前不支持此格式。如果使用Java 6,则需要org.joda.time.DateTime像本例中那样使用。

也可以看看

您可以在Google的网站管理员工具网站上找到Sitemap通讯录的说明

从本机iOS应用程序调用REST服务

问题

您希望从原生iOS设备到提升REST服务进行HTTP POST。

使用NSURLConnection,以确保您设置content-typeapplication/json

例如,假设我们要调用这个服务:

package code.rest

import net.liftweb.http.rest.RestHelper
import net.liftweb.json.JsonDSL._
import net.liftweb.json.JsonAST._

object Shouty extends RestHelper {

  def greet(name: String) : JValue =
    "greeting" -> ("HELLO "+name.toUpperCase)

  serve {
    case "shout" :: Nil JsonPost json->request =>
      for { JString(name) <- (json \\ "name").toOpt }
      yield greet(name)
  }

}

该服务期望具有参数的JSON文章name,并将其作为JSON对象返回问候语。为了演示来往服务的数据,我们可以在Boot.scala中包含该服务

LiftRules.statelessDispatch.append(Shouty)

从命令行调用它:

$ curl -d '{ "name" : "World" }' -X POST -H 'Content-type: application/json'
   http://127.0.0.1:8080/shout
{
  "greeting":"HELLO WORLD"
}

我们可以使用NSURLConnection以下方式实现POST请求

static NSString *url = @"http://localhost:8080/shout";

-(void) postAction {
  // JSON data:
  NSDictionary* dic = @{@"name": @"World"};
  NSData* jsonData =
    [NSJSONSerialization dataWithJSONObject:dic options:0 error:nil];
  NSMutableURLRequest *request = [
    NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]
    cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];

  // Construct HTTP request:
  [request setHTTPMethod:@"POST"];
  [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
  [request setValue:[NSString stringWithFormat:@"%d", [jsonData length]]
    forHTTPHeaderField:@"Content-Length"];
  [request setHTTPBody: jsonData];

  // Send the request:
  NSURLConnection *con = [[NSURLConnection alloc]
    initWithRequest:request delegate:self];
}

- (void)connection:(NSURLConnection *)connection
  didReceiveResponse:(NSURLResponse *)response {
   // Start off with new, empty, response data
   self.receivedJSONData = [NSMutableData data];
}

- (void)connection:(NSURLConnection *)connection
  didReceiveData:(NSData *)data {
   // append incoming data
   [self.receivedJSONData appendData:data];
}

- (void)connection:(NSURLConnection *)connection
  didFailWithError:(NSError *)error {
   NSLog(@"Error occurred ");
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
  NSError *e = nil;
  NSDictionary *JSON =
    [NSJSONSerialization JSONObjectWithData: self.receivedJSONData
    options: NSJSONReadingMutableContainers error: &e];
  NSLog(@"Return result: %@", [JSON objectForKey:@"greeting"]);
}

很明显,在这个例子中,我们使用了硬编码的值和URL,但这将有望成为您的应用程序的起点。

讨论

有很多方法可以从iOS中执行HTTP POST,并且可能会令人困惑,以确定正确的方法,特别是没有外部库的帮助。上一个示例使用iOS本机API。

另一种方法是使用AFNetworking这是iOS开发的流行外部库,可以应对许多场景,使用起来很简单

#import "AFHTTPClient.h"
#import "AFNetworking.h"
#import "JSONKit.h"

static NSString *url = @"http://localhost:8080/shout";

-(void) postAction {
  // JSON data:
  NSDictionary* dic = @{@"name": @"World"};
  NSData* jsonData =
   [NSJSONSerialization dataWithJSONObject:dic options:0 error:nil];

  // Construct HTTP request:
  NSMutableURLRequest *request =
   [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]
    cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
  [request setHTTPMethod:@"POST"];
  [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
  [request setValue:[NSString stringWithFormat:@"%d", [jsonData length]]
    forHTTPHeaderField:@"Content-Length"];
  [request setHTTPBody: jsonData];

  // Send the request:
  AFJSONRequestOperation *operation =
    [[AFJSONRequestOperation alloc] initWithRequest: request];
  [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation,
    id responseObject)
  {
     NSString *response = [operation responseString];

     // Use JSONKit to deserialize the response into NSDictionary
     NSDictionary *deserializedJSON = [response objectFromJSONString];
     [deserializedJSON count];

     // The response object can be a NSDicionary or a NSArray:
      if([deserializedJSON count]> 0) {
         NSLog(@"Return value: %@",[deserializedJSON objectForKey:@"greeting"]);
      }
      else {
        NSArray *deserializedJSONArray = [response objectFromJSONString];
        NSLog(@"Return array value: %@", deserializedJSONArray );
      }
  }failure:^(AFHTTPRequestOperation *operation, NSError *error)
  {
    NSLog(@"Error: %@",error);
  }];
  [operation start];
}

NSURLConnection方法更为通用,为您提供自己的解决方案,例如使内容类型更具体化。但是,AFNetworking很受欢迎,你可能更喜欢这条路线。

也可以看看

您可以Simply Lift中找到“完整的REST示例”,这是您对Lift的呼叫的良好测试平台。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值