Friday, December 26, 2008

A study of ESRI Java web adf lifecycle & events - how they fit it in the servlet and JSF lifecycle and events

ESRI has a Java web Application Development Framework (ADF) for ArcGIS Server, similar to Oracle's web ADF and other web frameworks. The ESRI web ADF is an API for building mapping web applications and consists of a set of JSF server controls, e.g. TOC control, Map control, OverviewMap control, Navigation control, PrintTask control etc. 
A very high-level workflow is:
  1. use a full-fledged WYSIWYG desktop editor for creating and symbolizing maps and data, aka ArcMap
  2. publish that map as a web mapping service (MapService) using ArcGIS Server
  3. consume that MapService in a web application created using the ESRI web ADF (either in .net or java)
  4. make changes to the map if necessary and restart MapService. 
  5. The data changes are automatically picked up, e.g. data added/deleted/updated
It is a complicated and sophisticated framework built on top of the JSF framework. 
The JSF components of the ESRI java web ADF are also Ajax enabled.
It exposes its own task framework for building tasks/wizards/workflows on using the various JSF controls it exposes.
Sun outlines a few approaches for creating Ajax-enabled components in JSF. Link,
At ArcGIS Server 9.2, the JSF components are ajax-enabled using the PhaseListener approach. This approach is nicely explained in this pdf slideshow and the below diagram:
The ADF uses a PhaseListener (PostBackPhaseListener.java) to handle all Ajax requests. It us also responsible for rendering xml fragments that contain the update state for each ajax-enabled web adf control. The xml fragments are transformed to html via an xslt using some pre-existing xsl files and the client javascript updates the UI elements on the browser. 
Similar to the doc and case study outlined by Sun, the PhaseListener is triggered by keywords in the HttpRequest QueryString (Request Parameters). The keyword being "doAJAX=true" in this case.
Each ADF JSF component can have 1 or more AjaxRenderers associated with it. The purpose of the AjaxRenderer is to detect changes in the JSF component's state, and render xml fragments of any changes to the state. The way to detect change is flexible, they serialize the variables of the JSF component and compare it with a new serialized state, or custom code can be written.  
Anyways, I've digressed, the ajax implementation of the JSF controls is another topic for another post. 
Today I'd like to talk about something that's very important when developing other phaseListeners, servlets in a web adf application - when do the ADF lifecycle & events get kicked off in relation to the standard jsf phases and servlet lifecycles? 
Web ADF lifecycle:
1) very first request to the Java EE container for the web app:
new WebApplication()
webcontext.Initialization - webcontext instance1
webcontext.Passivation - webcontext instance1
2) 2nd ajax request
webcontext.activation - webcontext instance1
webcontext.passivation - webcontext instance1
3) 1st request from a new client
webcontext.initialization - webcontext instance2
webcontext.passivation - webcontext instance2
4)  2nd ajax request from the 2nd client
webcontext.activation - webcontext instance2
webcontext.passivation - webcontext instance2
5) final request from 2nd client
webcontext.activation  - webcontext instance2
webcontext.passivation - webcontext instance2
webcontext.destruction - webcontext instance2
6) final request from 1st client
webcontext.activation - webcontext instance1
webcontext.passivation - webcontext instance1
webcontext.destruction - webcontext instance1
7) Web Application is removed or Java EE container is shut down
WebApplication.destroy()
There are 6 JSF phases and the ADF phases exist in between those.
Servlet lifecycle:
ContextInitialized - creates an ADF WebApplication class instance (1 per web application, exists in application scope)
ADFServletContextListener
ContextDestroyed - destroys an ADF WebApplication class instance (1 per web application, exists in application scope)
JSF lifecycle:
RESTORE_VIEW:
beforePhase: ADF webContext.activate()  - there is no predefined phase for this, it can happen as soon as possible
APPLY_REQUEST_VALUES:
PostBackPhaseListener is kicked off if an ajax request is made- calls all AjaxRenderers to gather xml fragments of changed JSF web control states. 
RENDER_RESPONSE:
afterPhase: ADF webContext.passivate() - this event can happen before render_response or before response_complete (since response_complete may happen before render_response), basically this is the last event of the web adf lifecycle for an http request (once the webcontext has been initialized).
RESPONSE_COMPLETE event:
ADF webContext.passivate
The below is a java class that will show you when the various lifecycle/events of the ESRI java web ADF are being executed
package com.ncedis.adf.web.common;

