Monday, August 22, 2005

Extend the ArcGIS ADF with POJOs - Part II

In Part I we discussed how you could implement GIS functionalities in POJOs and plug them into the ADF. In this part we'll extend the POJO a little further.

In Part I, the CountFeatures object calculated and updated the feature count on a client interaction such as a button click. Suppose that we now need for this object to recalculate the count automatically whenever the current extent of the map changes or the map refreshes due to some other action.

The ADF provides a very simple way to do this. Objects can register themselves as observers of the WebContext and whenever the context is refreshed (by virtue of the user calling the refresh() method on the context), all observers will be intimated of this event and each observer can act upon it individually. This way we have loosely coupled objects reacting together to the context refresh.

With this background let's now extend our CountFeatures class to implement this behavior.


public class CountFeatures implements WebContextInitialize, WebContextObserver {

public void init(WebContext context) {
...
context.addObserver(this);
}

public void update(WebContext context, Object arg) {
doCount(); //perform the business action on update
}
...
}

First up, all observers of the WebContext need to implement the WebContextObserver interface. Next, they register themselves as observers of the context by calling the addObserver() method on the context. Finally, on every context refresh, the update() method of the WebContextObserver interface is called by the ADF and the object reacts (performs the business action) to the same. In this case we simply call the doCount() method which recalculates the feature count of the updated map. This will ensure that whenever the context refreshes (for example when the user zooms or pans), this object will recalculate and display the new count to the user.

As simple as that. Apart from a few modifications to the Java code, nothing else needs to change from the Part I source code. The JSP as well as the configuration files remain unchanged. You can download all the source code (including the unchanged JSP) for this part from here.

In Part III we'll extend CountFeatures further by implementing the WebLifecycle interface.

Tuesday, August 9, 2005

Extend the ArcGIS ADF with POJOs - Part I

This is the first of a 3 part series where we'll discuss how to add custom GIS functionality to the ADF as POJOs (Plain Old Java Objects). To accomplish this we'll be leveraging the IOC inherent in the ADF discussed earlier. We talked about 3 very important interfaces in the IOC discussion - WebContextIntialize, WebContextObserver and WebLifecycle. Putting these 3 interfaces in practice will be central to the 3 parts respectively. In this part we'll make use of the WebContextInitialize interface.

We'll keep the functionality to be implemented quite simple: Count the number of features of a given layer in the map's current extent.

