Some of the most interesting opportunities for small mobile devices involve web services in one form or another. For instance, a bidder could use her mobile phone to keep tabs on an auction through the eBay SOAP API, or a commuter could browse his weblog subscriptions on a wireless PDA on the train ride to work. Of course, this requires the devices to support HTTP, and many do.
To download data from a web server, simply use the Connector or InputConnection class to open a regular HTTP URL:
InputConnection connection = (InputConnection) Connector.open( "http://www.google.com/search?q=xom");
The specific InputConnection returned is an HttpConnection:
public interface HttpConnection extends ContentConnection
It may also be an HttpsConnection, but thats a subinterface of HttpConnection that you normally use polymorphically as an instance of the superclass:
public interface HttpsConnection extends HttpConnection
In J2ME, HttpConnection fills in for several J2SE classes, including URL, URLConnection, and HttpURLConnection. Some of its methods are familiar from those classes, with occasionally subtle differences.
Of course, HttpConnection has the usual openInputStream( ), openOutputStream( ), openDataInputStream( ), openDataOutputStream( ), and close( ) methods common to any StreamConnection. It also has the getEncoding( ), getLength( ), and getType( ) methods of any ContentConnection. For basic uses such as downloading the latest sports scores or stock quotes, this is enough. However, more complex interactive applications will want to cast the Connection object returned by Connector.open( ) to HttpConnection so that they can use its additional methods. This is especially important if you want to send data back to the server via POST as well as simply GETting data from the server.
At any given time, a connection object is in one of three states:
When the object is first created, it is unconnected. At this point, you can call setRequestMethod( ) and setRequestProperty( ) to configure the HTTP header that is sent to the server.
There is no explicit connect( ) method. Instead, the connection is made and the header sent as soon as you invoke one of the methods that needs to read data from or send data to the server. These include obvious methods, such as openInputStream( ) and openOutputStream( ), as well as methods that read from the HTTP header, such as getHeaderField( ) and getLastModified( ).
Finally, the connection can be closed with the close( ) method. At this point, you can no longer read from the connection.
Which methods work depends on the connections state. For instance, you can use a method that sets a property in the HTTP request header after the connection has already been opened and the header sent. Nor can you reopen a closed connection. Calling the wrong method at the wrong time generally throws an IOException.
Every Connection object begins with a URL. However, the HttpConnection interface adds several methods that split that URL into its component parts. Generally speaking, URLs are composed of five pieces:
For example, in the URL http://www.example.com:8000/foo/bar/index.html? hl=en&q=test#p3, the scheme is http, the authority is www.example.com:8000, the path is /foo/bar/index.html. the fragment identifier is p3, and the query string is hl=en&q=test. The authority is often subdivided into a host and a port, and the port is often omitted.
However, not all URLs have all these pieces. For instance, the URL http://www.faqs.org/rfcs/rfc3986.html has a scheme, an authority, and a path but no fragment identifier and no query string.
Five public methods provide read-only access to these parts of a URL: getFile( ), getHost( ), getPort( ), getProtocol( ), geTRef( ), and getQuery( ).
The getProtocol( ) method returns a String containing the scheme of the URL. For example, this fragment sets the protocol variable to "http":
HttpInputConnection connection = (HttpInputConnection) Connector.open("http://www.google.com/"); String protocol = connection.getProtocol( );
In practice, this value is always "http" or "https", because no other URL scheme creates an HttpConnection object.
The getHost( ) method returns a String containing the hostname of the URL. For example, in this case, the host is www.google.com:
HttpInputConnection connection = (HttpInputConnection) Connector.open("http://www.google.com:80/"); String host = connection.getProtocol( );
The getPort( ) method returns the port number specified in the URL as an int. If no port was specified in the URL, getPort( ) returns 80 for HTTP and 443 for HTTPS.
The getFile( ) method returns a String that contains the path portion of a URL, not including the fragment identifier or query string. For example, here the path is /Top/News/:
HttpInputConnection connection = (HttpInputConnection) Connector.open("http://www.google.com/Top/News/"); String path = connection.getFile( );
If the URL does not have a path part, this method returns null.
The getref( ) method returns the fragment identifier. If the URL doesn have a fragment identifier, it returns null. In the following code, geTRef( ) returns the string aw2:
InputConnection connection = Connector.open( "http://www.google.com/search?hl=en&lr=&q=test#aw2"); String fragment = connection.getRef( );
The getQuery( ) method returns the query string. If the URL doesn have a query string, it returns null. In the following code, getQuery( ) returns the string hl=en&lr=&q=test:
InputConnection connection = Connector.open( "http://www.google.com/search?hl=en&lr=&q=test#aw2"); String query = connection.getQuery( ));
An HTTP request header precedes each request a browser sends to a server. For GET and HEAD requests, this is the only content. POST requests are followed by the body of the request. A typical GET request sent by HttpConnection looks like this:
GET /blog/feed HTTP/1.1 Host: www.elharo.com Content-length: 0
For simple GET requests, the default header HttpConnection sends is fine. However, to POST data to a web server, youll need to change the method using setRequestMethod( ):
public void setRequestMethod(String method) throws IOException
You then use the connections output stream to write the data.
|
The following code fragment connects to the service at http://www.example.org/cgi/postquery and submits the query string color=blue&n=7. This is written onto the body of the HTTP request:
HttpConnection connection = null; try { connection = (HttpConnection) Connector.open( "http://www.example.org/cgi/postquery"); connection.setRequestMethod("POST"); DataOutputStream out = connection.openDataOutputStream( ); out.writeUTF("color=blue&n=7"); out.flush( ); InputStream in = connection.openInputStream( ); // read and process the response... } catch (Exception ex) { // handle exception... } finally { try { if (connection != null) connection.close( ); } catch (IOException ex) { /* Oh well. We tried.*/ } }
Even if you e just using GET, you may need to modify the HTTP header to supply cookies, specify the languages the user prefers to read, or indicate how fresh a cached copy is. This is done with the setRequestProperty( ) method:
public void setRequestProperty(String key, String value) throws IOException public String getRequestProperty(String key)
For example, this request sets the Accept header to indicate that XML is preferred but HTML is accepted:
conn.setRequestProperty("Accept", "application/xml; text/xml; text/html");
There can be at most one header with any given key. Adding a second header with the same name changes the value rather than adding a new value. It is the clients responsibility to make sure that the strings passed here satisfy the requirements for HTTP headers (e.g., no line breaks in the name or value). The HttpConnection class does not check for illegal values.
HTTP servers provide a substantial amount of information in the header that precedes each response. For example, heres a typical HTTP header returned by an Apache web server:
HTTP/1.1 200 OK Date: Sun, 04 Dec 2005 16:15:16 GMT Server: Apache/2.0.55 (Unix) mod_ssl/2.0.55 OpenSSL/0.9.7d PHP/5.0.5 X-Powered-By: PHP/5.0.5 Last-Modified: Sat, 03 Dec 2005 21:32:30 GMT ETag: "f8dd0d8d4d24dc754b6a8aeab63ea0ac" X-Pingback: http://www.elharo.com/blog/xmlrpc.php Transfer-Encoding: chunked Content-Type: text/xml; charset=UTF-8
Theres a lot of information there. In general, an HTTP header may include the content type of the requested document, the length of the document in bytes, the character set in which the content is encoded, the current date and time, the date the content expires, the date the content was last modified, cookies, Etags, and more. However, the information depends on the server. Some servers send all this information for each request, others send only some information, and a few don send anything. The methods discussed in this section allow you to query an HttpConnection to find out what metadata the server provided.
The zeroth line of the response header is the status line. In this example, thats:
HTTP/1.1 200 OK
This consists of the HTTP version (HTTP/1.1), the response code (200), and the response message (OK). The geTResponseCode( ) and getresponseMessage( ) methods return the response code and the response message:
public int getResponseCode( ) throws IOException public String getResponseMessage( ) throws IOException
These methods throw an IOException if the connection to the server failed.
Codes between 200 and 299 indicate success, codes between 300 and 399 indicate redirection, codes between 400 and 499 indicate a client error, and codes between 500 and 599 indicate a server error. The HttpConnection class provides named constants for many of these codes, such as HttpConnection.HTTP_OK (200) and HttpConnection.HTTP_NOT_FOUND (404).
After the status line, the remainder of the header contains name/value pairs. The various getHeaderFieldKey( ) methods return the names of the fields, and the getHeaderField( ) methods return their values. You can iterate through these starting at zero:
public String getHeaderField(int n) throws IOException public String getHeaderFieldKey(int n) throws IOException
If you know the name of the field you e looking for, you can ask for it directly:
public String getHeaderField(String name) throws IOException
If no such field is present in the response header, this method returns null.
Some fields have obvious interpretations as integers (Content-length, Age) or dates (Retry-after, Last-modified). These two methods read the string value of the named field and convert it to the desired type:
public int getHeaderFieldInt(String name, int default) throws IOException public long getHeaderFieldDate(String name, long default) throws IOException
If the field is not present in the header or a conversion error occurs, these methods return the second argument instead.
Three convenience methods read particularly common and useful headers (the date the document was sent, the expiration date, and the last modified time):
public long getDate( ) throws IOException public long getExpiration( ) throws IOException public long getLastModified( ) throws IOException
Each returns a long measuring milliseconds since midnight, January 1, 1970. You can convert this value to a java.util.Date. For example:
Date documentSent = new Date(connection.getDate( ));
This is the time the document was sent as seen from the server. It often won match the time on the client. If the HTTP header does not include the corresponding field, these methods return 0.
Example 24-4 displays the complete headers from a user-specified URL. The startApp( ) method asks the user for a URL using a TextBox widget. Once the user enters one and activates the corresponding command, getInfo( ) spawns threads that connect to the server, download the headers, and display them. Networking, which may block, should not be done in the command thread. Doing so can deadlock the MIDlet (and in fact did in one of my tests before I added the separate thread).
import java.io.IOException; import javax.microedition.io.*; import javax.microedition.lcdui.*; import javax.microedition.midlet.*; public class HTTPInfo extends MIDlet implements CommandListener { private Display display; private TextBox textBox; private Form getInfo(String url) { Form form = new Form("HTTP Info"); HttpConnection connection = null; try { connection = (HttpConnection) Connector.open(url); connection.setRequestMethod("HEAD"); for (int i = 0; ; i++) { String key = connection.getHeaderFieldKey(i); String value = connection.getHeaderField(i); if (value == null) break; if (key != null) form.append(key + ": " + value + " "); else form.append("***" + value + " ");; } } catch (Exception ex) { form.append(ex.getMessage( ) +" "); } finally { try { if (connection != null) connection.close( ); } catch (IOException ex) { /* Oh well. we tried.*/ } } return form; } public void startApp( ) { display = Display.getDisplay(this); if (textBox == null) { textBox = new TextBox("URL", "http://", 255, TextField.URL); } display.setCurrent(textBox); Command getInfo = new Command("HTTP Headers", Command.OK, 10); textBox.addCommand(getInfo); textBox.setCommandListener(this); } public void commandAction(Command command, Displayable displayable) { Thread t = new Thread ( new Runnable( ) { public void run( ) { display.setCurrent(getInfo(textBox.getString( ))); } } ); t.start( ); } protected void pauseApp( ) {} protected void destroyApp(boolean unconditional) {} } |
Figure 24-4 shows the headers from http://www.google.com. The content type of the file at http://www.google.com is text/html. No content encoding was used, but the transfer encoding was chunked. A cookie that expires more than three decades in the future was fed to the phone. The server is Google Web Server 2.1. (Google uses its own custom web server to support its very high-volume site.)