Maintaining state
One of the biggest drawbacks of the earlier example is that it fetches the full set of results at one time, which is impractical for large result sets. It is more practical to display a full set of results across multiple pages and allow the user to move forward or backward through the pages.
This is simple in a conventional application where a connection to the separate server is maintained until the user terminates the application. In a web implementation however, this seemingly simple requirement involves a considerably higher level of complexity due to the stateless nature of web pages. One such complexity is that each time a new page of results is displayed, the initial search for the records must be re-executed. This is inconvenient for the web programmer and potentially slow for the user.
The IMu server provides a solution to this. When a handler object is created, a corresponding object is created on the server to service the handler's request: this server-side object is allocated a unique identifier by the IMu server. When making a request for more information, the unique identifier can be used to connect a new handler to the same server-side object, with its state intact.
The following example illustrates the connection of a second, independently
created Module
object to the same server-side object:
// Create a module object as usual Module first = new Module("eparties", session); // Run a search - this will create a server-side object long[] keys = { 1, 2, 3, 4, 5, 42 }; first.findKeys(keys); // Get a set of results ModuleFetchResult result = first.fetch("start", 0, 1, "SummaryData"); // Create a second module object Module second = new Module("eparties", session); /* * Attach it to the same server-side object as the first module. * This is the key step. */ second.setID(first.getID()); // Get a second set of results from the same search ModuleFetchResult result2 = second.fetch("current", 1, 2, "SummaryData");
Although two completely separate Module
objects have been created, they
are each connected to the same server-side object by virtue of having the same
id
property. This means that the second fetch
call will access the same
result set as the first fetch
. Notice that a flag of current
has been
passed to the second call. The current state is maintained on the server-side
object, so in this case the second call to Fetch
will return the third and
fourth records in the result set.
While this example illustrates the use of the id
property, it is not
particularly realistic as it is unlikely that two distinct objects which refer
to the same server-side object would be required in the same piece of code. The
need to re-connect to the same server-side object when generating another page
of results is far more likely. This situation involves creating a server-side
Module
object (to search the module and deliver the first set of
results) in one request and then re-connecting to the same server-side object
(to fetch a second set of results) in a second request. As before, this is
achieved by assigning the same identifier to the id
property of the object
in the second page, but two other things need to be considered.
By default the IMu server destroys all server-side objects when a session
finishes. This means that unless the server is explicitly instructed not to do
so, the server-side object may be destroyed when the connection from the first
page is closed. Telling the server to maintain the server-side object only
requires that the destroy
property on the object is set to false
before
calling any of its methods. In the example above, the server would be
instructed not to destroy the object as follows:
Module module = new Module("eparties", session); module.setDestroy(false); long[] keys = { 1, 2, 3, 4, 5, 42 }; module.findKeys(keys);
The second point is quite subtle. When a connection is established to a server, it is necessary to specify the port to connect to. Depending on how the server has been configured, there may be more than one server process listening for connections on this port. Your program has no control over which of these processes will actually accept the connection and handle requests. Normally this makes no difference, but when trying to maintain state by re-connecting to a pre-existing server-side object, it is a problem.
For example, suppose there are three separate server processes listening for connections. When the first request is executed it connects, effectively at random, to the first process. This process responds to the request, creates a server-side object, searches the Parties module for the terms provided and returns the first set of results. The server is told not to destroy the object and passes the server-side identifier to another page which fetches the next set of results from the same search.
The problem comes when the next page connects to the server again. When the connection is established any one of the three server processes may accept the connection. However, only the first process is maintaining the relevant server-side object. If the second or third process accepts the connection, the object will not be found.
The solution to this problem is relatively straightforward. Before the first
request closes the connection to its server, it must notify the server that
subsequent requests need to connect explicitly to that process. This is
achieved by setting the Session
object's suspend
property to true
prior to submitting any request to the server:
Session session = new Session("server.com", 12345); Module module = new Module("eparties", session); session.setSuspend(true); module.findKeys(keys);
The server handles a request to suspend a connection by starting to listen for
connections on a second port. Unlike the primary port, this port is guaranteed
to be used only by that particular server process. This means that a subsequent
page can reconnect to a server on this second port and be guaranteed of
connecting to the same server process. This in turn means that any saved
server-side object will be accessible via its identifier. After the request has
returned (in this example it was a call to findKeys
), the Session
object's port
property holds the port number to reconnect to:
session.setSuspend(true); module.findKeys(keys); int reconnect = session.getPort();
Example
To illustrate we'll modify the very simple JSP web page of the earlier
section to display the list of matching names in
blocks of five records per page. We'll provide simple Next and Prev links
to allow the user to move through the results, and we will use some more
GET
parameters to pass the port we want to reconnect to, the identifier of
the server-side object and the rownum
of the first record to be displayed.
First build the search page, which is a plain HTML form:
<head> <title>Party Search</title> </head> <body> <form action="example.jsp"> <p>Enter a last name to search for (e.g. S*):</p> <input type="text" name="name"/> <input type="submit" value="Search"/> </form> </body>
Next build the results page (source code), which runs the search and displays the results. The steps to build the search page are outlined in detail below.
- Create a
Session
object. Then theport
property is set to a standard value unless a port parameter has been passed in the URL:<%@ page import="com.kesoftware.imu.*" %>
<%
/*
* Create new session object.
*/
Session imuSession = new Session();
imuSession.setHost("imu.mel.kesoftware.com");
/*
* Work out what port to connect to.
*/
int port = 40136;
if (request.getParameter("port") != null)
port = Integer.parseInt(request.getParameter("port"));
imuSession.setPort(port);
- Connect to the server and immediately set the
suspend
property totrue
to tell the server that we may want to connect again:/*
* Establish connection and tell the server we may want to re-connect.
*/
imuSession.connect();
imuSession.setSuspend(true);
This ensures the server listens on a new, unique port.
- Create the client-side
Module
object and set itsdestroy
property tofalse
:/*
* Create module object and tell the server not to destroy it.
*/
Module module = new Module("eparties", imuSession);
module.setDestroy(false);
This ensures that the server will not destroy the corresponding server-side object when the session ends.
- If the URL included a
name
parameter, we need to do a new search. Alternatively, if it included anid
parameter, we need to connect to an existing server-side object:/* If name is supplied, do new search. The search term is passed from
* example.html using GET.
*/
if (request.getParameter("name") != null)
{
Terms terms = new Terms(TermsKind.OR);
terms.add("NamLast", request.getParameter("name"));
module.findTerms(terms);
}
/*
* Otherwise, if id is supplied reattach to existing server-side object.
*/
else if (request.getParameter("id") != null)
{
module.setID(request.getParameter("id"));
}
/*
* Otherwise, we can't process.
*/
else
{
throw new Exception("no name or id");
}
- Build a list of columns to fetch:
String[] columns = { "NamFirst", "NamLast" };
- If the URL included a
rownum
parameter, fetch records starting from there. Otherwise start from record number1
:/*
* Work out which block of records to fetch.
*/
int rownum = 1;
if (request.getParameter("rownum") != null)
rownum = Integer.parseInt(request.getParameter("rownum"));
- Build the main page:
/*
* Fetch next five records
*/
ModuleFetchResult result = module.fetch("start", (rownum - 1), 5, columns);
long hits = result.getHits();
/*
* Save rows in convenient variable for later use.
*/
Map[] rows = result.getRows();
/*
* Build the results page
*/
%>
<!DOCTYPE html>
<html>
<head>
<title>IMu JAVA API - Maintaining State</title>
</head>
<body>
<!-- show hit count -->
<p>Number of matches: <% out.print(hits); %></p>
<table>
<%
// Display each match in a separate row in a table
for (int i = 0; i < rows.length; i++)
{
Map row = rows[i];
Long num = row.getLong("rownum");
out.println("<tr>");
out.println("\t<td>" + num.toString() + "</td>");
out.println("\t<td>" + row.getString("NamFirst") + " " +
row.getString("NamLast") + "</td");
out.println("</tr>");
}
%>
</table>
<%
- Finally, add the Prev and Next links to allow the user to page backwards
and forwards through the results. This is the most complicated part! First,
to ensure that a connection is made to the same server and server-side
object, add the appropriate
port
andid
parameters to the link URL:/*
* Add the Prev and Next links
*/
String url = request.getRequestURL().toString();
url += "?port=" + imuSession.getPort();
url += "&id=" + module.getID();
- If the first record is not showing add a Prev link to allow the user to go
back one page in the result set. Similarly, if the last record is not
showing add a Next link to allow the user to go forward one page:
Map first = rows[0];
if (first.getLong("rownum") > 1)
{
Long prev = (first.getLong("rownum") - 5);
if (prev < 1L)
prev = 1L;
out.println("<a href=\"" + url + "&rownum=" + prev + "\">Prev</a>");
}
Map last = rows[rows.length - 1];
if (last.getLong("rownum") < hits)
{
Long next = last.getLong("rownum") + 1L;
out.println("<a href=\"" + url + "&rownum=" + next + "\">Next</a>");
}
%>