Unit 4. Developing and Accessing Services

4.6. Basic service access from Java

Nowadays, most applications rely on web services to access remote and centralized data stored in a remote server through the World Wide Web. A web service is a technology which uses a series of standards in order to communicate an application running in the server (using any technology like PHP, Node, Ruby, Java, .NET, …) with a client application that can be running in any device and be written also in any language. In this part we’ll mainly focus in accessing REST web services using Java as a client, relying on a server side implemented in Node.js from previous sections.

4.6.1. Opening and reading an HTTP connection

There are many ways to connect to a web via the HTTP protocol in Java. For instance, we can use the native classes derived from URLConnection, or an external library that simplifies the job.

Because one of the main uses of Java is for Android development, we’ll look at what’s recommended there. There’s a library called Apache Http Client, that was included in the Android libraries but it’s not supported anymore (and even if you still can use it, it’s not recommended). The recommended option is to use URLConnection class and its derivatives like HttpURLConnection and HttpsURLConnection.

4.6.1.1. Using URLConnection

This is the most low-level connection method, meaning that the programmer will be controlling every aspect of the connection but in contrast the resulting code will be larger and uglier. It’s recommended in Android because, if used properly, it’s the faster and the least memory, processor (and battery) consuming method.

The URL class is used to represent the remote resource in the World Wide Web we’ll be accessing.

1URL google = new URL("http://www.google.es");

This object will return a URLConnection object when we connect to it.

1URLConnection conn = google.openConnection();

To get the response body (content), the connection provides an InputStream for that purpose. It’s also recommended to retrieve the charset encoding from the response headers, in order to read everything (like accents) properly. We can use this static method for this purpose:

1// Get charset encoding (UTF-8, ISO,...)
2public static String getCharset(String contentType) {
3    for (String param : contentType.replace(" ", "").split(";")) {
4        if (param.startsWith("charset=")) {
5            return param.split("=", 2)[1];
6        }
7    }
8    return null; // Probably binary content
9}

This can be a basic way of connecting to a URL and gettint its contents:

 1public static void main(String[] args) {
 2    BufferedReader bufInput = null;
 3    try {
 4        URL google = new URL("http://www.google.es");
 5        URLConnection conn = google.openConnection();
 6        
 7        String charset = getCharset(conn.getHeaderField("Content-Type"));
 8        
 9        bufInput = new BufferedReader(
10                new InputStreamReader(conn.getInputStream(), charset));
11        
12        String line;
13        while((line = bufInput.readLine()) != null) {
14            System.out.println(line);
15        }
16    } catch (MalformedURLException e) {
17        ...
18    } catch (IOException e) { 
19        ...
20    } finally {
21        if(bufInput != null) {
22            try {
23                bufInput.close();
24            } catch (IOException e) {...}
25        }
26    }
27}

4.6.1.2. HttpURLConnection and following redirections

The class HttpURLConnection provides additional methods like following redirections automatically or getting the response code (such as 404 for “Not Found”). To get an HttpURLConnection and follow redirections automatically you should call this method:

1URL sanvi = new URL("http://iessanvicente.com");
2HttpURLConnection conn = (HttpURLConnection)sanvi.openConnection();
3conn.setInstanceFollowRedirects(true);

It doesn’t always work. For example, it may return code 301 (Moved Permanently) and thus you will not be redirected to the new location automatically. You can check what the response (and its headers) is by using available methods:

1System.out.println(conn.getResponseCode());
2System.out.println(conn.getResponseMessage());
3System.out.println(conn.getHeaderFields());

With this information, we could manage manually these redirections (with the risk of falling into a redirection loop), even if there are many, like this:

1URL url = new URL("http://iessanvicente.com");
2HttpURLConnection conn;
3do {
4    conn = (HttpURLConnection)url.openConnection();
5    if(conn.getResponseCode() == 301) {
6        url = new URL(conn.getHeaderField("Location"));
7    }
8} while(conn.getResponseCode() == 301);