import java.util.Iterator;
import java.util.Map;

import org.apache.log4j.Logger;

import com.esri.adf.web.data.WebContext;
import com.esri.adf.web.data.WebContextInitialize;
import com.esri.adf.web.data.WebContextObserver;
import com.esri.adf.web.data.WebLifecycle;

public class WebADFLifecycleNotifier implements WebContextObserver, WebContextInitialize, WebLifecycle
{
 private static final long serialVersionUID = -5663664360161434104L;
 private static final Logger logger = Logger.getLogger(WebADFLifecycleNotifier.class);
 
 private boolean isStartup = true;
 private WebContext webContext;

 public void update(WebContext webContext, Object obj)
 {
  logger.debug("WebContext.refresh() has been invoked, which invoked me(WebContextObserver) ...");
  if (isStartup)
  {  }
  this.webContext = webContext; 
  isStartup = false;
 }
 
 
 public static String arrayToString(Object[] array)
 {
  if (array == null)
   return null;
  
  StringBuilder sb = new StringBuilder();
  for (int i = 0; i < array.length; i++)
  {
   sb.append("\t"+array[i]+"\n");
  }
  return sb.toString();
 } 
 
 public void destroy()
 {
  logger.debug("WebContext.destory() has been invoked, which invoked me ... [Terminating Client session]\n");
  logger.debug("Printing stack trace [NOT ERROR]-\n" + arrayToString(Thread.currentThread().getStackTrace()));  
  isStartup = true;
  webContext = null;
 }

 public void init(WebContext webContext)
 {
  logger.debug("WebContext.init() has been invoked, which invoked me ... [Received new Client Session Request]");
  webContext.addObserver(this);
  this.webContext = webContext;
  
  if (logger.isDebugEnabled())
  {
   try
   {
    StringBuffer sb = new StringBuffer("NONE");
    Map attribs = webContext.getAttributes();
    if (!attribs.isEmpty())
    { 
     sb = new StringBuffer("[");
     for (Iterator iter = attribs.entrySet().iterator(); iter.hasNext();)
     {
      Map.Entry element = (Map.Entry) iter.next();
      sb.append(element.getKey()).append(", ");   
     }
     sb.replace(sb.length()-1, sb.length(), "]");
    }
    logger.debug("WebContext.attributes = " + sb.toString());
   } 
   catch (Exception ex) 
   {
    logger.warn("Unable to print webContext.attributes - " + ex.getMessage() + " - " + ex.getStackTrace()[1]);
   }
  }
  
  /**
   * NOTE IMPORTANT: some managed-beans could be instantiated at this stage, 
   * but their properties are set later during the webapp execution.
   * 
   */
 }
 
 public void activate()
 {
  logger.debug("WebContext.activate() has been invoked, which invoked me ... [Client http-request is received]"); 
  isStartup = false;
 }

 public void passivate()
 {
  logger.debug("WebContext.passivate() has been invoked, which invoked me ... [Done with servicing HTTP-request] \n");
  isStartup = false;
 }
}
To better understand the web adf lifecycle with respect to the adf events/servlet events, we should create a PhaseListener class that outputs a message before and after every phase. This class should be as follows:
package com.ncedis.adf.web.faces;

import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;

import org.apache.log4j.Logger;

public class VerboseTestPhaseListener implements PhaseListener
{
 private static final long serialVersionUID = 131479863251034948L;
 private static final Logger logger = Logger.getLogger(VerboseTestPhaseListener.class);

 public void afterPhase(PhaseEvent event)
 {
   logger.debug(event.getPhaseId().toString() + " afterPhase");   
   if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) 
    logger.debug("Done with Request!\n"); 
   
 }

 public void beforePhase(PhaseEvent event)
 {
  //this is actually not the beginning of the new request.
  //if (event.getPhaseId() == PhaseId.RESTORE_VIEW) logger.debug("Processing new Request!");          
    logger.debug(event.getPhaseId().toString() + " beforePhase");
 }

 public PhaseId getPhaseId()
 {
  return PhaseId.ANY_PHASE;
 }
 
}
The above java class must be instantiated by the jsf managed-bean facility and inserted as an attribute of webcontext using the dependency injection framework of jsf. In simpler words, you'll have to modify the faces-config.xml file and declaratively add this class as an attribute of the webcontext instance as follows below:

No comments:

Post a Comment