To implement this scenario our POJO will need a few basic properties and methods - a read-only count property, a read/write layerId property representing the layer whose features are to be counted and a business method doCount() which implements the business task at hand. With this said, the skeleton of the class (we'll call it CountFeatures) will be as such:


public class CountFeatures {

//properties
int count;
int layerId;
public int getCount() { return count; }
public int getLayerId() { return layerId; }
public void setLayerId(int layerId) { this.layerId = layerId; }

//business method
public String doCount() {
...
...
count = ...;
return null;
}
}

You might have noticed that the doCount() method returns a String. This is because the ADF is JSF based and when the user clicks on say a command button on a web page, it results in a call to doCount(). Based on the return value of this method the JSF framework decides which page to navigate to. Returning a null ensures that the webapp stays on the same page.

OK, so now we have the skeleton in place but we also need access to the ArcGIS Server and the underlying ArcObjects to perform the GIS task at hand. This is where the WebContextIntialize comes into the picture:

public class CountFeatures implements WebContextInitialize {

//the context associated with this object
AGSWebContext agsctx;
public void init(WebContext context) {
agsctx = (AGSWebContext)context;
}
...
}

The ADF will call the init(WebContext) method of objects implementing WebContextInitialize immediately after the object is instantiated. This gives the object access to the WebContext. The AGSWebContext (which is the actual implementation of the WebContext that we work with) maintains references to the ArcGIS Server objects and ArcObjects (such as IMapServer, IMapDescription, etc.) as well as to other ADF objects (such as AGSWebMap). This implies that by virtue of gaining access to the AGSWebContext our custom object now has a hook into the whole of ArcObjects as well as the ADF - basically everything that you need to accomplish your GIS task at hand.

With access to everything that our class needs, the business logic can now be implemented in the doCount() method to perform the count operation and set the result to the count variable.

That's it - our Java code ends here. All that is left to do now is to register this object as a managed attribute of the WebContext so that the ADF can automatically instantiate the object on demand as well as call the init(WebContext) method immediately after instantiation. This is accomplished by adding the following lines of XML to managed_context_attributes.xml which you can find in the /WEB-INF/classes folder of your ADF webapp:

<managed-context-attribute>
<name>countFeatures</name>
<attribute-class>custom.CountFeatures</attribute-class>
<description>counts features of a given layer in the current extent...</description>
</managed-context-attribute>

With this done, you can now access our custom object by name (countFeatures in this case).

You can download the full source here. In addition to the Java code, the ZIP file also contains a sample JSP. The JSP has a command button to trigger the business method, a dropdown to choose the layer and a text out to display the count.

In conclusion I'd like to mention that while admittedly the functionality that we have implemented here is trivial, you can essentially follow the same programming model to implement your own functionality as well: POJOs which implement WebContextInitialize

In Part II we'll extend this same object to be an observer of the context and in Part III we'll make this object participate in the ADF lifecycle.

Sunday, August 7, 2005

Inversion of Control in the ArcGIS Java ADF

Martin Fowler in a recent blog gave a good short explanation of the inversion of control pattern... In ESRI's ArcGIS Java ADF we employ this approach at a few places.


  1. The WebContextInitialize interface declares an init(WebContext) method. The ADF will call this method on objects which implement this interface and register themselves as attributes of the WebContext. This method will be called immediately after they are registered with the WebContext. Users interested in getting access to the associated WebContext object or want to do some initialization tasks should implement this interface.

  2. The WebLifecycle interface declares methods which will be called by the ADF at various phases of the webapp's lifecycle. Users can implement activation, passivation and destroy logic in these methods. This interface is most relevant when using pooled objects since users may want to rehydrate and dehydrate the states of the server objects when the ADF reconnects and releases its connection to the ArcGIS server on every request.

  3. The WebContextObserver interface declares an update(WebContext webContext, Object args) method. Objects implementing this interface can register themselves as observers of the WebContext by calling the addObserver(WebContextObserver) method. After users perform operations which change the state of the objects that they work with (for example zoom to a different extent, add a graphic element, etc.), they call the refresh() method on the WebContext. When this happens, the ADF iterates thru all the registered observers of the context and calls their update() methods. This ensures loose coupling among the various objects but at the same time gives these loosely coupled objects an opportunity to be in sync with the changed state of the app. This is a classic implementation of the observer pattern with the WebContext acting as the Observable object.


With the advent of JDK 5 annotations, it might be convenient for the users if #1 could be achieved by simply annotating a field or a setter method with an @Resource like annotation. The ADF on encountering this annotation on a WebContext field or setter method could inject the same into the interested object. Further, users can do the initialization tasks in any arbitrary method annotated with the @InjectionComplete annotation and the ADF will call this method immediately after injecting the WebContext. (Both these annotations are proposed by JSR 250 - Common Annotations).

#2 could also be achieved through annotations. Much like how EJB3 is proposing @PostActivate, @PrePassivate, et al; users can annotate the lifecycle methods with annotations such as @OnActivate, @OnPassivate and @OnDestroy. This would alleviate them of having to implement interfaces for lifecycle callbacks and further, they can choose to participate in only those phases of the ADF lifecycle which makes business sense for their objects.

Comments / feedbacks welcome.

Tuesday, August 2, 2005

Adding layers dynamically in the ArcGIS Java ADF

There have been many questions about adding layers dynamically in the ADF... And of course the requirement is that the added layer will reflect not only on the map but also on the TOC, layer drop downs, etc...

When working with non-pooled objects, there's a straightforward way of doing this. Look at the source code below:


AGSWebContext agsCtx = ...; //get hold of the AGSWebContext
AGSWebMap agsMap = (AGSWebMap)agsCtx.getWebMap();

//Step 1
agsCtx.applyDescriptions();

//Step 2
MapServer mapso = new MapServer(agsCtx.getServer());
IMap map = mapso.getMap(agsMap.getFocusMapName());
ILayer layer = ...; //create the layer
map.addLayer(layer);

//Step 3
agsCtx.reloadDescriptions();

Let's discuss the 3 steps now:

  • Step 1: Before making stateful changes to the object graph (like adding a new layer in this case) you want to apply the current state of the MapDescriptions to the object graph. Calling the applyDescriptions() method on the AGSWebContext does exactly that.

  • Step 2: Now that you have the object graph in the current state, you can make the modifications there. In this case we add a new layer to the map.

  • Step 3: Once you have made changes to the object graph you want to reload the MapDescriptions to reflect the changed state and additionally, you also want the web controls to display this new state (in this case display the layer on the map control, on the TOC control as well as on any other components working with layers). A single call to the reloadDescriptions() method on the AGSWebContext will do all of this for you.


And that's about it! This sequence of steps holds true for any stateful changes that you want to make to non-pooled objects. The 3-word mantra is APPLY-CHANGE-RELOAD.

If you wanted to work with dynamic layers (or make any stateful changes) in the pooled context, there's indeed more work to do because you are now sharing the server object with others and you want to return the object back to the pool in the same state that you had received it. You need to get access to the server object, apply the current MapDescriptions to the object graph, make the changes to the object graph, reflect the changes in the MapDescriptions and the web controls, undo the changes to the graph and then return the object back to the pool. You can check out the dynamic layers sample on EDN to see this use case in action.