Introduction
ItsNat is a web framework strongly focused on Single Page Interface
applications.
The Single Page Interface (SPI) paradigm is not new, these days SPI is
very popular on "web applications" because modern web frameworks with
AJAX support make this kind of applications easier than ever.
However this tutorial shows how to build SPI web sites, web sites
based on pages can be evolved to SPI without losing the benefits of page
based web sites like bookmarking, Search Engine Optimization (SEO) or accessibility (JavaScript disabled)
according to The Single Page Interface Manifesto.
To understand this tutorial some basic knowledge of ItsNat is required.
How can a web site be SPI and page based at the same time?
The typical behavior of ItsNat is the following: when the DOM in server
changes JavaScript code is automatically generated and sent to the client
to update the client DOM accordingly. Usually in web sites many elements are
shared like the header, footer etc and the content area is almost the unique
zone being fully changed, in SPI this "content area" can be changed with new
HTML fragments, a HTML fragment is dynamically inserted into the DOM document
in server using W3C DOM Java API, in the same time this new HTML code is also
automatically inserted by JavaScript code in client (usually using innerHTML),
unfortunately this approach is not SEO friendly because web crawlers absolutely
ignore JavaScript.
To provide page simulation one ItsNat feature is the key: fast-load mode.
If ItsNat is configured in fast-load mode (the default mode), when the
initial page (based on a pure HTML template) is being loaded, any DOM change
performed in server is not sent as JavaScript, DOM rendering to generate the
initial HTML page being sent to the client, is done after the user code in
server is executed. Hence the same user code manipulating DOM can generate
JavaScript or plain HTML depending on the phase is executed, when an event is
received or on load time of the initial page.
When fast-load mode is disabled, the template being used is the markup being
sent to the client and DOM changes in server are sent as JavaScript DOM operations.
In fast-load mode we can use a parameter in the query part of URL or in
your preferred pretty URL to specify what "page" ("fundamental state" following
the SPI Manifesto terminology) is going to be loaded, executing the same code
being used to insert the required HTML fragment into the content zone. The final
DOM tree is the tree being serialized as HTML and loaded by the client.
These are the basics, more things are required to provide in SPI the same
capabilities as page based web sites, such as dual (AJAX/normal) links, URL
rewriting for bookmarking, support of page visit counters (in this case "state visit counters"),
JavaScript disabled and support of Back/Forward buttons (history navigation).
These will be explained with code.
Web application set-up
ItsNat does not require special set up or application servers, any servlet
container supporting Java 1.4 or upper is enough. Use your preferred IDE to
create an empty web application, this tutorial uses spitut as the name of the
web application. Add to the WEB-INF/lib directory all required JAR files by ItsNat
(you can find these jars in /fw_dist/lib folder of the ItsNat distribution).
Creating the ItsNat servlet
ItsNat does not impose a bootstrap, there is no "default" ItsNat servlet,
in fact you can use several servlets using ItsNat in your web application.
Using your IDE add a new servlet class with name servlet (this name is not
mandatory) in the default package (again not mandatory).
Default registration in web.xml is valid, ItsNat does not require special
initialization parameters or filters in web.xml.
According to this setup the URL accessing our servlet is (8080 is supposed):
http://localhost:8080/spitut/servlet
Because our web site is SPI we would like a prettier URL like
http://localhost:8080/spitut/
We have two options:
- Add servlet as the welcome file
<welcome-file-list>
<welcome-file>servlet</welcome-file>
</welcome-file-list>
- Add a simple
index.jsp (this file is usually by default
the "welcome file" and lazy people use this JSP to start the web application
in their IDE) with this content:
<jsp:forward page="/servlet" />
Now replace the generated code of the servlet class with this code:
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.itsnat.core.tmpl.ItsNatDocumentTemplate;
import org.itsnat.core.ItsNatServletConfig;
import org.itsnat.core.ItsNatServletContext;
import org.itsnat.core.http.HttpServletWrapper;
import org.itsnat.core.http.ItsNatHttpServlet;
import org.itsnat.spitut.SPITutGlobalEventListener;
import org.itsnat.spitut.SPITutGlobalLoadRequestListener;
import org.itsnat.spitut.SPITutMainLoadRequestListener;
public class servlet extends HttpServletWrapper
{
public void init(ServletConfig config) throws ServletException
{
super.init(config);
ItsNatHttpServlet itsNatServlet = getItsNatHttpServlet();
ItsNatServletContext itsNatCtx = itsNatServlet.getItsNatServletContext();
itsNatCtx.setMaxOpenDocumentsBySession(4);
ItsNatServletConfig itsNatConfig = itsNatServlet.getItsNatServletConfig();
itsNatConfig.setFastLoadMode(true);
String pathBase = getServletContext().getRealPath("/");
String pathPages = pathBase + "/WEB-INF/pages/";
String pathFragments = pathBase + "/WEB-INF/fragments/";
itsNatServlet.addEventListener(new SPITutGlobalEventListener());
itsNatServlet.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());
ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",
pathPages + "main.xhtml");
docTemplate.addItsNatServletRequestListener(new SPITutMainLoadRequestListener());
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("google_analytics","text/html",
pathPages + "google_analytics.xhtml");
docTemplate.setScriptingEnabled(false);
itsNatServlet.registerItsNatDocFragmentTemplate("not_found","text/html",
pathFragments + "not_found.xhtml");
itsNatServlet.registerItsNatDocFragmentTemplate("overview","text/html",
pathFragments + "overview.xhtml");
itsNatServlet.registerItsNatDocFragmentTemplate("overview.popup","text/html",
pathFragments + "overview_popup.xhtml");
itsNatServlet.registerItsNatDocFragmentTemplate("detail","text/html",
pathFragments + "detail.xhtml");
itsNatServlet.registerItsNatDocFragmentTemplate("detail.more","text/html",
pathFragments + "detail_more.xhtml");
}
}
As you can see our servlet now inherits from HttpServletWrapper:
public class servlet extends HttpServletWrapper
This ItsNat class redirects any request to the ItsNatHttpServlet
object wrapping the servlet instance.
Web application configuration is done in the standard init(ServletConfig) method,
configuration is ItsNat is "classic", imperative, calling configuration methods.
itsNatCtx.setMaxOpenDocumentsBySession(4);
This call sets 4 documents with server state by user session, this value
tries to avoid abusive users of opening too many browser windows with the same
page because these pages are isolated in server (no shared data between pages).
itsNatConfig.setFastLoadMode(true);
This call is not really necessary because fast-load mode is the default,
this method is called to clearly state fast-load mode is mandatory in SPI web
sites with page simulation.
String pathBase = getServletContext().getRealPath("/");
String pathPages = pathBase + "/WEB-INF/pages/";
String pathFragments = pathBase + "/WEB-INF/fragments/";
The folder WEB-INF/pages is going to be used to save "ItsNat page
templates", because our web application is SPI only one page template is need,
later another very simple page will be added to monitor page visits. Into the
folder WEB-INF/fragments will be saved "page fragments", page
fragments are pure HTML pages where only the content of <body> (or <head>) is used.
itsNatServlet.addEventListener(new SPITutGlobalEventListener());
SPITutGlobalEventListener is a global org.w3c.dom.events.EventListener,
all AJAX requests received by this servlet (for any document loaded by this servlet)
are first dispatched to this listener. This is the code:
package org.itsnat.spitut;
import org.itsnat.core.ClientDocument;
import org.itsnat.core.event.ItsNatEvent;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
public class SPITutGlobalEventListener implements EventListener
{
public SPITutGlobalEventListener()
{
}
public void handleEvent(Event evt)
{
ItsNatEvent itsNatEvt = (ItsNatEvent)evt;
if (itsNatEvt.getItsNatDocument() == null)
{
StringBuffer code = new StringBuffer();
code.append("if (confirm('Expired session. Reload?'))");
code.append(" window.location.reload(true);");
ClientDocument clientDoc = itsNatEvt.getClientDocument();
clientDoc.addCodeToSend(code.toString());
itsNatEvt.getItsNatEventListenerChain().stop();
}
}
}
When the web browser of an end user sends an event from a client document
lost in server (usually because the user session is expired), the method
ItsNatEvent.getItsNatDocument() returns null because this event
is "orphan", there is no document in server attached to the client document.
In this case we ask to the end user to reload the page.
The call:
itsNatEvt.getItsNatEventListenerChain().stop();
is not really needed, it prevents other global event listeners of being
called (not in this case).
This SPITutGlobalEventListener class is not actually needed
because the default behavior of ItsNat processing an orphan event is to
automatically reload the page (the only difference is that end user is not asked).
Following in the servlet:
itsNatServlet.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());
Registers a global document (page) listener. This listener is called when
a page (=document) load is requested or any other unknown page request (not AJAX
events). Source code:
package org.itsnat.spitut;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.itsnat.core.ItsNatDocument;
import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import org.itsnat.core.event.ItsNatServletRequestListener;
public class SPITutGlobalLoadRequestListener implements ItsNatServletRequestListener
{
public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
{
ItsNatDocument itsNatDoc = request.getItsNatDocument();
if (itsNatDoc == null)
{
ServletRequest servRequest = request.getServletRequest();
String gAnalytState = servRequest.getParameter("ganalyt_st");
if (gAnalytState != null)
servRequest.setAttribute("itsnat_doc_name","google_analytics");
else
servRequest.setAttribute("itsnat_doc_name","main");
ServletResponse servResponse = response.getServletResponse();
request.getItsNatServlet().processRequest(servRequest,servResponse);
}
}
}
If the request includes the default parameter itsnat_doc_name and the
specified template exists the method ItsNatServletRequest.getItsNatDocument()
returns a non-null ItsNatDocument object, if the URL requested
does not include itsnat_doc_name is a "custom" request and getItsNatDocument()
returns null. The later is the case of pretty URLs.
We first check whether the parameter ganalyt_st is present,
this parameter is going to be used later for state monitoring using Google
Analytics. If this parameter is not present is the expected case of a request
like this:
http://localhost:8080/spitut/
Or this:
http://localhost:8080/spitut/servlet
In this case we have to load the main page of our web site, so itsnat_doc_name
is specified with value "main" as a request attribute, the value "main" is
the name of the main template of our SPI web site, ItsNat checks first if
itsnat_doc_name was specified as attribute and then as parameter.
Now we are ready to re-send the request to ItsNat calling:
request.getItsNatServlet().processRequest(servRequest,servResponse);
This new request has the same effect as the URL:
http://localhost:8080/spitut/servlet?itsnat_doc_name=main
Now the target page template is specified and found and request.getItsNatDocument();
returns a non-null value and nothing is done in this case because this global
listener is not the typical place to manage normal page load requests.
Main page processing
Returning to the servlet:
ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",
pathPages + "main.xhtml");
This call registers with the name "main" the page template file main.xhtml
(saved in WEB-INF/pages/):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="expires" content="Wed, 1 Dec 1997 03:01:00 GMT" />
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
<title id="titleId" itsnat:nocache="true">Tutorial: Single Page Interface Web Site With ItsNat</title>
<link rel="stylesheet" type="text/css" href="css/style.css" />
<script type="text/javascript" src="js/spi_bookmarking.js?timestamp=2011-02-11_01"></script>
<script type="text/javascript">
function setState(name)
{
if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded
var itsNatDoc = document.getItsNatDoc();
var evt = itsNatDoc.createUserEvent("setState");
evt.setExtraParam("name",name);
itsNatDoc.dispatchUserEvent(null,evt);
}
window.spiSite.onBackForward = setState;
</script>
</head>
<body>
<div class="main">
<table style="width:100%; height:100%; padding:0; margin:0;" border="0px" cellpadding="0" cellspacing="0">
<tbody>
<tr style="height:50px;">
<td>
<h2 style="text-align:center;">Tutorial: Single Page Interface Web Site With ItsNat</h2>
</td>
</tr>
<tr style="height:40px;">
<td>
<table style="width:100%; margin:0; padding:0; border: #ED752A solid; border-width: 0 0 2px 0; ">
<tbody>
<tr class="mainMenu" itsnat:nocache="true">
<td id="menuOpOverviewId">
<a href="?st=overview" onclick="setState('overview'); return false;"
class="menuLink">Overview</a>
</td>
<td id="menuOpDetailId">
<a href="?st=detail" onclick="setState('detail'); return false;"
class="menuLink">Detail</a>
</td>
<td> </td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr style="height:70%; /* For MSIE */">
<td id="contentParentId" itsnat:nocache="true" style="padding:20px; vertical-align:super;" >
</td>
</tr>
<tr style="height:50px"">
<td style="border-top: 1px solid black; text-align:center;">
SOME FOOTER
</td>
</tr>
</tbody>
</table>
</div>
<iframe id="googleAnalyticsId" itsnat:nocache="true" src="?ganalyt_st=" style="display:none;" ></iframe>
</body>
</html>
As you can see ItsNat templates are pure HTML files because view logic is
coded with W3C DOM Java API. By default templates are cached, that is, DOM
nodes are internally serialized as plain HTML and shared between users; any
cached DOM subtree is replaced in server by a text node containing a special
mark, when this subtree is sent to the client ItsNat automatically sends the
cached markup. DOM caching is interesting for "static" (server point of view)
parts of the template, caching can be fully avoided with a configuration flag.
Because we want to modify some parts, these parts are not static and must be
marked as "not cached" with the special attribute itsnat:nocache="true"
where itsnat prefix is declared in <html> as xmlns:itsnat="http://itsnat.org/itsnat".
For instance:
<title id="titleId" itsnat:nocache="true">Tutorial: Single Page Interface Web Site With ItsNat</title>
The title of the SPI web site will change when a new fundamental state is loaded.
<tr class="mainMenu" itsnat:nocache="true">
This row contains the web site main menu, is not cached because we need
to access menu items in server to change the color of the current selected option.
<td id="contentParentId" itsnat:nocache="true" style="padding:20px; vertical-align:super;" >
This table cell is the parent of the "content area", when the end user clicks
some menu option, this zone will change accordingly.
Finally:
<iframe id="googleAnalyticsId" itsnat:nocache="true" src="?ganalyt_st=" style="display:none;" ></iframe>
The URL of this <iframe> will be changed to keep track of fundamental states.
The following code:
<script type="text/javascript">
function setState(name)
{
if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded
var itsNatDoc = document.getItsNatDoc();
var evt = itsNatDoc.createUserEvent("setState");
evt.setExtraParam("name",name);
itsNatDoc.dispatchUserEvent(null,evt);
}
window.spiSite.onBackForward = setState;
</script>
Is being used to send ItsNat "user events", a user event is an extension of
W3C DOM Events and is fired calling some public ItsNat methods from JavaScript.
User events are used in this tutorial to notify the server about the next fundamental
state to be loaded, these user events are received by a user event listener in server
previously registered.
An example of how fundamental states are changed is this link (a menu item):
<a href="?st=overview" onclick="setState('overview'); return false;"
class="menuLink">Overview</a>
When end user clicks this link the call setState('overview'); sends a
user event to command server to load the new fundamental state "overview", because
onclick returns false the default behavior of the link is aborted
hence href="?st=overview" is ignored and the page remains the same
(partially changed with the new state). This is not the case of search engine
crawlers, these bots ignore JavaScript and follow the link loading a new page
with "overview" as the initial state. This is an example of dual link AJAX/normal.
Now is the time of registering a load listener for the main template
(back to the servlet):
ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",
pathPages + "main.xhtml");
docTemplate.addItsNatServletRequestListener(new SPITutMainLoadRequestListener());
The SPITutMainLoadRequestListener.processRequest(…) method
will be called when the servlet receives a new load request of this template,
one call per load request. The source code is really simple:
package org.itsnat.spitut;
import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import org.itsnat.core.event.ItsNatServletRequestListener;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
public class SPITutMainLoadRequestListener implements ItsNatServletRequestListener
{
public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
{
new SPITutMainDocument((ItsNatHttpServletRequest)request,(ItsNatHttpServletResponse)response);
}
}
A new SPITutMainDocument instance is created per call (load request),
this instance holds an ItsNatHTMLDocument object which represents
the client document (page) being loaded.
This object is not going to be garbage collected because some event listener
is going to be registered in ItsNatHTMLDocument and this document
instance is automatically hold by ItsNat following the life cycle of the client
document: when the end user leaves the page the document in server is automatically
discarded. If no unload event is received ItsNat automatically discards documents
in server when no event is received for a long time (the session expiring time is
used) or when the user session expires (in spite of ItsNat developers do not use
sessions, in ItsNat servlet sessions are the basic mechanism to identify end users).
Source code of SPITutMainDocument:
package org.itsnat.spitut;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.itsnat.core.ItsNatServlet;
import org.itsnat.core.domutil.ItsNatDOMUtil;
import org.itsnat.core.event.ItsNatUserEvent;
import org.itsnat.core.html.ItsNatHTMLDocument;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.core.tmpl.ItsNatDocFragmentTemplate;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.html.HTMLDocument;
import org.w3c.dom.html.HTMLTitleElement;
public class SPITutMainDocument implements EventListener
{
protected ItsNatHTMLDocument itsNatDoc;
protected String title;
protected HTMLTitleElement titleElem;
protected Map<String,Element> menuElemMap = new HashMap<String,Element>();
protected Element currentMenuItemElem;
protected Element contentParentElem;
protected SPITutState currentState;
protected Element googleAnalyticsElem;
protected String googleAnalyticsIFrameURL;
public SPITutMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response)
{
this.itsNatDoc = (ItsNatHTMLDocument)request.getItsNatDocument();
HTMLDocument doc = itsNatDoc.getHTMLDocument();
this.titleElem = (HTMLTitleElement)doc.getElementById("titleId");
this.title = titleElem.getText();
menuElemMap.put("overview",doc.getElementById("menuOpOverviewId"));
menuElemMap.put("detail",doc.getElementById("menuOpDetailId"));
itsNatDoc.addUserEventListener(null,"setState", this);
this.contentParentElem = doc.getElementById("contentParentId");
this.googleAnalyticsElem = doc.getElementById("googleAnalyticsId");
this.googleAnalyticsIFrameURL = googleAnalyticsElem.getAttribute("src");
HttpServletRequest servReq = request.getHttpServletRequest();
String stateName = servReq.getParameter("_escaped_fragment_");
if (stateName != null)
{
if (stateName.startsWith("st="))
stateName = stateName.substring("st=".length(), stateName.length());
else
stateName = "overview";
}
else
{
stateName = servReq.getParameter("st");
if (stateName == null)
stateName = "overview";
}
changeState(stateName);
}
public ItsNatHTMLDocument getItsNatHTMLDocument()
{
return itsNatDoc;
}
public void setStateTitle(String stateTitle)
{
String pageTitle = title + " - " + stateTitle;
if (itsNatDoc.isLoading())
titleElem.setText(pageTitle);
else
itsNatDoc.addCodeToSend("document.title = \"" + pageTitle + "\";\n");
}
public Element getContentParentElement()
{
return contentParentElem;
}
public ItsNatDocFragmentTemplate getFragmentTemplate(String name)
{
ItsNatServlet servlet = itsNatDoc.getItsNatDocumentTemplate().getItsNatServlet();
return servlet.getItsNatDocFragmentTemplate(name);
}
public DocumentFragment loadDocumentFragment(String name)
{
ItsNatDocFragmentTemplate template = getFragmentTemplate(name);
if (template == null) return null;
return template.loadDocumentFragment(itsNatDoc);
}
public String getFragmentName(String stateName)
{
String fragmentName = stateName;
int pos = stateName.indexOf('.');
if (pos != -1) fragmentName = stateName.substring(0, pos);
return fragmentName;
}
public void changeState(String stateName)
{
String fragmentName = getFragmentName(stateName);
ItsNatDocFragmentTemplate template = getFragmentTemplate(fragmentName);
if (template == null)
{
changeState("not_found");
return;
}
if (currentState != null)
{
currentState.dispose();
this.currentState = null;
}
ItsNatDOMUtil.removeAllChildren(contentParentElem);
changeActiveMenu(stateName);
DocumentFragment frag = template.loadDocumentFragment(itsNatDoc);
contentParentElem.appendChild(frag);
if (stateName.equals("overview")||stateName.equals("overview.showpopup"))
{
boolean popup = stateName.equals("overview.showpopup");
this.currentState = new SPITutStateOverview(this,popup);
}
else if (stateName.equals("detail"))
this.currentState = new SPITutStateDetail(this);
itsNatDoc.addCodeToSend("try{ window.scroll(0,-5000); }catch(ex){}");
}
public void registerState(SPITutState state)
{
setStateTitle(state.getStateTitle());
String stateName = state.getStateName();
itsNatDoc.addCodeToSend("spiSite.setURLReference(\"" + stateName + "\");");
googleAnalyticsElem.setAttribute("src",googleAnalyticsIFrameURL + stateName);
}
public void handleEvent(Event evt)
{
if (evt instanceof ItsNatUserEvent)
{
ItsNatUserEvent itsNatEvt = (ItsNatUserEvent)evt;
String name = (String)itsNatEvt.getExtraParam("name");
changeState(name);
}
}
public void changeActiveMenu(String stateName)
{
String mainMenuItem;
int pos = stateName.indexOf('.');
if (pos != -1) mainMenuItem = stateName.substring(0, pos);
else mainMenuItem = stateName;
if (currentMenuItemElem != null)
currentMenuItemElem.removeAttribute("class");
this.currentMenuItemElem = menuElemMap.get(mainMenuItem);
if (currentMenuItemElem != null)
currentMenuItemElem.setAttribute("class","menuOpSelected");
}
}
This class is the core of the web site, tightly associated to the main
template and responsible of (fundamental) state management, states directly
dependent on main menu.
The first sentence:
this.itsNatDoc = (ItsNatHTMLDocument)request.getItsNatDocument();
Saves the ItsNat document object in an attribute because SPITutMainDocument
is a wrapper of the ItsNatHTMLDocument.
this.titleElem = (HTMLTitleElement)doc.getElementById("titleId");
this.title = titleElem.getText();
We are going to change the page title when a new fundamental state is loaded.
menuElemMap.put("overview",doc.getElementById("menuOpOverviewId"));
menuElemMap.put("detail",doc.getElementById("menuOpDetailId"));
The menuElemMap collection maps state names with the DOM elements of the menu
items, we need these elements to change their appearance when a menu option is
selected, menu selection changes the current fundamental state being shown.
itsNatDoc.addUserEventListener(null,"setState", this);
Adds this instance as the user event listener listening state changes,
usually when end users click a menu option. User events are received by the
handleEvent(Event) method of this class.
this.contentParentElem = doc.getElementById("contentParentId");
this.googleAnalyticsElem = doc.getElementById("googleAnalyticsId");
this.googleAnalyticsIFrameURL = googleAnalyticsElem.getAttribute("src");
These DOM elements are useful to insert/remove the markup of the "content area"
and for monitoring state visits with Google Analytics.
Finally the constructor ends with:
HttpServletRequest servReq = request.getHttpServletRequest();
String stateName = servReq.getParameter("_escaped_fragment_");
if (stateName != null)
{
if (stateName.startsWith("st="))
stateName = stateName.substring("st=".length(), stateName.length());
else
stateName = "overview";
}
else
{
stateName = servReq.getParameter("st");
if (stateName == null)
stateName = "overview";
}
changeState(stateName);
This demo application is prepared to be crawled by Google Search bots following its
AJAX Crawling Specification,
links ending with #! are also followed by Google bots, in this case the target
site is accessed replacing #! with a parameter _escaped_fragment=
followed by the text following #!. For instance this
link
is crawled by Google bots requesting with this link.
The st parameter is used to load the web site with the specified initial state.
When no st parameter is provided, for instance when loading with this URL,
http://localhost:8080/spitut,
the default state is "overview" as if the "Overview" menu option was selected.
This normal parameter is interesting for non-Google crawlers and to provide navigation with JavaScript disabled.
The method changeState(String) is the responsible of change
management. This method loads the specified fundamental state inserting the
new markup into the content area, changes the appearance of the active menu
option and delegates further state processing to the appropriated SPITutState
class.
The event listener receiving user events is very simple:
public void handleEvent(Event evt)
{
if (evt instanceof ItsNatUserEvent)
{
ItsNatUserEvent itsNatEvt = (ItsNatUserEvent)evt;
String name = (String)itsNatEvt.getExtraParam("name");
changeState(name);
}
}
It just changes the current fundamental state with the selected state when
end user clicks some menu option.
Infrastructure of fundamental states
Now is the time to deep inside the fundamental states being used as examples
in this SPI web site.
Back to the servlet:
itsNatServlet.registerItsNatDocFragmentTemplate("not_found","text/html",
pathFragments + "not_found.xhtml");
itsNatServlet.registerItsNatDocFragmentTemplate("overview","text/html",
pathFragments + "overview.xhtml");
itsNatServlet.registerItsNatDocFragmentTemplate("overview.popup","text/html",
pathFragments + "overview_popup.xhtml");
itsNatServlet.registerItsNatDocFragmentTemplate("detail","text/html",
pathFragments + "detail.xhtml");
itsNatServlet.registerItsNatDocFragmentTemplate("detail.more","text/html",
pathFragments + "detail_more.xhtml");
The fragment "overview" contains the markup in <body> being included
in the content area of the main page when user selects the "Overview" menu option
or the URL loading our web site specifies "overview" as the initial state.
The same for "detail".
The fragment "overview.popup" is going to be inserted into the overview fragment,
the same for "detail.more".
The method SPITutMainDocument.changeState(String stateName) instances
the specified fundamental state, classes of fundamental states inherits from
SPITutState, this class establishes a contract with concrete
inherited state classes.
package org.itsnat.spitut;
import org.itsnat.core.html.ItsNatHTMLDocument;
public abstract class SPITutState
{
protected SPITutMainDocument spiTutDoc;
public SPITutState(SPITutMainDocument spiTutDoc)
{
this.spiTutDoc = spiTutDoc;
spiTutDoc.registerState(this);
}
public SPITutMainDocument getSPITutMainDocument()
{
return spiTutDoc;
}
public ItsNatHTMLDocument getItsNatHTMLDocument()
{
return spiTutDoc.getItsNatHTMLDocument();
}
public abstract void dispose();
public abstract String getStateTitle();
public abstract String getStateName();
}
The call:
spiTutDoc.registerState(this);
is specially interesting, this SPITutMainDocument method sets
the state being created as the default state.
public void registerState(SPITutState state)
{
setStateTitle(state.getStateTitle());
String stateName = state.getStateName();
itsNatDoc.addCodeToSend("spiSite.setURLReference(\"" + stateName + "\");");
googleAnalyticsElem.setAttribute("src",googleAnalyticsIFrameURL + stateName);
}
The call:
setStateTitle(state.getStateTitle());
changes the page title adding the state name.
itsNatDoc.addCodeToSend("spiSite.setURLReference(\"" + stateName + "\");");
This code sends to the client some JavaScript code to specify the fundamental
state being loaded in client. The JavaScript method setURLReference(stateName)
is included into the file spi_bookmarking.js, which is included by
the main template and ever present in our SPI web site. This method changes
the reference part of the URL to save the current state, this change does not
imply a page reload remaining our web site as SPI. When one end user bookmarks
the main page, the URL, containing the current state value in the reference part, is also saved.
When returning to the web site with this bookmarked URL, a script
contained in spi_bookmarking.js is executed on load time adding
the appropriated st parameter to the URL reloading again the main page with
the required initial fundamental state, the same bookmarked state.
The method setURLReference(stateName) also calls detectBackForward().
This method uses a JavaScript timer to keep track of Back/Forward clicks (or browser history
navigation in general) for Back/Forward simulation in SPI. When the Back/Forward buttons are clicked only
the reference part of the URL changes hence no reload happens, this is correct
if you do not want the typical Back/Forward behavior to remain pure SPI. The
method detectBackForward() may be used to provide some Back/Forward
simulation, a JavaScript timer detects when the reference part of the URL has
been changed (usually by Back/Forward) and if no window.spiSite.onBackForward function is registered,
the page automatically is reloaded of
course this behavior is not pure SPI but it may be "acceptable" if end users
reclaim Back/Forward support in your SPI web site.
In our example Back/Forward buttons are supported with no reload because window.spiSite.onBackForward
was registered in main.xhtml with the call:
<script>
function setState(name)
{
...
}
window.spiSite.onBackForward = setState;
</script>
When Back/Forward button is detected, the state change in the URL reference is forwarded to be processed
in server calling setName with the new state name, this method performs the required change delegating to the server by an AJAX call,
and in server state transition is executed accordingly. This way our Single Page Interface remains pure SPI including Back/Forward behavior.
If arbitrary change state transitions are too complex without reloading the page, use the
default approach (page reload) and leaving window.spiSite.onBackForward undefined.
This is the code of spi_bookmarking.js:
function SPISite()
{
this.load = load;
this.detectURLChange = detectURLChange;
this.detectURLChangeCB = detectURLChangeCB;
this.setURLReference = setURLReference;
this.onBackForward = null;
this.firstTime = true;
this.initialURLWithState = null;
this.href = null;
this.disabled = ((navigator.userAgent.indexOf(" IEMobile") != -1) &&
(navigator.userAgent.indexOf("MSIEMobile") == -1));
this.load();
function load()
{
if (this.disabled) return;
var url = window.location.href;
var posR = url.lastIndexOf("#!st=");
if (posR == -1) return;
var state = url.substring(posR + "#!st=".length);
if (state == "") return;
this.initialURLWithState = window.location.href;
}
function setURLReference(stateName)
{
if (this.disabled) return;
var url = window.location.href;
var posR = url.lastIndexOf("#");
if (posR != -1) url = url.substring(0,posR);
url = url + "#!st=" + stateName;
if (url != window.location.href)
window.location.href = url;
this.href = window.location.href;
if (!this.firstTime) return;
this.firstTime = false;
if (this.initialURLWithState != null)
{
window.location.href = this.initialURLWithState;
this.initialURLWithState = null;
this.detectURLChange(1);
}
else this.detectURLChange(200);
}
function detectURLChange(time)
{
var func = function()
{
arguments.callee.spiSite.detectURLChangeCB();
window.setTimeout(arguments.callee,200);
};
func.spiSite = this;
window.setTimeout(func,time);
}
function detectURLChangeCB()
{
var url = window.location.href;
if (this.href == url) return;
var posR = url.lastIndexOf("#!st=");
if (posR == -1) return;
var posR2 = this.href.lastIndexOf("#!st=");
if (posR != posR2) return;
var url2 = url.substring(0,posR);
var spiHref2 = this.href.substring(0,posR);
if (url2 != spiHref2) return;
this.href = url;
var stateName = url.substring(posR + "#!st=".length);
if (this.onBackForward) this.onBackForward(stateName);
else try { window.location.reload(true); }
catch(ex) { window.location = window.location; }
}
}
window.spiSite = new SPISite();
Support of Google Analytics to monitor fundamental states
The last call of SPITutMainDocument.registerState(...) is:
googleAnalyticsElem.setAttribute("src",googleAnalyticsIFrameURL + stateName);
Sets the src attribute of the <iframe> being used for
Google Analytics adding the name of the new fundamental state. For instance:
<iframe id="googleAnalyticsId" itsnat:nocache="true" src="?ganalyt_st=" style="display:none;" ></iframe>
Because the src attribute has been changed the page of the <iframe>,
only containing Google Analytics’s scripts, is reloaded with the new URL.
This way you can monitor how many times (and who and how) end users have been
visiting the fundamental states of your SPI web site.
The parameter ganalyt_st is detected in SPITutGlobalLoadRequestListener
and redirected the request to load the google_analytics template registered
in servlet with the following call:
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("google_analytics","text/html",
pathPages + "google_analytics.xhtml");
docTemplate.setScriptingEnabled(false);
This template (google_analytics.xhtml) only contains scripts of Google
Analytics:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="expires" content="Wed, 1 Dec 1997 03:01:00 GMT" />
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
<title>Google Analytics</title>
</head>
<body style="margin:0; padding:0;">
<!-- <script>alert(window.location);</script> -->
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost +
"google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
var pageTracker = _gat._getTracker("UA-2924757-2");
pageTracker._trackPageview();
</script>
</body>
</html>
This file could be a simple html (or JSP) file outside ItsNat control, but
ItsNat ever adds response headers to disable page caching in browsers (a good
thing to force and ensure script execution).
Note the call:
docTemplate.setScriptingEnabled(false);
This configuration call sets this page template as non-scriptable, ItsNat
does not add JavaScript code to the page for client-server synchronization
purposes hence there is no AJAX, furthermore, the ItsNat document in server
only is created and used in load time then is discarded because this page is
stateless (server point of view).
Fundamental states: overview and overview.showpopup
Concrete fundamental states (inherited from SPITutState) are
SPITutStateOverview, SPITutStateOverviewShowPopup,
SPITutStateDetail.
SPITutStateOverview and SPITutStateDetail are
directly launched by SPITutMainDocument because both are the main
menu options. SPITutStateOverviewShowPopup is very interesting because
in spite of it is a sub-state of SPITutStateOverview we want this state
("Overview showing a popup window") to be a fundamental state, that is bookmarkable,
content reached by search engine crawlers and monitored by Google Analitycs,
so it is inherited from SPITutState because the SPITutMainDocument.registerState(String)
method must be called.
Now we are ready to show SPITutStateOverview:
package org.itsnat.spitut;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLDocument;
public class SPITutStateOverview extends SPITutState implements EventListener
{
protected Element showPopupElem;
protected SPITutStateOverviewShowPopup popup;
public SPITutStateOverview(SPITutMainDocument spiTutDoc,boolean showPopup)
{
super(spiTutDoc);
HTMLDocument doc = getItsNatHTMLDocument().getHTMLDocument();
this.showPopupElem = doc.getElementById("showPopupId");
((EventTarget)showPopupElem).addEventListener("click",this,false);
if (showPopup) showOverviewPopup();
}
public void dispose()
{
if (popup != null) popup.dispose();
((EventTarget)showPopupElem).removeEventListener("click",this,false);
}
public void handleEvent(Event evt)
{
showOverviewPopup();
}
public void showOverviewPopup()
{
((EventTarget)showPopupElem).removeEventListener("click",this,false);
this.popup = new SPITutStateOverviewShowPopup(this);
}
public void onDisposeOverviewPopup()
{
this.popup = null;
((EventTarget)showPopupElem).addEventListener("click",this,false);
spiTutDoc.registerState(this);
}
@Override
public String getStateTitle()
{
return "Overview";
}
@Override
public String getStateName()
{
return "overview";
}
}
To understand what is doing this class we need the overview.xhtml
fragment template, the markup contained in <body> is inserted into the "content area"
of our web site when overview state is selected (the markup in <head> is
not inserted):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Overview</title>
</head>
<body>
<div >
<h2>Overview</h2>
<p>This "fundamental" state is processed by crawlers of search engines (Google, Yahoo, Bing...)</p>
<p>To understant how this SPI web site is "seen" by search engines, disable
JavaScript in your browser.
</p>
<p>You can load a new text like a pop-up window, the link
used to open this pop-up contains a URL specifying as fundamental
state this state with the popup already loaded, this URL is used
by crawlers because "return false" is not executed.
By this way text contained in pop-up is also processed by crawlers.
</p>
<a itsnat:nocache="true" id="showPopupId" href="?st=overview.showpopup" onclick="return false;">Show popup</a>
</div>
</body>
</html>
Because "Overview" is the default state the following picture shows this
state as the initial state our web application:

Pay attention to the link:
<a id="showPopupId" href="?st=overview.showpopup" onclick="return false;">Show popup</a>
This link is dual, AJAX and normal, in this case we directly bind an event
listener in server with the call:
((EventTarget)showPopupElem).addEventListener("click",this,false);
When clicked, the sub-state "overview.showpopup" (also a fundamental state)
is instanced showing a popup modal window with some text.
When this link is reached by web crawlers is also processed trying to load
the linked web site with the initial state "overview.showpopup" because
onclick handler is ignored. In SPITutMainDocument we
know this fundamental state is a sub-state of "overview" so one
SPITutStateOverview object is created with showPopup
parameter set to true:
public void changeState(String stateName)
{
...
if (stateName.equals("overview")||stateName.equals("overview.showpopup"))
{
boolean popup = stateName.equals("overview.showpopup");
this.currentState = new SPITutStateOverview(this,popup);
}
...
}
Then the popup window is automatically shown in load time, and because
ItsNat is in fast-load mode the markup in the popup is rendered and sent to
the client as markup, hence reached by search engine crawlers.
The class SPITutStateOverviewShowPopup loads the fragment with
some markup and insert this markup to the page on top of a "modal layer":
package org.itsnat.spitut;
import org.itsnat.comp.ItsNatComponentManager;
import org.itsnat.comp.layer.ItsNatModalLayer;
import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.itsnat.core.html.ItsNatHTMLDocument;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLBodyElement;
import org.w3c.dom.html.HTMLDocument;
public class SPITutStateOverviewShowPopup extends SPITutState implements EventListener
{
protected SPITutStateOverview parent;
protected Element container;
protected ItsNatModalLayer layer;
public SPITutStateOverviewShowPopup(SPITutStateOverview parent)
{
super(parent.getSPITutMainDocument());
this.parent = parent;
SPITutMainDocument spiTutDoc = parent.getSPITutMainDocument();
ItsNatHTMLDocument itsNatDoc = parent.getItsNatHTMLDocument();
HTMLDocument doc = itsNatDoc.getHTMLDocument();
ItsNatComponentManager compMgr = itsNatDoc.getItsNatComponentManager();
this.layer = compMgr.createItsNatModalLayer(null,false,1,0.5f,"black",null);
HTMLBodyElement body = (HTMLBodyElement)doc.getBody();
DocumentFragment frag = spiTutDoc.loadDocumentFragment("overview.popup");
this.container = ItsNatTreeWalker.getFirstChildElement(frag);
body.appendChild(container);
((EventTarget)container).addEventListener("click", this, false);
itsNatDoc.addCodeToSend("try{ window.scroll(0,-1000); }catch(ex){}");
}
@Override
public String getStateTitle()
{
return "Overview Popup";
}
@Override
public String getStateName()
{
return "overview.showpopup";
}
public void handleEvent(Event evt)
{
dispose();
}
public void dispose()
{
((EventTarget)container).removeEventListener("click",this, false);
container.getParentNode().removeChild(container);
layer.dispose();
parent.onDisposeOverviewPopup();
}
}
This is the template file (overview_popup.xhtml) associated to the name
"overview.popup":
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Overview Popup</title>
</head>
<body>
<div style="position:absolute; z-index:1; background:white; width:80%; height:80%; left:10%; top:5%; padding:30px;">
<h2>Overview Popup</h2>
<p>Overview + popup is also a fundamental state so this text is processed by
search engine crawlers (Google,Yahoo,Bing...), because you can reach this
state on load time with a URL <a href="?st=overview.showpopup">like this</a>
(you can find this text in the end of the page loaded).
</p>
<p><a href="?st=overview" onclick="setState('overview'); return false;"
itsnat:nocache="true">Click to exit</a></p>
<!-- This "useless" link is only needed in Pocket IE because with some ItsNat
help it sends events to parent nodes, and to exit with JavaScript disabled -->
</div>
</body>
</html>
Fundamental state detail and secondary state "More detail"
SPITutStateDetail is another fundamental state and also
contains a sub-state ("More Detail"), however in this case this sub-state
is not fundamental hence there is no new class inherited from SPITutState
and there is no call to SPITutMainDocument.registerState(String)
when this sub-state is reached.
Source code of SPITutStateDetail:
package org.itsnat.spitut;
import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLDocument;
public class SPITutStateDetail extends SPITutState implements EventListener
{
protected Element detailMoreLink;
protected Element detailMoreElem;
protected boolean inserted = false;
public SPITutStateDetail(SPITutMainDocument spiTutDoc)
{
super(spiTutDoc);
HTMLDocument doc = getItsNatHTMLDocument().getHTMLDocument();
this.detailMoreLink = doc.getElementById("detailMoreId");
((EventTarget)detailMoreLink).addEventListener("click",this,false);
}
public void dispose()
{
((EventTarget)detailMoreLink).removeEventListener("click",this,false);
}
@Override
public String getStateTitle()
{
return "Detail";
}
@Override
public String getStateName()
{
return "detail";
}
public void handleEvent(Event evt)
{
if (detailMoreElem == null)
{
DocumentFragment frag = spiTutDoc.loadDocumentFragment("detail.more");
this.detailMoreElem = ItsNatTreeWalker.getFirstChildElement(frag);
}
if (!inserted)
{
Element contentParentElem = spiTutDoc.getContentParentElement();
contentParentElem.appendChild(detailMoreElem);
detailMoreLink.setTextContent("Hide");
this.inserted = true;
}
else
{
detailMoreElem.getParentNode().removeChild(detailMoreElem);
detailMoreLink.setTextContent("More Detail");
this.inserted = false;
}
}
}
And the template fragment detail.xhtml:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Detail</title>
</head>
<body>
<h2>Detail</h2>
<p>This "fundamental" state is processed by crawlers of search engines (Google, Yahoo, Bing...).
The link below is used to load some new text, in this case the new
state is not fundamental (is secondary) and the new text cannot be
reached by crawlers of search engines.
</p>
<a href="javascript:;" id="detailMoreId" >More Detail</a><br />
</body>
</html>
Now the link pure AJAX based and the markup with more info ("More Detail")
only is inserted when end users click the link, therefore new inserted markup
is not reached by web crawlers and there is no call to set this state as
bookmarkable, following The Single Page Interface Manifesto this state
(showing the "More Detail" text) is a "secondary state".
The fragment with name "detail.more" is the file detail_more.xhtml:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>More Detail</title>
</head>
<body>
<span>
<h3>More Detail</h3>
<p>This text cannot be reached by search engines, because there is no
fundamental state registered including this fragment on load time.
</p>
</span>
</body>
</html>
Fundamental state not_found
Finally we have the state "not_found", this state is provided to show a
"state of error" when the main page is being loaded with a state name unknown
(for instance an old bookmark saved before a web site redesign changing state names).
When this "not_found" state is reached the <body> content of not_found.xhtml
is inserted:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>State Not Found</title>
</head>
<body>
<h2>State Not Found</h2>
</body>
</html>
Conclusion
This tutorial has shown one generic example of how to build SPI web sites
with ItsNat similar to page based counterparts without sacrificing the typical
features of the page paradigm like bookmarking, SEO, JavaScript disabled,
Back/Forward buttons (history navigation), visit counters etc.
When a web site is being ported to SPI, pages are usually converted to states,
these states can be fundamental and secondary, this tutorial has shown how
fundamental states are very similar to pages including templating design with
all of benefits of SPI and how we can set as fundamental state virtually any
state thanks to the server-centric nature of ItsNat, The Browser Is The Server
approach and the fast-load feature.
Most of the code in this tutorial is infrastructure code, it could be generalized
and reused in many projects, this is the fixed cost of a SPI web site,
any new fundamental state just require a new plain HTML template
some minimal registration code and a new class inherited from SPITutState,
this class could be empty or be shared (the same class) for all fundamental states with
just static content. In summary, the cost of adding a new fundamental state with
only static content is almost the same as adding a simple HTML file like in page based
development, with the advantage of no care and no repetition of headers, footers and,
in general, content outside of the static content being added avoiding the burden of
repetition in the case of page based development.
Download, Online Demo and Links
See running online
Download source code and binaries
Discussion about this tutorial at JavaLobby
Discussion about this tutorial at JavaHispano (in Spanish)