Exercise 8

Create a console Java application named LinkSearch that will ask you for an address and print all the links (<a>) detected in the response. If you want, it’s a good idea to create an auxiliary class that extends from BufferedReader as we saw on Unit 1, to only filter those links from the output. This is part of the output that https://iessanvicente.com should show:

Exercise 8 Exercise 8

4.6.2. Basics of web service access

To access a REST web service we need a URL (which represents a resource being accessed), and an operation (GET, POST, PUT, DELETE) to do with that resource, along with additional data needed for the operation.

The simplest operation is GET, that is usually used for searching and getting information about something that already exists. In a GET operation, data (if necessary) is sent in the URL in two possible ways:

  • http://domain/resource?data1=value1&data2=value2.
  • http://domain/resource/value1/value2

This is a really basic Express service that will read two numbers passed in the url (GET) and will print (response) the result of that sum:

1app.get('/sum/:n1/:n2', (req, res) => {
2    let result = parseInt(req.params.n1) + parseInt(req.params.n2)
3    res.send("" + result);
4});

And this is how we can call it from Java and obtain the result:

 1private static String getSumFromService(int n1, int n2) {
 2    BufferedReader bufInput = null;
 3    String result;
 4    try {
 5        URL google = new URL("http://localhost/services/sum/" 
 6            + n1 + "/" + n2);
 7        URLConnection conn = google.openConnection();
 8        
 9        bufInput = new BufferedReader(
10                       new InputStreamReader(conn.getInputStream()));
11        result = bufInput.readLine();
12    } catch (IOException e) {
13        return "Error";
14    } finally {
15        if(bufInput != null) {
16            try {
17                bufInput.close();
18            } catch (IOException e) { return "Error"; }
19        }
20    }
21    
22    return result == null?"Error":result;
23}
24
25public static void main(String[] args) {
26    System.out.println(getSumFromService(3, 5));
27}

4.6.2.1. ServiceUtils class

In order to wrap all the code needed to connect to a web service and send/receive information to/from it, we are going to create our own class. We call it ServiceUtils, and its code is:

 1public class ServiceUtils {
 2
 3    private static String token = null;
 4
 5    public static void setToken(String token) {
 6        ServiceUtils.token = token;
 7    }
 8
 9    public static void removeToken() {
10        ServiceUtils.token = null;
11    }
12
13    public static String getCharset(String contentType) {
14        for (String param : contentType.replace(" ", "").split(";")) {
15            if (param.startsWith("charset=")) {
16                return param.split("=", 2)[1];
17            }
18        }
19
20        return null; // Probably binary content
21    }
22
23    public static String getResponse(String url, String data, 
24        String method) {
25
26        BufferedReader bufInput = null;
27        StringJoiner result = new StringJoiner("\n");
28        try {
29            URL urlConn = new URL(url);
30            HttpURLConnection conn = 
31                (HttpURLConnection) urlConn.openConnection();
32            conn.setReadTimeout(20000 /*milliseconds*/);
33            conn.setConnectTimeout(15000 /* milliseconds */);
34            conn.setRequestMethod(method);
35
36            conn.setRequestProperty("Host", "localhost");
37            conn.setRequestProperty("Connection", "keep-alive");
38            conn.setRequestProperty("Accept", "application/json");
39            conn.setRequestProperty("Origin", "http://localhost");
40            conn.setRequestProperty("Accept-Encoding", 
41                                    "gzip,deflate,sdch");
42            conn.setRequestProperty("Accept-Language", "es-ES,es;q=0.8");
43            conn.setRequestProperty("Accept-Charset", "UTF-8"); 
44            conn.setRequestProperty("User-Agent", "Java");
45
46            // If set, send the authentication token
47            if(token != null) {
48                conn.setRequestProperty("Authorization", 
49                                        "Bearer " + token);
50            }
51
52            if (data != null) {
53                conn.setRequestProperty("Content-Type", 
54                    "application/json; charset=UTF-8");
55                conn.setRequestProperty("Content-Length", 
56                    Integer.toString(data.length()));
57                conn.setDoOutput(true);
58                //Send request
59                DataOutputStream wr = 
60                    new DataOutputStream(conn.getOutputStream());
61                wr.write(data.getBytes());
62                wr.flush();
63                wr.close();
64            }
65
66            String charset = getCharset(
67                                conn.getHeaderField("Content-Type"));
68
69            if (charset != null) {
70                InputStream input = conn.getInputStream();
71                if ("gzip".equals(conn.getContentEncoding())) {
72                    input = new GZIPInputStream(input);
73                }
74
75                bufInput = new BufferedReader(
76                                    new InputStreamReader(input));
77
78                String line;
79                while((line = bufInput.readLine()) != null) {
80                    result.add(line);
81                }
82            }
83        } catch (IOException e) {
84        } finally {
85            if (bufInput != null) {
86                try {
87                    bufInput.close();
88                } catch (IOException e) { }
89            }
90        }
91
92        return result.toString();
93    }
94}

As you can see, we have the getCharset method explained before to get the charset encoding for the communication. The getResponse method will be used to send a request to a web service. It has 3 parameters: the url to connect, the data to send in the body of the request (or null if there’s no data), and the operation or method (GET, POST, PUT, DELETE). This response will be stored in a String that will be returned from this static method.

Also, some HTTP headers have been established so that the request is similar to what a web browser would send, like the origin domain (in this case localhost), the preferred language (Spanish), the possibility to compress data (gzip) in order to save bandwidth, a time out for the connection, or the data type used for communication (application/json).

There are also some other methods and attributes to deal with tokens for client authentication, so that we can store the token provided by the server in a static variable and send it back to the server in every request. But we are not going to use them for now.

You will be provided with this class in the Virtual Classroom, so that you can use it in the exercises to help you connect and get data from the web services more quickly.

4.6.3. JSON processing

Nowadays, most web services send and receive information in JSON format (XML is almost abandoned for this use). This format is native of JavaScript but most languages like Java have the necessary tools to process it.

The basic information about JSON and the available tools for each language can be found at http://www.json.org/. To process this information we can use the org.json API (also present in Android) or other options like Google’s GSON, but there are a lot of options. We will see here how to use GSON library.

4.6.3.1. Using GSON

Let’s see an example of how to use GSON library. First of all, we need to add that library to our Java project. You can download the latest version of the JAR file in the Maven repository, or the version that you will find in the Virtual Classroom. You must add the JAR file as a global or local library to your IntelliJ project, as you did with JavaFX in previous units.

Imagine that we receive this information from a web service in JSON format:

 1{
 2	"error":false,
 3	"person": {
 4		"name":"Peter",
 5		"age":30,
 6		"address":[
 7			{"city":"London","street":"Some street 24"},
 8			{"city":"New York","street":"Other street 12"}
 9		]
10	}
11}

GSON will try to automatically convert from JSON to a native Java object. So it will need a class that contains the same fields as the JSON response (same name). There’s no need to create any specific constructor. For the example above, we need a class called Address with two attributes called city (String) and street (String), and another class called Person with the attributes name (String), age (int) and address (of type Address).

Now we need an additional class that maps the initial JSON response format:

 1public class GetPersonResponse {
 2    boolean error;
 3    Person person;
 4    
 5    public boolean getError() {
 6        return error;
 7    }
 8    
 9    public Person getPerson() {
10        return person;
11    }
12}

If the field names are correctly set, it will map everything automatically:

 1public static void main(String[] args) {
 2
 3    String json = 
 4        ServiceUtils.getResponse("http://localhost/services/example", 
 5            null, "GET");
 6
 7    if(json != null) {
 8        Gson gson = new Gson();
 9        GetPersonResponse personResp = gson.fromJson(json, 
10            GetPersonResponse.class);
11        if(!personResp.getError()) {
12            System.out.println(personResp.getPerson().toString());
13            System.out.println(personResp.getPerson().getClass());
14        } else {
15            System.out.println("There was an error in the request");
16        }
17    }
18}

