
Ajax Patterns And Best Practices (2006)
.pdfC H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
249 |
public class WhoisOnline extends HttpServlet implements SingleThreadModel { private String _user;
private UserIdentificationResolver _userIdentification; private ArrayList _users = new ArrayList();
private int _version;
public void init(javax.servlet.ServletConfig config) throws javax.servlet.ServletException {
_version = 0; try {
_userIdentification = (UserIdentificationResolver)WhoisOnline.class.
getClassLoader().loadClass( config.getInitParameter("user-identification")).newInstance();
}
catch (Exception e) {
throw new ServletException(
"Could not instantiate _userIdentification", e);
}
}
protected void service( javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, java.io.IOException {
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
}
}
The global resource state for WhoisOnline is the data member _users, which is an array list instance of users who are online. The data member _user represents a transient state used to identify the user currently accessing the resource. The value of _user for the present implementation coincides with the identity of the authenticated user, but as will be illustrated in the following “Example: Server Push” section, it is not the rule. The data member _userIdentification represents an instance of the user authentication implementation based on the interfaces
250 |
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
defined before this code segment. And the last data member, _version, represents the version number of the state data member _users used to determine whether the reading stream should generate some content.
I have already explained the purpose of the method init, but to quickly summarize, it is used to initialize the Java servlet. In the case of WhoisOnline, the version number (_version) is reset to zero, and the user authentication implementation (_userIdentification) is instantiated. The technique used to instantiate the user authentication implementation relies on using the dynamic loading capabilities of Java.
What is completely new is the method service that is implemented as follows:
protected void service( javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException { UserIdentification userid =
_userIdentification.identifyUser(request); if(userid.isIdentified()) {
_user = userid.getIdentifier(); super.service(request, response);
}
else { response.setStatus(500,
"User could not be identified");
}
}
The methods doPost and doGet process HTTP POST and GET actions, respectively. But these methods are not called directly by the HTTP server. Instead, the HTTP server casts the servlet implementation for the Servlet interface. Having retrieved the interface, the method service is called, that in the default implementation will call the appropriate HTTP action method (for example, doPost and doGet). When the user-defined class WhoisOnline implements the method service, WhoisOnline is responsible for processing the individual HTTP action methods.
In the case of WhoisOnline, the purpose of the service method is not to override the default functionality, but to provide a single place where the global action extracting the user identification is placed. In the implementation, the method identifyUser is called and an instance of UserIdentification is retrieved. The instance of UserIdentification will contain the information about the user currently accessing the resource. If the user is identified (userid.isIdentified), the data member _user is assigned the user ID. With an identified user, the implementation can process an HTTP POST or GET. Calling the default service implementation by using the method super.service calls the default functionality that in turn calls doGet and doPost. If the user cannot be identified, the service method implementation will generate an HTTP 500 error code, thus not calling doGet or doPost because only authenticated users can use the presence detection global resource.
The implementation of doPost is as follows:
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
251 |
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException { Iterator iter = _users.iterator();
boolean didFind = false; while(iter.hasNext()) {
String user = (String)iter.next(); if(user.compareTo(_user) == 0) {
didFind = true; break;
}
}
if(!didFind) { _users.add(_user); _version ++;
}
response.setStatus(200, "All ok");
}
In the implementation of doPost, the sent data of the HTTP POST is not processed because our presence-detection example needs only to detect the user identity. In the implementation of doPost, what is processed is the user identity. If the user identification (_user) already exists in the list of present users (_users), nothing happens. If the user does not exist, the user is added to the users list, and the version (_version) number is incremented to indicate a change of state. Regardless of whether a user is added, the HTTP 200 code is returned, indicating a successful operation. It could be argued that if the user already exists in the users list, an error should be returned. However, that is not entirely appropriate; if a user is already added to the list, that does not indicate an error condition, but a condition of doing something repeatedly. And doing something repeatedly may be inefficient, but it is not wrong.
Following is the implementation of the doGet and getSentVersion methods:
private int getSentVersion(HttpServletRequest request) { Cookie[] cookies = request.getCookies();
if(cookies != null) {
for(int c1 = 0; c1 < cookies.length; c1 ++) { if(cookies[ c1].getName().compareTo("VersionId")
== 0) {
return new Integer(
cookies[ c1].getValue()).intValue();
}
}
}
return 0;
}
252 C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int sentVersion = getSentVersion(request); int waitCount = 0;
while(waitCount < 100) { if(sentVersion < _version) {
PrintWriter out = response.getWriter(); Iterator iter = _users.iterator(); while(iter.hasNext()) {
String user = (String)iter.next(); out.println("User (" + user + ")");
}
response.addCookie(new Cookie("VersionId", new Integer(_version).toString()));
return;
}
try { Thread.currentThread().sleep(1000);
}
catch (InterruptedException e) { } waitCount ++;
}
response.setStatus(408, "No change");
}
The doGet method implementation is similar to the doGet method implementation illustrated earlier, in the “Calling the ServerCommunicator Intelligently” section. The difference is that HTTP cookies are used to track the version number that the client has. The method getSentVersion extracts the version number from the client-sent cookies. If the version number does not exist, a value of 0 is returned. Then the server goes through the looping process of checking for a version number difference. If a version number difference is present, the output is generated, and the cookie VersionId is sent with the version number of generated content.
Example: Server Push
For each example, the level of complexity has increased, and the only remaining scenario to explain is the server push. What makes a server push unique is that each client accessing the global resource (for example, http://mydomain.com/global/resource) has a unique child URL (for example, http://mydomain.com/global/resource/unique-child). In previous examples, the URL used was a global resource that was shared among individual users. This time the URL must be unique because when the server pushes content, it is individualized.
As a side note, when referencing the unique URLs for the scope of this example, the unique identifier will always be a username or user identifier. That does not need to be the case; it could be a feed identifier, message queue, and so on. The unique identifier represents some type of unique resource that distinguishes itself from the other resources. It also does not mean that a single user is allocated a single resource. It could be that multiple users share the same unique resource.
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
253 |
Some Thoughts on Specifying URLs
Before continuing with the server push implementation, a bit more thought to specifying the URL has to be given. For the Permutations pattern, I illustrated how to generate and retrieve URLs. You saw techniques for associating a representation with a resource based on the needs of the client. When using the server push, the URL must be unique, and this can be a problem because we don’t know what the URL is in the first place. The question is how to figure out that the URL http://mydomain.com/global/resource/unique-child is unique based on the global resource http://mydomain.com/global/resource.
Using a Hard-Coded URL
A hard-coded URL is a URL that is written directly into the HTML, as illustrated by the following example:
<html>
<head>
<title>Hard Code Reference</title>
<script language="JavaScript" src="../lib/factory.js"></script> <script language="JavaScript" src="../lib/asynchronous.js"></script> <script language="JavaScript" type="text/javascript">
var asynchronous = new Asynchronous();
</script>
</head>
<body>
<button onclick="asynchronous.call('../chap04/chunked.ashx')"> Get Image</button>
<table>
<tr><td id="counter"></td></tr> </table>
</body>
</html>
In the example HTML code, the button calls the method asynchronous.call, and the called URL is ../chap04/chunked.ashx. In a traditional development, this would be called a hardcoded URL reference. Programmers tend not to like hard-coded URLs because they make it difficult to update a website if the URL changes. In the example, the hard-coded URL may not be optimum. The preferred URL would be ../chap04/chunked as the preferred URL implements the separation of resource from representation. The point, though, is to reference the design practices of the Permutations pattern.
Specifying a URL by Using User Identification
Another approach to specifying a URL is to use the server-side framework to generate the URL dynamically. In the following example, some ASP.NET code is used to generate the URL dynamically:
254 C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N
<%@ Page Language="C#" %> <script runat="server">
class DynamicURL {
public static string GetAsync() { return "/url";
}
}
</script>
<html>
<head>
<title>Hard Code Reference</title>
<script language="JavaScript" src="../lib/factory.js"></script> <script language="JavaScript" src="../lib/asynchronous.js"></script> <script language="JavaScript" type="text/javascript">
var asynchronous = new Asynchronous();
</script>
</head>
<body>
<button
onclick="asynchronous.call('<%=DynamicURL.GetAsync() %>')"> Get Image
</button>
<table>
<tr><td id="counter"></td></tr> </table>
</body>
</html>
In this modified example of the HTML code, there is code that is executed on the server side, and code that is executed on the client side. For those who code in PHP, JSP, or other similar technologies, you will know that what is executed on the server side is surrounded by escape tags. For ASP.NET, the escape tags usually are the <% and %> characters. Another way to run server-side code using ASP.NET is to use the script tag, where the runat attribute has a value of server.
What is of interest is the text DynamicURL.GetAsync, which is a method call issued on the server to generate a URL. In the implementation of the GetAsync method, a hard-coded /url is returned, but the implementation really represents a piece of dynamically generated code.
Generating the URL dynamically is not a real advantage because that is the purpose of the Permutations pattern. Where generating the URL does make sense is if the Content Chunking and Decoupled Navigation patterns are used. In those cases, there are scenarios where functionality is referenced that is orthogonal to the functionality of the HTML page contained in the URL. The orthogonal URL might be a dependency of some web application plug-in, and hence generating the URL gives some extra flexibility. In the case of the server push, the dynamically generated URL can be used to identify the specific URL.
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
255 |
Specifying a URL by Using HTTP Redirection
Looking at the preceding example, you can see that the class DynamicURL, when called, generates a single URL. As I have outlined, one use of the dynamically generated URL is to identify the unique server push URL. The URL is generated by using the early definition approach (the counterpart late definition approach will be illustrated shortly). The approach is called early definition because the unique URL is identified after the HTML content has been generated. Using such an approach is not always possible nor useful.
Imagine the scenario where e-mails are sent to ask users to update details. Generating the unique URLs at the time of creating the URLs would be a security risk. A better approach is to let the user log in and then be redirected to the specific URL. The same can be said for the Persistent Communications pattern. The solution is to use HTTP redirection that generates the unique URL at the last possible moment. HTTP redirection uses a late-definition approach.
Following is an example HTTP conversation that performs an HTTP redirection. As usual, a client makes an HTTP request:
GET /resource/ HTTP/1.1
Accept: */*
Accept-Language: en
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)
AppleWebKit/412.6.2 (KHTML, like Gecko) Safari/412.2.2
Connection: keep-alive
Host: 192.168.1.242:8100
The URL /resource is recognized by the HTTP server as a generic URL that when called will redirect to a specific URL. The HTTP server responds with an HTTP 302 to indicate a redirection, as illustrated by the following HTTP response:
HTTP/1.1 302 Found
Date: Mon, 05 Sep 2005 16:29:04 GMT
Server: Apache/2.0.53 (Ubuntu) PHP/4.3.10-10ubuntu4
Location: /resource/joesmith
Content-Length: 346
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1
In the example, the specific URL is defined as /resource/joesmith that is sent to the client. When either a web browser or XMLHttpRequest object receives a redirect, the client will recognize the redirect and attempt to retrieve the contents of the redirected URL, as illustrated by the following final request:
GET /resource/joesmith HTTP/1.1
Accept: */*
Accept-Language: en
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)
AppleWebKit/412.6.2 (KHTML, like Gecko) Safari/412.2.2
Connection: keep-alive
Host: 192.168.1.242:8100
256 |
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
An HTTP redirection, whether executed by the web browser or XMLHttpRequest, can be executed only if the redirection follows the same origin policy. If a redirection to another domain is attempted with XMLHttpRequest, the results vary. For example, Microsoft Internet Explorer returns a status code of zero and no further data, Mozilla-based browsers return the status code 302 and the redirected URL, and finally Apple Safari crashes. Note that at the time of this writing, the Safari bug has been filed.
Completing the ServerCommunicator
For the server push implementation, the conversion of the general URL to the specific URL will be implemented by using HTTP redirection. The server push implementation is a wrapping together of all the concepts that the Persistent Communications pattern offers. There is a shared resource, a specific resource, user authentication, and version number tracking. The actions that the ServerCommunicator implements to make a server push happen are as follows:
1.The client accesses the root resource URL (for example, /ajax/chap06/serverpush).
2.The server reads the URL and checks whether there is a user identifier indicating a specific URL.
3.If no user identifier exists, a generic URL has been called that needs to be converted into a specific URL. The conversion involves the reading of the client authentication information that is used to execute a redirection to a specific URL (for example, /ajax/ chap06/serverpush/username).
4.If there is a user identifier, the URL is not redirected but processed.
5.If having reached this step the URL is specific and therefore contains a user identifier, the user identifier is extracted from the URL and cross-referenced with a user state.
6.If a user state is not found, an HTTP 500 error is generated.
7.If a user state is found, the cookie associated with the user is retrieved and the version number is extracted.
8.Based on the user state and version number, the server either generates new data or waits for new data to be generated.
In the list of actions are some new actions and some already discussed actions. What is new is the direct reference of the user URL or specific URL. There is an important item to note, in that a redirection is not automatic and will happen only if a user references the root URL. This is done on purpose because it allows a client to access a URL that might not be related to its authentication information. So, for example, if an administrator authenticates herself, then by explicitly referencing a user, the administrator can administer the details of a user. It goes without saying that by implementing the Permutations pattern, an administrator could send a MIME type to the root resource that stops a redirection and instead returns a directory listing of all users. The idea is to allow a certain amount of flexibility by the HTTP server when performing an HTTP redirection based on the user identity and sent HTTP.
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
257 |
For each of the specific URLs, there is an associated user state. The user state could be some data in a database, file, or anywhere else. The user state is a depiction of the data that the client is interested in. In the case of our example, the user state is defined as follows:
class UserState {
private String _userIdentifier; private Object _state;
private int _version;
public UserState(String userIdentifier, Object state) { _userIdentifier = userIdentifier;
_state = state;
}
public String getUserIdentifier() { return _userIdentifier;
}
public Object getState() { return _state;
}
public void setState(Object state) { _state = state;
_version ++;
}
public int getVersion() { return _version;
}
}
The UserState class has three properties: _userIdentifier, which uniquely identifies the user; _state, which references some object instance representing the state of the user; and _version, which represents the version number of the state. The properties _userIdentifier and _version are read-only because you do not want a consumer class to manipulate either of the properties. The property _userIdentified when assigned never changes, whereas _version is incremented every time setState method is called.
The class ServerPush is responsible for the server push ServerCommunicator implementation. The abbreviated implementation of ServerPush that builds on the presence detection implementation is as follows; the new pieces of functionality are bolded for easy reference:
public class ServerPush extends HttpServlet implements SingleThreadModel { private ArrayList _users = new ArrayList();
private String _user;
private String _baseDirectory;
private UserIdentificationResolver _userIdentification;
258 |
C H A P T E R 8 ■ P E R S I S T E N T C O M M U N I C A T I O N S P A T T E R N |
public void init(javax.servlet.ServletConfig config) throws javax.servlet.ServletException {
_baseDirectory = config.getInitParameter("base-url"); try {
_userIdentification = (UserIdentificationResolver)ServerPush.class. getClassLoader().loadClass( config.getInitParameter("user-identification")).newInstance();
}
catch (Exception e) {
throw new ServletException(
"Could not instantiate _userIdentification", e);
}
}
protected void service( javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, java.io.IOException {
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws javax.servlet.ServletException, java.io.IOException {
// Do something with the URL
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
}
}
Looking at the code, you can see that scattered throughout are pieces of bolded new functionality. With respect to data members, the array list _users is newly added. The array list specifies the state of each individual user. In a real-life application, this is not what you would probably do because there could be literally thousands of users. In real-life, though, you would associate the user identifier with a key. The user identifier is the trailing end of the resource URL, and need not be a username but could be an alphanumeric number or sequence of characters.
The other new data member is _baseDirectory, which represents the root resource URL.
A root resource definition URL is needed because the servlet has to be able to distinguish between a generic URL (/ajax/chap06/serverpush) and a specific URL (/ajax/chap06/serverpush/ username). The data member _baseDirectory is assigned in the init method implementation and represents a reference point for all URLs that will be processed by ServerPush.
The modified service method is implemented as follows: