Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Ajax Patterns And Best Practices (2006)

.pdf
Скачиваний:
39
Добавлен:
17.08.2013
Размер:
15.94 Mб
Скачать

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

99

function CacheProxy() {

}

function CacheProxy_get(url) { CacheController.getURL(url, this, false);

}

function CacheProxy_post(url, mimetype, datalength, data) { var thisreference = this;

asynchronous = new Asynchronous();

asynchronous.openCallback = function(xmlhttp) { thisreference.openCallback(xmlhttp);

}

asynchronous.complete = function(status, statusText, responseText, responseXML) { thisreference.complete(status, statusText,

responseText, responseXML);

}

asynchronous.post(url, mimetype, datalength, data);

}

CacheProxy.prototype.openCallback = CacheProxy_openCallback; CacheProxy.prototype.complete = CacheProxy_complete; CacheProxy.prototype.get = CacheProxy_get; CacheProxy.prototype.put = CacheProxy_put; CacheProxy.prototype.del = CacheProxy_delete; CacheProxy.prototype.post = CacheProxy_post;

To act as a full proxy for Asynchronous, CacheProxy needs to expose the methods that Asynchronous exposes. This means CacheProxy needs to implement the get, put, del, and other such methods. Implementing a proxy means delegating functionality. In the case of the client calling an HTTP GET, it means the function CacheProxy_get needs to delegate to CacheController. For all of the other functions (for example, CacheProxy_post), CacheProxy delegates to Asynchronous. Doing a full proxy implementation of Asynchronous also requires implementing openCallback and callback, which then delegates to the method CacheProxy.openCallback or CacheProxy. complete that will call the user-defined implementations, if the user defined an implementation.

Putting It All Together

When the HTML code is combined with CacheController and CacheProxy, a cache is created that uses HTTP validation. Using the CacheController HTML code is quicker than not using a cache. Multiple instances of CacheProxy instantiate multiple instances of Asynchronous to allow concurrent downloads. Do not confuse multiple instances with multiple threads. When writing web-browser-based JavaScript applications, there is no such thing as threads, because the JavaScript code in the web browser runs within a single thread. The asynchronous downloads might be running on individual threads, but the point is that when using JavaScript it is not possible to create threads nor use any synchronization mechanisms. Having said all that,

100

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

the cache code was written to be as thread-friendly as possible in case an individual web browser decides to optimize the JavaScript code.

As a side note, many may comment that threading is possible when using JavaScript timers. Again, timers are not threads, but they allow multiple tasks to be executed. Be forewarned that if your script is executing, there is no way to manipulate to the HTML page because the HTML page will be frozen for the length of the request. The example of freezing the browser was illustrated in Chapter 2.

Implementing the Server Side of the HTTP Validator

As mentioned in the explanation of the variable CacheController and the type CacheProxy, it is expected that the server send entity tags and perform the heavy work when comparing these tags. When implementing a server-side HTTP validator, that does not mean implementing a cache. The cache is on the client side, the proxy, or somewhere along the chain of Internet infrastructure. Implementing HTTP validation on the server side means processing entity tags only.

Defining the Book

Let’s begin the book application with the definition of the Book class. The Book class was briefly illustrated in the “Architecture” section, but the details were not discussed, just the hash code feature. The Book class is relatively undemanding and has only data members. It is important to not have any built-in serialization defined in the class because two persistence techniques will be used: file system and general storage. If the general storage were a relational database, an object to relational mapper could be used (Hibernate for Java, or NHibernate for .NET).

Using Java, the Book class is defined as follows:

public class Book { private String _ISBN;

private String _author; private String _title;

public void setISBN(String iSBN) { _ISBN = iSBN;

}

public String getISBN() { return _ISBN;

}

public void setAuthor(String author) { _author = author;

}

public String getAuthor() { return _author;

}

public void setTitle(String title) { _title = title;

}

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

101

public String getTitle() { return _title;

}

}

The Book class has three data members: _ISBN, _author, and _title. The three data members represent the unique state of the Book.

Implementing the Action Classes

For every operation, there is an action set interface. The action set interface is responsible for retrieving, updating, and performing other operations on the data set. The following is an example definition:

public interface Librarian {

public Book checkOutBook(String isbn) throws Exception; public void checkInBook(Book book) throws Exception;

}

The Librarian interface has two methods, checkOutBook and checkInBook, that are used to retrieve and add a book, respectively. An interface is preferred because to implement the operations, the Decorator pattern is going to be used. In a nutshell, the purpose of the Decorator pattern is to dynamically add responsibilities to already existing classes. Relating the Decorator pattern to the Librarian interface means that when the checkInBook method is called, multiple implementations will be called with the same calling parameters.

Implementing the Decorator pattern is appropriate because, as you saw in the “Architecture” section, when implementing static HTTP validation it is necessary to save content to a file and the database. The file was consumed by the HTTP server, and the database for the application. So a calling sequence could be first saving to a file and then saving to the relational database. The Decorator pattern masks these two steps as one. The client thinks only one call is being made. The underlying Decorator pattern implementation handles the details of chaining together the various action set interface implementations.

The following example classes implement the static HTTP validator, which saves to a file and a database. Note that the classes have not been fully implemented from a persistence point of view because doing so would detract from the discussion of the pattern implementations:

public class LibrarianSaveToFile implements Librarian { private static String _rootPath;

private Librarian _next;

public LibrarianSaveToFile(Librarian next) throws InstantiationException { if( _next == null) {

throw new InstantiationException("Next element cannot be null");

}

_next = next;

}

public static void setRootPath(String path) { _rootPath = path;

}

102

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

public Book checkOutBook(String isbn) throws Exception { // Ignore nothing to do and continue to next element return _next.checkOutBook( isbn);

}

public void checkInBook(Book book) throws Exception{

String path = _rootPath + "/books/" + book.getISBN() + ".xml";

// Save the data to a file ...

_next.checkInBook(book);

}

}

public class LibrarianSaveToStorage implements Librarian { public LibrarianSaveToStorage() {

}

public Book checkOutBook(String isbn) throws Exception { // Retrieve from the storage mechanism

return null;

}

public void checkInBook(Book book) throws Exception { // Save to the storage mechanism

}

}

The class LibrarianSaveToFile implements the Librarian interface and is responsible for saving changed content to the file retrieved by using the URL /ajax/books/[ISBN].xml. The class LibrarianSaveToStorage also implements the Librarian interface and is responsible for retrieving and saving the Book data to a relational database. The two classes are separate, and when they are wired together they form the basis of the Decorator pattern.

The way that LibrarianSaveToFile works is that if the class is used, the constructor requires an instance of Librarian. The instance is assigned to the data member next, which is used by LibrarianSaveToFile to delegate Librarian method calls. Looking closer at the abbreviated method implementation LibrarianSaveToFile.checkinBook, the building of a string will be used to save the content to a file on the hard disk. After the file has been saved, the next Librarian instance is called with the exact same parameters and method. The class LibrarianSaveToFile is responsible only for saving the data to a file, and the next instance is responsible for doing its work. In our example, the next instance references the type LibrarianSaveToStorage, which means that the content is saved to the relational database. The advantage of this approach is that each class (LibrarianSaveToFile and LibrarianSaveToStorage) can do what it is best at and leave the rest of the work to another class. The advantage of using the Decorator pattern is that classes can be dynamically wired together without changing the functionality of the other.

To instantiate the classes LibrarianSaveToFile and LibrarianSaveToStorage and to wire them together, the Builder pattern is used, as illustrated in the following example:

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

103

public class LibrarianBuilder {

public static Librarian create(String rootPath) throws InstantiationException { LibrarianSaveToFile.setRootPath(rootPath);

return new LibrarianSaveToFile(new LibrarianSaveToStorage());

}

}

The Builder pattern is an extension of the Factory pattern. It is used to instantiate multiple instances of different types that are arranged in a specific configuration. In the case of our example class LibrarianBuilder, that would mean assigning the root directory by using the method setRootPath, instantiating LibrarianSaveToFile, instantiating LibrarianSaveToStorage, and wiring the two Librarian instances together. The returned Librarian instances would appear to the caller to be a single Librarian instance. However, when a Librarian method is called, two different instances are called.

Implementing Static HTTP Validation

The last step to implementing static HTTP validation is to put the entire solution together to build a web application. The following example uses Java servlets, but other implementations such as ASP.NET could have easily been used:

public class LibrarianServlet extends HttpServlet {

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws javax.servlet.ServletException, java.io.IOException { if(req.getContentType().compareTo(

"application/x-www-form-urlencoded") == 0) {

String operation = req.getParameter("operation"); if(operation != null &&

operation.compareTo("addBook") == 0) {

Librarian librarian = LibrarianBuilder.create(

getServletContext().getInitParameter("generatepath")); try {

Book book = new Book();

String isbn = req.getParameter("isbn"); if(isbn != null) {

try {

book = librarian.checkOutBook(isbn);

}

catch(Exception ex) { book.setISBN(isbn);

}

}

String author = req.getParameter("author"); if(author != null) {

book.setAuthor(author);

}

104

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

String title = req.getParameter("title"); if(title != null) {

book.setTitle(title);

}

resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); librarian.checkInBook(book); out.println( ~

"<html><body>Did update</body></html>");

}

catch(Exception ex) {

throw new ServletException( "LibrarianServlet generated error", ex);

}

}

}

}

}

The servlet LibrarianServlet has implemented the method doPost, meaning that the servlet will react to only HTTP POST requests. As per the “Architecture” section, when implementing the static HTTP validation, the servlet is used to only update data and not to retrieve data. The servlet will process only those requests that post the content as being the type application/x-www-form-urlencoded. Other data types could have been processed, but for the scope of this example only CGI-encoded data is supported. It is important that the server check which content type is sent because the Permutations pattern calls for the server to be able to react to different types.

Because the content type is CGI encoded, there exists an action to carry out, and it is retrieved by using the method req.getParameter( "operation"). Then, based on the operation, the remaining parameters are retrieved: isbn, author, and title. If the isbn parameter exists, the method librarian.checkOutBook is called to retrieve a book instance. This is done on purpose because an already existing book may be updated. The design is to let the servlet incrementally update the contents of the book if it already exists.

Contrast the incremental update to an update in a traditional software language. In traditional software development, when a method requires three parameters, the caller must supply three parameters. This means that to update an object with a single method, all parameters must be supplied. A solution is to create multiple methods with multiple parameters, or to create a structure and then determine which properties are populated. Regardless of which approach is chosen, using a URL is simpler, because the client needs to provide only those details that need to be updated.

When the updated parameters have been retrieved and assigned to the Book instance, the book needs to be saved. The book is saved by using the method librarian.checkInBook. When calling the method checkInBook, the Decorator pattern is called that will then call both

LibrarianSaveToFile and LibrarianSaveToStorage. As illustrated earlier, calling the Librarian instance saves the book to the file and to the relational database. Because the HTTP server is managing the entity tag, a new entity tag will be created.

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

105

Implementing Dynamic HTTP Validation

Implementing dynamic HTTP validation is not that difficult if static HTTP validation has been implemented, because static HTTP validation provides a base for dynamic HTTP validation. When implementing dynamic HTTP validation, the LibrarianSaveToStorage is kept identical as is the use of the Decorator pattern. What changes is the implementation of the Builder pattern: the class LibrarianSaveToFile is replaced with LibrarinHTTPValidation, and the class Book has some additional properties.

What is different in this instance of using the Decorator pattern is that the

LibrarianHTTPValidation class is used to figure out whether LibrarianSaveToStorage has to be called. Additionally, LibrarianSaveToStorage is a bit misnamed because when using dynamic HTTP validation LibrarianSaveToStorage is used for both retrieval and saving of data.

Modifying the Decorator Pattern Implementation

In the static HTTP server validation, the Decorator pattern was used. For the dynamic HTTP server validation, the implementation LibrarianHTTPValidation is used to manage the hash codes of the individual book instances:

public class LibrarianHTTPValidation implements Librarian { private Librarian _next;

private String _etag;

public LibrarianHTTPValidation(String etag, Librarian next) throws InstantiationException {

if( _next == null) {

throw new InstantiationException("Next element cannot be null");

}

_next = next; _etag = etag;

}

public Book checkOutBook(String isbn) throws Exception { if(isSameState( _etag, isbn)) {

Book book = new Book(); book.assignHashCode(Integer.parseInt( _etag)); book.setISBN( isbn);

return book;

}

else {

return _next.checkOutBook( isbn);

}

}

public void checkInBook(Book book) throws Exception { saveHashCode(book);

_next.checkInBook(book);

}

}

In the instantiation of the LibrarianHTTPValidation, the constructor has two parameters. The first parameter identifies the ETag, and the second parameter is the next Librarian instance,

106

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

which is LibrarianSaveToStorage. The method checkOutBook has an incomplete method isSameState that is used to test whether the input etag parameter and the to-be-retrieved book instance associated with the isbn number are identical. The method isSameState is incomplete because the way that the cross-referencing of the client-supplied ETag identifier and the current hash code value is done depends on how the old hash code value is stored. It’s an implementation detail that is beyond the scope of this book.

If the method isSameState indicates that the state has not changed, Book is instantiated and the hash code is assigned to the input ETag value. The instantiated value is returned. If the method isSameState indicates that the state has changed, then the checkOutBook is delegated to the next Librarian instance (_next.checkOutBook).

In the implementation of checkInBook, a call is made to an incomplete method implementation, saveHashCode. The incomplete method saveHashCode saves the current hash code value and its associated unique ISBN identifier. After the values have been saved, the next Librarian instance is called to persist the value to the underlying storage mechanism.

To instantiate the new Decorator pattern structure, the Builder pattern has to be modified and would appear similar to the following:

public class LibrarianBuilder {

public static Librarian create(String etag) throws InstantiationException {

if(etag != null && etag.length() > 0) {

return new LibrarianHTTPValidation(etag, new LibrarianSaveToStorage());

}

else {

return new LibrarianSaveToStorage();

}

}

}

The modified method create requires a parameter that is passed in etag from the client. If the etag value is null, the class LibrarianSaveToStorage is instantiated without any parameters, indicating that either the content sent to the client is called for the first time or HTTP validation is not used. If there is an etag value and its length is greater than zero, a validation using

LibrarianHTTPValidation is performed. The class LibrarianSaveToStorage is still instantiated, but the instance is a parameter to the constructor of LibrarianHTTPValidation, and both instances are chained together.

Putting It All Together

In dynamic HTTP validation, it is necessary to implement multiple HTTP verbs. In the example, the verbs GET and PUT are implemented. Note that the same code used for PUT could also be used for POST to make the servlet HTML form-friendly.

The implementation of the hash code calculation has been shown in the “Architecture” section and will not be reiterated because doing so would provide no value. The hash code would be calculated on the state of the object that is saved to a file or a relational database.

The servlet implementation is defined as follows:

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

107

public class ValidationLibrarianServlet extends HttpServlet { protected void doGet(HttpServletRequest req,

HttpServletResponse resp)

throws javax.servlet.ServletException, java.io.IOException {

String isbn = getISBNFromURL(req.getRequestURI()); try {

String etagvalue = req.getHeader("If-Match"); Librarian librarian =

LibrarianBuilder.create(etagvalue); Book book = librarian.checkOutBook(isbn);

if(etagvalue != null && book.hashCode() == Integer.parseInt(etagvalue)) { resp.setStatus(304, "Not modified"); return;

}

resp.setHeader("ETag", Integer.toString( book.hashCode()));

generateGetContent(resp, book);

}

catch (Exception ex) {

throw new ServletException( "LibrarianServlet generated error", ex);

}

}

protected void doPut(HttpServletRequest req, HttpServletResponse resp)

throws javax.servlet.ServletException, java.io.IOException {

try {

Librarian librarian = LibrarianBuilder.create("empty");

Book book = getDetailsFromRequest(req); librarian.checkInBook(book); generatePutContent(resp, book);

}

catch (Exception ex) {

throw new ServletException( "LibrarianServlet generated error", ex);

}

}

}

In the example code, a number of incomplete methods are beyond the scope of this pattern because they are implementation details specific to a code base. Starting with the method goGet, which is called when the HTTP GET method is called, the ISBN is retrieved. At the beginning of this chapter, the URL /ajax/books/[ISBN].xml was used to uniquely identify a book. The method getISBNFromURL will parse the URL and retrieve the desired ISBN. Having multiple

108

C H A P T E R 4 C A C H E C O N T R O L L E R P A T T E R N

URLs associated with a single servlet is not difficult. Specifically for Java, the administrator would change the web.xml file to associate the base URL /ajax/books with the

ValidationLibrarianServlet.

After having extracted the ISBN number, the ETag identifier is retrieved from the request by using the method req.getHeader( "If-Match"). The retrieved instance is passed as a parameter to the method LibrarianBuilder.create. Depending on the value of the ETag, a decorated

LibrarianSaveToStorage class is created.

The method checkOutBook is called, and an instance will be retrieved that indicates either that an HTTP 304 should be returned, or that a new instance has been instantiated and output should be generated. If output is generated, an ETag identifier is generated and added to the HTTP output.

The method doPut is called whenever an HTTP PUT is called. The implementation is relatively simple in that the decorated Librarian classes are instantiated, and the Book class parameters are retrieved and added to the underlying storage mechanism by using the method checkInBook. Because the Librarian classes are decorated, the hash code value will be automatically identified with the ISBN of the book.

The examples illustrated a relatively simple HTTP GET and PUT. Let’s say that you want to search for a book based on the title. Then the URL /ajax/books/search?author=[name] could be used, and ValidationLibrarianServlet would need to be extended to include the functionality.

Pattern Highlights

Let’s wrap all of this up and consider what the Cache Controller pattern accomplishes. The purpose of the Cache Controller pattern is to provide a temporary cache by complementing the Internet infrastructure. The idea is not to re-create yet another caching infrastructure, because the Internet already does that very well. In the scope of writing web applications, the cache infrastructure to use is HTTP validation. Even though HTTP validation is not typically used for scripts, it can and should be.

The following points are important highlights of the Cache Controller pattern:

When using a cache, it is preferable to use the HTTP Validation model. The HTTP Expiration model is less useful because expiration says content is good for a certain time frame regardless of what happens to the server.

When using HTTP validation for writing a cache, only the client actually caches the information. The server is responsible for generating the entity tags and for comparing old with new entity tags. This means that the server has to keep a sense of history with respect to the changing state of the objects.

There are two ways to implement HTTP validation: letting the HTTP server do the heavy lifting, or creating a server-side processor that does everything.

When letting the HTTP server do the heavy lifting, the server framework (for example, JSP, Servlet, ASP.NET) is responsible for updating the static content pieces managed by the HTTP server.