For this example, we would need to override the toString method in both classes Person and Address to show their data in an appropriate format. Note how we use the ServiceUtils class explained above to get a reponse, and then use GSON library to parse the response and store the corresponding data in the appropriate objects, according to GetPersonResponse class.

If a class property’s name doesn’t match the JSON field name, we can tell the GSON parser that it has to assign that field by using an annotation with that property:

1@SerializedName("error")
2boolean haserror; // In JSON it will be named "error"

You can learn more about GSON library in this GSON tutorial.

Exercise 9

Create a Java project called JsonParsing. Add the GSON library on it, and then implement a program (a console application, not a JavaFX one) that uses previous code (ServiceUtils, Person, Address and GetPersonResponse classes, apart from the main application) to connect to a server and retrieve a person information.

You can use the Node server provided to you in this session’s resources, and access the localhost/services/example URL to get the JSON data back. You can also edit the code of this server to change the URI or the port, if you want to.

4.6.4. Accessing web services from a different thread

Connecting to a web service and getting a response can be a costly operation, specially if the Internet connection is not the best and/or the server is overloaded. If we access a web service in the main thread, the application will be blocked (unresponsive) until we get the result.

The best way to deal with web services (or any other remote connection) is by using a separate thread to start the connection and then process the results when they’re received. If processing those results implies changing the view in a JavaFX application, we can use a Service or the Platform.runLater() method, like in this example that calls the sum service example shown in previous sections.

 1public class GetSumService extends Service<Integer> {
 2
 3    int n1, n2;
 4
 5    public GetSumService(int n1, int n2) {
 6        super();
 7        this.n1 = n1;
 8        this.n2 = n2;
 9    }
10
11    @Override
12    protected Task<Integer> createTask() {
13        return new Task<Integer>() {
14            @Override
15            protected Integer call() throws Exception {
16                BufferedReader bufInput = null;
17                Integer result = 0;
18                try {
19                    URL url = new URL("http://localhost/services/sum/" + 
20                        n1 + "/" + n2);
21                    URLConnection conn = url.openConnection();
22                    
23                    bufInput = new BufferedReader(
24                    new InputStreamReader(conn.getInputStream()));
25                    result = Integer.parseInt(bufInput.readLine()); 
26                } catch (IOException e) {} finally {
27                    if(bufInput != null) {
28                        try {
29                            bufInput.close();
30                        } catch (IOException e) {}
31                    }
32                }
33                
34                Thread.sleep(5000); // simulate a 5 seconds delay!
35                return result;
36            }
37        };
38    }
39    
40}

If this is the view…

WS WS

In the application’s controller, when we click “Add” button, we’ll create and start the service, and update the corresponding label when it’s finished:

 1private void sumNumbers(ActionEvent event) {
 2    gss = new GetSumService(
 3                  Integer.parseInt(num1.getText()), 
 4                  Integer.parseInt(num2.getText()));
 5    gss.start();
 6    addButton.setDisable(true);
 7    resultLabel.setVisible(false);
 8        
 9    gss.setOnSucceeded(e -> {
10        resultLabel.setText("Result: " + gss.getValue());
11        addButton.setDisable(false);
12        resultLabel.setVisible(true);
13    });
14}

Exercise 10

Create a JavaFX project called FXWebServiceExample and create a JavaFX application similar to the one shown above. Use the GetSumService class to access the sum service and retrieve the sum of the two digits sent as parameters.

As in previous exercise, you can use the Node services provided to you in this session. In this case, you should access localhost/services/sum with the two parameters needed (for instance, localhost/services/sum/5/2 should return 7 as a result). You can also change the URI or port in the Node project, if you want to.