Command-line REST client: building a working example
You’ve looked into a number of interesting concepts about Scala in this chapter, and it’ll be nice to see some of these concepts in action together. In this section you’ll build a command-line-based REST client in Scala script.You’re going to use the Apache HttpClient library to handle HTTP connections and various HTTP methods. To make a REST call to a RESTful service, you have to be aware of the operations supported by the service. To test your client you need a RESTful web service. You could use free public web services to test the client, but to have better control of the operations on the service you’ll create one. You could use any REST tool or a framework to build the REST service. I’ll use a Java servlet (Java developers are familiar with it) to build the service to test the REST client. Understanding how the service is implemented isn’t important for this example.
The simple way to create a RESTful service for now is to use a Java servlet, as shown in the following listing.
TBD
Introducing HttpClient library
HttpClient is a client-side HTTP transport library. The purpose of HttpClient is to transmit and receive HTTP messages. It’s not a browser, and it doesn’t execute JavaScript or try to guess the content type or other functionality unrelated to the HTTP transport. The most essential function of HttpClient is to execute HTTP methods. Users are supposed to provide a request object like HttpPost or HttpGet, and the HttpClient is expected to transmit the request to the target server and return the corresponding response object, or throw an exception if the execution is unsuccessful.
HttpClient encapsulates each HTTP method type in an object, and they’re available under the org.apache.http.client.methods package. In this script you’re going to use four types of requests: GET, POST, DELETE, and OPTIONS.11 The previous example implemented only GET, POST, and DELETE because the web container will automatically implement the OPTIONS method. HttpClient provides a default client and is good enough for our purpose. To execute an HTTP DELETE method you have to do the following:
- val httpDelete = new HttpDelete(url)
- val httpResponse = new DefaultHttpClient().execute(httpDelete)
- import org.apache.http._
- import org.apache.http.client.entity._
- import org.apache.http.client.methods._
- import org.apache.http.impl.client._
- import org.apache.http.client.utils._
- import org.apache.http.message._
- import org.apache.http.params._
Building the client, step by step (p49)
Now, because the service is up and running, you can focus on the client script. To make the script useful, you have to know the type of operation (GET or POST), request parameters, header parameters, and the URL to the service. The request parameters and header parameters are optional, but you need an operation and a URL to make any successful REST call:
- require(args.size >= 2,
- "at minimum you should specify action(post, get, delete, options) and url")
- val command = args.head
- val params = parseArgs(args)
- val url = args.last
The request parameters and header parameters are determined by a prefix parameter, –d or –h. One way to define a parseArgs method to parse request and header parameters is shown in the following listing.
- Listing 2.6 Parsing headers and parameters passed to the program
- def parseArgs(args: Array[String]): Map[String,List[String]] = {
- def nameValuePair(paramName: String) = {
- def values(commaSeparatedValues: String) = commaSeparatedValues.split(",").toList
- val index = args.findIndexOf(_ == paramName)
- (paramName, if(index == -1)) Nil else values(args(index + 1))
- }
- Map(nameValuePair("-d"), nameValuePair("-h"))
- }
The last interesting thing I’d like to mention about the parseArgs method is the Map. A Map is an immutable collection of keys and values. Chapter 4 discusses Map in detail. In this example you’re creating a Map of parameter name(-d or –h) and listing all the parameters as values. When you pass a tuple of two elements to Map, it takes the first element of the tuple as the key and the second element as the value:
For now you’ll support only four types of REST operations: POST, GET, DELETE, and OPTIONS, but I encourage you to implement other HTTP methods like PUT and HEAD. To check what type of operation is requested, you can use simple pattern matching:
- command match {
- case "post" => handlePostRequest
- case "get" => handleGetRequest
- case "delete" => handleDeleteRequest
- case "options" => handleOptionsRequest
- }
- Listing 2.7 Preparing a GET request and invoking the REST service
- def headers = for(nameValue <- params="" span="">"-h")) yield { ->
- def tokens = splitByEqual(nameValue)
- new BasicHeader(tokens(0), tokens(1))
- }
- def handleGetRequest = {
- val query = params("-d").mkString("&")
- val httpget = new HttpGet(s"${url}?${query}")
- headers.foreach { httpget.addHeader(_) }
- val responseBody =
- new DefaultHttpClient().execute(httpget,
- new BasicResponseHandler())
- println(responseBody)
- }
- Listing 2.8 Preparing a POST request and invoking the REST service
- def formEntity = {
- def toJavaList(scalaList: List[BasicNameValuePair]) = {
- java.util.Arrays.asList(scalaList.toArray:_*)
- }
- def formParams = for(nameValue <- params="" span="">"-d")) yield { ->
- def tokens = splitByEqual(nameValue)
- new BasicNameValuePair(String.valueOf(tokens(0)), String.valueOf(tokens(1)))
- }
- def formEntity =
- new UrlEncodedFormEntity(toJavaList(formParams), "UTF-8")
- formEntity
- }
- def handlePostRequest = {
- val httppost = new HttpPost(url)
- headers.foreach { httppost.addHeader(_) }
- httppost.setEntity(formEntity)
- val responseBody = new DefaultHttpClient().execute(httppost, new BasicResponseHandler())
- println(responseBody)
- }
The following listing contains the complete RestClient.scala script.
- Listing 2.9 RestClient.scala
- import org.apache.http._
- import org.apache.http.client.entity._
- import org.apache.http.client.methods._
- import org.apache.http.impl.client._
- import org.apache.http.client.utils._
- import org.apache.http.message._
- import org.apache.http.params._
- // (1) Validate the number of argument
- require(args.size >= 2, "at minimum you should specify action(post, get, delete, options) and url")
- val command = args.head
- val params = parseArgs(args)
- val url = args.last
- // (2) Patent match command argument
- command match {
- case "post" => handlePostRequest
- case "get" => handleGetRequest
- case "delete" => handleDeleteRequest
- case "options" => handleOptionsRequest
- }
- def splitByEqual(params:String): List[String]={
- params.split("=").toList
- }
- def parseArgs(args: Array[String]): Map[String,List[String]] = {
- def nameValuePair(paramName: String) = {
- def values(commaSeparatedValues: String) = commaSeparatedValues.split(",").toList
- val index = (args.toList).indexOf(paramName)
- (paramName, if(index == -1) Nil else values(args(index + 1)))
- }
- Map(nameValuePair("-d"), nameValuePair("-h"))
- }
- def headers = for(nameValue <- params="" span="">"-h")) yield { ->
- val tokens = splitByEqual(nameValue)
- new BasicHeader(tokens(0), tokens(1))
- }
- def handleGetRequest = {
- val query = params("-d").mkString("&")
- val httpget = new HttpGet(s"${url}?${query}")
- headers.foreach { httpget.addHeader(_) }
- val responseBody =
- new DefaultHttpClient().execute(httpget,
- new BasicResponseHandler())
- println(responseBody)
- }
- def formEntity = {
- def toJavaList(scalaList: List[BasicNameValuePair]) = {
- java.util.Arrays.asList(scalaList.toArray:_*)
- }
- def formParams = for(nameValue <- params="" span="">"-d")) yield { ->
- def tokens = splitByEqual(nameValue)
- new BasicNameValuePair(String.valueOf(tokens(0)), String.valueOf(tokens(1)))
- }
- def formEntity =
- new UrlEncodedFormEntity(toJavaList(formParams), "UTF-8")
- formEntity
- }
- def handlePostRequest = {
- val httppost = new HttpPost(url)
- headers.foreach { httppost.addHeader(_) }
- httppost.setEntity(formEntity)
- val responseBody = new DefaultHttpClient().execute(httppost, new BasicResponseHandler())
- println(responseBody)
- }
- def handleDeleteRequest = {
- val httpDelete = new HttpDelete(url)
- val httpResponse = new DefaultHttpClient().execute(httpDelete)
- println(httpResponse.getStatusLine())
- }
- def handleOptionsRequest = {
- val httpOptions = new HttpOptions(url)
- headers.foreach { httpOptions.addHeader(_) }
- val httpResponse = new DefaultHttpClient().execute(httpOptions)
- println(httpOptions.getAllowedMethods(httpResponse))
- }
To run the REST client you can use any build tool that can build Scala code. This example uses a build tool called simple build tool (SBT). You’ll learn about this tool in detail in chapter 6, but for now go ahead and install the tool following the instructions from the SBT wiki (http://www.scala-sbt.org). Take a look at the codebase for this chapter for an example.
Supplement
* [ Gossip in Java(2) ] 網路 : 程式實例 (簡單 HTTP 伺服器)
沒有留言:
張貼留言