程式扎記: [Scala IA] The Basics : Ch2. Getting Started - Command-line REST client

標籤

2016年7月18日 星期一

[Scala IA] The Basics : Ch2. Getting Started - Command-line REST client

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: 
  1. val httpDelete = new HttpDelete(url)  
  2. val httpResponse = new DefaultHttpClient().execute(httpDelete)  
The HTTP POST method is a little different because, according to the HTTP specification, it’s one of the two entity-enclosing methods. The other one is PUT. To work with entities HttpClient provides multiple options, but in the example you’re going to use the URL-encoded form entity. It’s similar to what happens when you POST a form submission. Now you can dive into building the client. To use the HttpClient in your script, you have to import all the necessary classes. I haven’t talked about import, but for now think of it as similar to Java import except that Scala uses _ for importing all classes in a package, as in the following: 
  1. import org.apache.http._  
  2. import org.apache.http.client.entity._  
  3. import org.apache.http.client.methods._  
  4. import org.apache.http.impl.client._  
  5. import org.apache.http.client.utils._  
  6. import org.apache.http.message._  
  7. import org.apache.http.params._  
You can do other interesting things with Scala imports, but that’s in the next chapter. 

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
  1. require(args.size >= 2,  
  2. "at minimum you should specify action(post, get, delete, options) and url")  
  3. val command = args.head  
  4. val params = parseArgs(args)  
  5. val url = args.last  
You’re using a require function defined in Predef to check the size of the input. Remember that the command-line inputs are represented by an args array. The require function throws an exception when the predicate evaluates to false. In this case, because you expect at least two parameters, anything less than that will result in an exception. The first parameter to the script is the command, and the next ones are the request and header parameters. The last parameter is the URL. The input to the script will look something like the following: 
post -d <comma separated name value pair> -h <comma separated name value pair> <url>

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 
  1. def parseArgs(args: Array[String]): Map[String,List[String]] = {  
  2.     def nameValuePair(paramName: String) = {  
  3.         def values(commaSeparatedValues: String) = commaSeparatedValues.split(",").toList  
  4.         val index = args.findIndexOf(_ == paramName)  
  5.         (paramName, if(index == -1)) Nil else values(args(index + 1))  
  6.     }  
  7.     Map(nameValuePair("-d"), nameValuePair("-h"))  
  8. }  
This listing has defined a function inside another function. Scala allows nested functions, and nested functions can access variables defined in the outer scope function— in this case, the args parameter of the parseArgs function. Nested functions allow you to encapsulate smaller methods and break computation in interesting ways. Here the nested function nameValuePair takes the parameter name, –d or –h, and creates a list of name-value pairs of request or header parameters. The next interesting thing about the nameValuePair function is the return type. The return type is a scala.Tuple2, a tuple of two elements. Tuple is immutable like List, but unlike List it can contain different types of elements; in this case, it contains a String and a List. Scala provides syntax sugar for creating a Tuple by wrapping elements with parentheses ()
scala> val tuple2 = ("list of one element", List(1))
tuple2: (String, List[Int]) = (list of one element,List(1))

scala> val tuple2 = new scala.Tuple2("list of one element", List(1))
tuple2: (String, List[Int]) = (list of one element,List(1))

scala> val tuple3 = (1, "one", List(1)) // Here’s how to create a tuple of three elements:
tuple3: (Int, String, List[Int]) = (1,one,List(1))

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: 
scala> val m = Map(("key1", "value1"), ("key2", "value2"))
m: scala.collection.immutable.Map[String,String] = Map(key1 -> value1, key2 -> value2)

scala> m("key1")
res0: String = value1

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: 
  1. command match {  
  2.     case "post" => handlePostRequest  
  3.     case "get" => handleGetRequest  
  4.     case "delete" => handleDeleteRequest  
  5.     case "options" => handleOptionsRequest  
  6. }  
Here handlePostRequesthandleGetRequesthandleDeleteRequest, and handleOptionRequest are functions defined in the script. Each needs to be implemented a little differently. For example, in the case of a GET call, you’ll pass the request parameters as query parameters to the URL. POST will use a URL-encoded form entity to pass the request parameters. DELETE and OPTIONS won’t use any request parameters. Look at the handleGetRequest method, shown in the following listing. 
- Listing 2.7 Preparing a GET request and invoking the REST service 
  1. def headers = for(nameValue <- params="" span="">"-h")) yield {  
  2.     def tokens = splitByEqual(nameValue)  
  3.     new BasicHeader(tokens(0), tokens(1))  
  4. }  
  5.   
  6. def handleGetRequest = {  
  7.     val query = params("-d").mkString("&")  
  8.     val httpget = new HttpGet(s"${url}?${query}")  
  9.     headers.foreach { httpget.addHeader(_) }  
  10.     val responseBody =  
  11.     new DefaultHttpClient().execute(httpget,  
  12.     new BasicResponseHandler())  
  13.     println(responseBody)  
  14. }  
In this method you’re retrieving all the request parameters from the Map params and creating the query string. Then you create the HttpGet method with the given URL and query string. The DefaultHttpClient is executing the httpgetrequest and giving the response. The handlePostRequest method is a little more involved because it needs to create a form entity object, as shown in the following listing. 
- Listing 2.8 Preparing a POST request and invoking the REST service 
  1. def formEntity = {  
  2.   def toJavaList(scalaList: List[BasicNameValuePair]) = {  
  3.     java.util.Arrays.asList(scalaList.toArray:_*)  
  4.   }  
  5.   def formParams = for(nameValue <- params="" span="">"-d")) yield {  
  6.   def tokens = splitByEqual(nameValue)  
  7.   new BasicNameValuePair(String.valueOf(tokens(0)), String.valueOf(tokens(1)))  
  8. }  
  9. def formEntity =  
  10.   new UrlEncodedFormEntity(toJavaList(formParams), "UTF-8")  
  11.   formEntity  
  12. }  
  13. def handlePostRequest = {  
  14.   val httppost = new HttpPost(url)  
  15.   headers.foreach { httppost.addHeader(_) }  
  16.   httppost.setEntity(formEntity)  
  17.   val responseBody = new DefaultHttpClient().execute(httppost, new BasicResponseHandler())  
  18.   println(responseBody)  
  19. }  
Something interesting and unusual is going on here. First is the toJavaList method. The Scala List and the Java List are two different types of objects and aren’t directly compatible with each other. Because HttpClient is a Java library, you have to convert it to a Java type collection before calling the UrlEncodedFormEntity. The special :_* tells the Scala compiler to send the result of toArray as a variable argument to the Arrays.asList method; otherwise, asList will create a Java List with one element. The following example demonstrates that fact: 
scala> val scalaList = List(1, 2, 3)
scalaList: List[Int] = List(1, 2, 3)

scala> val javaList = java.util.Arrays.asList(scalaList.toArray)
javaList: java.util.List[Array[Int]] = [[I@54ba2e1f]

scala> val javaList = java.util.Arrays.asList(scalaList.toArray:_*)
javaList: java.util.List[Int] = [1, 2, 3]

The following listing contains the complete RestClient.scala script. 
- Listing 2.9 RestClient.scala 
  1. import org.apache.http._  
  2. import org.apache.http.client.entity._  
  3. import org.apache.http.client.methods._  
  4. import org.apache.http.impl.client._  
  5. import org.apache.http.client.utils._  
  6. import org.apache.http.message._  
  7. import org.apache.http.params._  
  8.   
  9. // (1) Validate the number of argument  
  10. require(args.size >= 2"at minimum you should specify action(post, get, delete, options) and url")  
  11. val command = args.head  
  12. val params = parseArgs(args)  
  13. val url = args.last  
  14.   
  15. // (2) Patent match command argument  
  16. command match {    
  17.     case "post" => handlePostRequest    
  18.     case "get" => handleGetRequest    
  19.     case "delete" => handleDeleteRequest    
  20.     case "options" => handleOptionsRequest    
  21. }    
  22.   
  23. def splitByEqual(params:String): List[String]={  
  24.   params.split("=").toList  
  25. }  
  26.   
  27. def parseArgs(args: Array[String]): Map[String,List[String]] = {  
  28.     def nameValuePair(paramName: String) = {  
  29.         def values(commaSeparatedValues: String) = commaSeparatedValues.split(",").toList  
  30.         val index = (args.toList).indexOf(paramName)  
  31.         (paramName, if(index == -1) Nil else values(args(index + 1)))  
  32.     }  
  33.     Map(nameValuePair("-d"), nameValuePair("-h"))  
  34. }  
  35.   
  36.   
  37. def headers = for(nameValue <- params="" span="">"-h")) yield {  
  38.     val tokens = splitByEqual(nameValue)  
  39.     new BasicHeader(tokens(0), tokens(1))  
  40. }  
  41.   
  42. def handleGetRequest = {  
  43.     val query = params("-d").mkString("&")  
  44.     val httpget = new HttpGet(s"${url}?${query}")  
  45.     headers.foreach { httpget.addHeader(_) }  
  46.     val responseBody =  
  47.     new DefaultHttpClient().execute(httpget,  
  48.     new BasicResponseHandler())  
  49.     println(responseBody)  
  50. }  
  51.   
  52. def formEntity = {  
  53.   def toJavaList(scalaList: List[BasicNameValuePair]) = {  
  54.     java.util.Arrays.asList(scalaList.toArray:_*)  
  55.   }  
  56.   def formParams = for(nameValue <- params="" span="">"-d")) yield {  
  57.   def tokens = splitByEqual(nameValue)  
  58.   new BasicNameValuePair(String.valueOf(tokens(0)), String.valueOf(tokens(1)))  
  59. }  
  60. def formEntity =  
  61.   new UrlEncodedFormEntity(toJavaList(formParams), "UTF-8")  
  62.   formEntity  
  63. }  
  64. def handlePostRequest = {  
  65.   val httppost = new HttpPost(url)  
  66.   headers.foreach { httppost.addHeader(_) }  
  67.   httppost.setEntity(formEntity)  
  68.   val responseBody = new DefaultHttpClient().execute(httppost, new BasicResponseHandler())  
  69.   println(responseBody)  
  70. }  
  71.   
  72. def handleDeleteRequest = {  
  73.   val httpDelete = new HttpDelete(url)  
  74.   val httpResponse = new DefaultHttpClient().execute(httpDelete)  
  75.   println(httpResponse.getStatusLine())  
  76. }  
  77. def handleOptionsRequest = {  
  78.   val httpOptions = new HttpOptions(url)  
  79.   headers.foreach { httpOptions.addHeader(_) }  
  80.   val httpResponse = new DefaultHttpClient().execute(httpOptions)  
  81.   println(httpOptions.getAllowedMethods(httpResponse))  
  82. }  
In this complete example you implemented the support for four types of HTTP requests: POSTGETDELETE, and OPTIONS. The require function call (1) ensures that your script is invoked with at least two parameters: the action type and the URL of the REST service. The pattern-matching block at the end of the script (2) selects the appropriate action handler for a given action name. The parseArgs function handles the additional arguments provided to the script, such as request parameters or headers, and returns a Map containing all the name-value pairs. The formEntity function is interesting because the URL encodes the request parameters when the http request type is POST, because in POSTrequest parameters are sent as part of the request body and they need to be encoded. 

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 伺服器)

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!