Ajax and Partial-Page Refresh in Oracle ADF Rich Client, Part 2

Get an overview of the support for Ajax and PPR in the Oracle Application Development Framework (Oracle ADF) Faces rich client (RC) constituent of Oracle ADF 11g

By Lucas Jellema Oracle ACE Director and Chris Muir Oracle ACE Director

Published July 2009

Part 1 of this article provided an introduction to the partial-page refresh (PPR) mechanism in the Oracle Application Development Framework (Oracle ADF) Faces rich client (RC) constituent of Oracle ADF 11g  and focused on the straightforward, largely declarative application of PPR. This second part takes you to a more advanced level of client/server integration using programmatic facilities such as the Oracle ADF Faces RC client-side (JavaScript) API. It describes common use cases such as autosuggest, context-sensitive pop-ups, poll-based autorefresh, and integration of Google Maps.

Prerequisites and Preparation

This article pertains to Oracle ADF 11g and Oracle JDeveloper 11g, the production release currently available for download on Oracle Technology Network.

The examples discussed in this article are all available in an Oracle JDeveloper 11g application that can be downloaded here. Note that no database is required for running this application.


The S.H.O.P. Case

The case that provides the context for the examples presented in this article is the Second Hand Opportunities Plaza (or S.H.O.P. for short), an eBay-like Web community site where people can do their online garage sales—posting advertisements for items they want to part with and hope to find a new owner for—preferably making some money in the bargain (see Figure 1).

Figure 1 S.H.O.P. logo

Implementing a S.H.O.P.-ping Basket

Oracle ADF Faces RC enables us to register various types of event listeners on many components. For example, on a table, we can register listeners for events such as row selection, range change, row disclosure, and sort. When the event occurs, a PPR call is made to the registered listener method in a server-side managed bean. This listener method can process the event, using the event payload, and then programmatically instruct the framework to refresh specific components in the client.

We will now add a shopping basket to the page with offered products. Clicking a product in the list is interpreted as an “add to shopping basket” request. Clicking the product calls a row selection event listener. This listener adds the selected product to the shopping basket collection and then makes the shopping list refresh. The following example demonstrates a mechanism in which the PPR cycle is not initiated from an input component with autoSubmit set to true or a command component with partialSubmit set to true; instead, the framework itself uses PPR to inform server-side listeners of client-side events.

The selectionEventListener on the table with the product advertisement is easy to register:

< af:table id="prodlistTable"                value="#{postingsManager.productPostings}" var="row"                                        width="922" filterVisible="false"                                        rowSelection="multiple"                                        filterModel="#{postingsManager.filterModel}"                                        selectionListener="#{postingsManager.productSelectionListener}">
                              


The productSelectionListener method in the postingsManager bean determines which products have been selected in this event—note that with Shift-click the user can select multiple rows at once. These products are then passed to the shoppingBasket bean, which adds them to the basket if they are not already there. selectionEvent passed to selectListener contains the set of row keys for the rows that are added in the selection event. These row keys can be used against rowData in the table component to retrieve the ProductPosting objects.

When all products have been passed to the shoppingBasket bean, one last important step remains: the shopping list on the page needs to be refreshed to show the new contents. In the first part of this series, we used the partialTriggers attribute in conjunction with the autoSubmitting component’s ID to tell Oracle ADF Faces RC about the refresh dependencies. However, this time we have no autoSubmitting component as such. We use a programmatic solution indicating the components that need to be partially refreshed. The shopping basket panelList is tied to the postingsManager bean through its bindings attribute:

< af:panelList id="shoppingBasketPL"

               binding="#{postingsManager.shoppingBasketPanel}" >


This binding makes the panelList component available in the bean and in productSelectionListener. Using a call such as AdfFacesContext.getCurrentInstance().addPartialTarget(component), we can add partial targets—components to be refreshed at the end of the PPR cycle.

  public void productSelectionListener(SelectionEvent selectionEvent) {

    RichTable table = (RichTable)selectionEvent.getSource();
        
    RowKeySetImpl selectedRowKey =
        
      (RowKeySetImpl)selectionEvent.getAddedSet();
          
    Iterator keys = selectedRowKey.iterator();
        
    while (keys.hasNext()) {
        
      Integer k = (Integer)(keys.next());
          
      ProductPosting selectedProduct = (ProductPosting)table.getRowData(k);
          

      shoppingBasket.addItem(selectedProduct);
      
    }
        
    AdfFacesContext adfFacesContext = AdfFacesContext.getCurrentInstance();
        
    adfFacesContext.addPartialTarget(this.shoppingBasketPanel);
 }


panelList itself contains an iterator over the shoppingBasket bean’s list of items. Each item’s name is rendered in a bullet list:
< af:panelList id="shoppingBasketPL"

        binding="#{postingsManager.shoppingBasketPanel}">

   < af:iterator id="shoppingBasketIter"

         value="#{shoppingBasket.items}" var="item"

         varStatus="index">

    < af:outputText id="basketitemLI" value="#{item.name}"/>

   < /af:iterator>

 < /af:panelList>

Finally the shoppingBasket bean is based on the almost trivial [ ShoppingBasket class:
public class ShoppingBasket {


  private List < ProductPosting> items = new ArrayList();
  
  public ShoppingBasket() {
  
  }

  public void setItems(List< ProductPosting> items) {
  
    this.items = items;
        
  }

  public List< ProductPosting> getItems() {
  
    return items;
        
  }
  
  void addItem(ProductPosting selectedProduct) {
  
    if (!items.contains(selectedProduct)) {
        
     items.add(selectedProduct);
         
    }
         
  }
  
}


When we run the page, the shopping basket is initially empty, as shown in Figure 2:

Figure 2 Empty shopping basket



Clicking rows in the table adds products to the shopping basket (see Figure 3):


Figure 3 Shopping basket containing some products - including three just added upon table row selection

We have not yet provided a way of removing items from the shopping basket. Our profits would soar without this capability, but our customers would complain, so we’ll add it. To do this, we’ll add a commandLink to the items in the shoppingBasket. The link has its actionListener referring to the removeItem() method in our shoppingBasket bean. Of course, we use PPR to smoothly refresh the shopping basket when an item has been removed. The panelList refers to the remove link in its partialTriggers attribute, and partialSubmit on the link is set to true.

< af:panelList id="shoppingBasketPL"

        partialTriggers="shoppingBasketIter:removeLink"
                
        binding="#{postingsManager.shoppingBasketPanel}">
                
   < af:iterator id="shoppingBasketIter"
   
         value="#{shoppingBasket.items}" var="item"
                 
         varStatus="index">
                 
   < af:panelGroupLayout id="basketItemPG" layout="horizontal">
   
    < af:outputText id="basketitemLI" value="#{item.name}"/>
        
    < af:spacer width="15"/>
        
    < af:commandLink id="removeLink"
        
            actionListener="#{shoppingBasket.removeItem}"
                        
            partialSubmit="true">
                        
     < af:image id="removeFromSetImage"
         
          source="/images/removeFromSet.png"
                  
          shortDesc="Remove from Shopping Basket"/>
                  
     < af:setPropertyListener from="#{item}"
         
                 to="#{shoppingBasket.itemToRemove}"
                                 
                 type="action"/>
                                 
    < /af:commandLink>
        
   < /af:panelGroupLayout>
   
  < /af:iterator>
  
 < /af:panelList>


The server-side action listener is the straightforward removeItem() method in the shoppingBasket bean. The interesting bit here is a new component, setPropertyListener. This component can be associated with an event on its parent component—in this case, the action event on the commandLink. When that event occurs, the setPropertyListener is invoked in a PPR request to transfer a value—or set a property—prior to the actionListener’s execution.

In this example, we specify #{item} in the from attribute; this uses the item object used for rendering the commandLink as the source for the data transfer. The target attribute is set to #{shoppingBasket.itemToRemove}, which makes the setItemToRemove() method on the shoppingBasket bean the target for the set property operation. When commandLink is clicked and just prior to the call to removeItem(), the setItemToRemove() method is called with the item (ProductPosting) associated with the activated commandLink.

public class ShoppingBasket {

  private List< ProductPosting> items = new ArrayList();
  
  private ProductPosting itemToRemove;
  
  public ShoppingBasket() {
  
  }
  
  public List< ProductPosting> getItems() {
  
    return items;
  }
  
  void addItem(ProductPosting selectedProduct) {
  
    if (!items.contains(selectedProduct)) {
        
     items.add(selectedProduct);
         
    } 
        
  }
  
  public void removeItem(ActionEvent actionEvent) {
  
    if (itemToRemove!=null) {
        
      items.remove(itemToRemove);   
             
    }
  }
  
  public void setItemToRemove(ProductPosting itemToRemove) {
  
    this.itemToRemove = itemToRemove;
        
  }
}


The shopping basket with remove icons is shown in Figure 4.

Figure 4 Shopping basket with remove icons

Adding a Context-Sensitive Pop-up to the Shopping Basket

The shopping basket lists the names of the products we are interested in, but no more than that. From the list itself, we cannot learn the price, age, or description of the posted products. It would be useful for our end users if we could provide them with a pop-up with product-specific details that appears when the cursor is hovering over an item in the shopping basket.

Adding a showPopupBehavior child element to the panelList’s outputText component that writes out the items in the shopping basket, we can configure a pop-up to be shown when the cursor is hovering over the item. The pop-up’s ID is prodDetailsPopup. We will create this pop-up somewhat later.

We would also like to make some additional product details available to the pop-up when it appears. A challenge is figuring out how to determine which details to show. If we, for example, include a reference such as #{item.name} in the pop-up, it will be unclear which item it’s referring to. In fact, the #{item} reference is valid only inside the iterator component, so the expression #{item.name} is not allowed in the pop-up. What, then, can we refer to in the pop-up? One straightforward way of making values available on the client during events such as launchPopup is to use

clientAttributes. With a clientAttribute child component, we can associate a custom attribute with the outputText component. These attributes and their values are available when an event is triggered on the outputText component. The EL expression #{source.attributes.attributeName} gives us the value of any attribute, including clientAttributes, for the component indicated with source. In JavaScript, we can refer to clientAttribute values with event.getSource().getProperty(attributeName).

Our first step toward making the item details available in the pop-up is to add them as clientAttributes to outputText:

  < af:outputText id="basketitemLI" value="#{item.name}">
  
   < af:showPopupBehavior triggerType="mouseHover"
   
              popupId="::prodDetailsPopup"
                          
              align="afterEnd"
                          
              alignId="basketitemLI"/>
                          
   < af:clientAttribute name="productName"
   
             value="#{item.name}"/>
                         
   < af:clientAttribute name="productDescription"
   
             value="#{item.description}"/>
                         
   < af:clientAttribute name="productPrice"
   
             value="#{item.price}"/>
                         
   < af:clientAttribute name="productAge"
   
             value="#{item.age}"/>
                         
  < /af:outputText>


When the cursor is hovering over a product in the shopping basket, showPopupBehavior kicks in and launches the pop-up. The pop-up component has setPropertyListener children that are activated on the popupFetch event. This event occurs whenever the pop-up needs to fetch its contents, which, in this case, is every time the pop-up is launched. The setPropertyListener components read the values of clientAttributes on the shopping basket item that was the source of the launch pop-up event and adds them as parameters to the request.

< af:setPropertyListener from="#{source.attributes.productName}"

                 to="#{requestScope.productName}"
                                 
                 type="popupFetch"/> 

   

Note how the expression #{source.attributes.attributeName} is used to retrieve the values of clientAttributes. The source part refers to launcherVar, a reference to the outputText component whose showPopupBehavior child triggered the pop-up.

The pop-up component’s code is shown below. The contents of the pop-up will be fetched every time it is opened. This is configured through the contentDelivery attribute, which is set to lazyUncached. The contents of the outputText components in the pop-up are retrieved from the requestScope parameters created in setPropertyListeners in the PPR request that is triggered by popupFetchEvent.
   < af:popup id="prodDetailsPopup" contentDelivery="lazyUncached"
 
         eventContext="launcher" launcherVar="source">
                 
     < af:setPropertyListener from="#{source.attributes.productName}"
         
                 to="#{requestScope.productName}"
                                 
                 type="popupFetch"/>
                                 
     < af:setPropertyListener from="#{source.attributes.productDescription}"
         
                 to="#{requestScope.productDescription}"
                                 
                 type="popupFetch"/>
                                 
     < af:setPropertyListener from="#{source.attributes.productPrice}"
         
                 to="#{requestScope.productPrice}"
                                 
                 type="popupFetch"/>
                                 
     < af:setPropertyListener from="#{source.attributes.productAge}"
         
                 to="#{requestScope.productAge}"
                                 
                 type="popupFetch"/>
                                 
     < af:panelWindow id="productDetailsPW"
         
             title="Product Posting Details for #{requestScope.productName}">
                         
      < af:panelList id="prodDetailsPopupList">
          
       < af:outputText id="prodDescPopup"
           
               value="#{requestScope.productDescription}"/>
                           
       < af:outputText id="prodPricePopup"
           
               value="#{requestScope.productPrice}"/>
                           
       < af:outputText id="prodAgePopup"
           
               value="#{requestScope.productAge} years old"/>
                           
      < /af:panelList>
          
     < /af:panelWindow>
         
    < /af:popup>


Running the page shows us the shopping basket with a little pop-up appearing when the cursor hovers over an item in the basket (see Figure 5):


Figure 5 Product details pop-up

Run the ProductAdvertismentsListWithBasket.jspx page in the sample application for this article to see the functionality described here in action.

The Poll Component for an Active User Interface with Periodic Autorefresh

Bids can be placed on the product postings on S.H.O.P. These bids can come from anywhere and can be placed at any moment. To get an overview of the most-recent bids, the user normally would have to perform an action to make the list of bids refresh. Using the poll component, we can make Oracle ADF Faces RC refresh specific components every time a period of the indicated length has expired.

In our S.H.O.P. application, we have a BidManager class that exposes a method that returns the latest bids that have been collected from all users and all sessions. To make our life a little bit more interesting, we have implemented a bid generator—a method that, when invoked, will create random bids within certain boundaries.

  public class BidManager {

  private PostingsManager postingsManager;
  
  private List< Bid> bids = new ArrayList < Bid>();
  

  public void generateBids() {
  
    for (ProductPosting posting : postingsManager.getProductPostings()) {
        
      // 30% chance that we generate a bid for this posting
          
      if (Math.random() < 0.3) {
          
        Bid bid = new Bid();
                
        bid.setProductPosting(posting);
                
        short stateIndex =
                
        (short)Math.round(Math.random() * (StateManager.getStatesList().size() - 1));

        bid.setState(StateManager.getStatesList().get(stateIndex));
                
        bid.setOffer((float)(posting.getPrice() * (0.4 + 0.7 * Math.random())));
                
        bid.setTimestamp(new Date());
                
        bids.add(bid);
                
      }
          
    }
        
  }
    public void pollEventListener(PollEvent pollEvent) {
        
        generateBids();
    }



  }


We next create a page that lists the most-recent bids and add a poll component to autorefresh this page every few seconds. This active page will present the user with the latest bids, without requiring the user to perform any action.
   < af:panelHeader id="bidsPH" text="Most recent  bids" size="-1">
 
     < af:table value="#{ bidManager.bids}" var="row"
         
          partialTriggers="::bidpoller" width="994"
                  
          inlineStyle="height:466px;" fetchSize="50">
                  
      < af:column sortable="true" headerText="Product Posting"
          
            align="start" sortProperty="productPosting.name">
                        
       < af:outputText value="#{row.productPosting.name}"/>
           
      < /af:column>
          
      < af:column sortable="false" headerText="Offer" align="start">
          
       < af:outputText value="#{row.offer}">
           
        < af:convertNumber currencyCode="USD" type="currency"
                
                 maxFractionDigits="0" groupingUsed="true"
                                 
                 integerOnly="true"/>
                                 
       < /af:outputText>
           
      < /af:column>
          
      ...
          
     < /af:table>
         
    < /af:panelHeader>
        
        < af:poll id="bidpoller" interval="15000" pollListener="#{bidManager.pollEventListener}" />


The poll component fires every 15,000 milliseconds. When it does so, the pollEventListener method in the bidManager bean is invoked. This method does nothing terribly useful, except call the generateBids() method, which randomly creates some new bids on the products. The table has a partialTriggers attribute that contains the value bidpoller, the ID of the poll component. That ensures that the table is refreshed every time the poll event occurs (see Figure 6).

Figure 6 The bid monitor at some point in time with the most recent bids

When the poll fires, the page briefly flashes the message shown in Figure 7:

Figure 7 The Bid monitor right after the poll fired with the PPR underway



And when the new list of bids is available, it is presented in the browser, as shown in Figure 8:

Figure 8 The updated list of bids after the POLL triggered PPR completes

Note: this particular use case can be implemented in an even more advanced way with the Active Data Source feature. That topic is beyond this article’s scope.

Run the BidMonitor.jspx page in the sample application for this article to see the poll-driven refresh in action.

Implementing Autosuggest

Autosuggest is considered to be the mother of all Ajax features. Google Auto Suggest introduced Ajax to the masses and is frequently cited as a prime example. The autosuggest functionality consists of an input item where, as the user starts typing in characters, a list of suggested values is presented, from which the user can select one. Entering values on a Web page can be made much more efficient through appropriate use of autosuggest.

In the S.H.O.P. application, we will add the notion of location to the product advertisement: the physical location where a product is being offered. The advertisement will include the (optional) address—street address, zip code, and city—as well as the (required) state. We will implement autosuggest on the State field. When the user starts typing, a list appears that contains no more than five state/territory names that start with the typed-in string and are ranked alphabetically after it (see Figure 9).

Figure 9 Autosuggest in action in the state field

For this functionality to work, we need some additional PPR mechanisms from Oracle ADF Faces RC.

Implementation of autosuggest requires a response to each keyUp event and updating of a list of suggested values. We will use the clientListener and serverListener components, which enable us to start background communication from client to server. A clientListener component is a component we can add inside other components, such as input components, and configure to invoke a client-side (JavaScript) function when a specific type of client event occurs on the parent component. For the autosuggest functionality, we will add a clientListener component to an inputText component to listen for keyUp events—a sign that the value in the field has changed by one character and the list of suggestions should be refreshed.

A serverListener component is a component that is defined in the JavaServer Faces (JSF) page but really does not describe anything on the client side. A serverListener component specifies a server-side method in a managed bean that should be invoked by the Oracle ADF framework when a certain event is reported from the client. It takes a bit of client (JavaScript) code to queue an event that is then propagated all the way to the managed bean method by Oracle ADF. The JavaScript method invoked by the clientListener will queue a custom event that is sent asynchronously or in the background to the serverListener, which will prepare the updated list of suggested values and then instruct the framework to refresh the suggestions list component. For this last step, we programmatically specify the partial-refresh targets by adding component references to the AdfFacesContext object.

The implementation of autosuggest will be somewhat basic: we present a list of state names in a list shown below the State field. Whenever the user types in a character or removes one, the list is refreshed accordingly. The user can pick an entry in the list at any time, and the value that is selected is copied into the State field.

The steps are as follows:

  • Create a StateManager class, which publishes a getStates()method that returns a list of state names.
  • Configure a new managed bean called stateManager based on this class.
  • Surround the State field with a PanelLabelAndMessage component. To this container, add a SelectOneListBox component that shows the list of states returned by the StateManager.
  • Add a clientListener to the State field that responds to the keyUp event (whenever a character is typed into or removed from the value for the State field); the clientListener component should call a JavaScript function that queues a custom event to be reported back to the server.
  • Add the JavaScript handleStateChange() function, which queues the stateChange custom event.
  • Add the serverListener component that listens for the stateChange event and invokes a server-side method in the stateManager bean.
  • Implement a method in the StateManager class that receives the stateChange event, which  contains the value currently entered in the State field and uses it to filter the list of states. This method then causes the list box with states to refresh.

In the following code example, the stateIT inputText is wrapped deep inside a panelLabelAndMessage component. Inside this container, panelGroupLayout vertically arranges its children: the stateIT inputText child and the stateSuggestions selectOneListbox child. The latter—obviously—is used to present the suggested state names. These names are provided by the getStatesSelectItems() method on the stateManager bean. A user selection of one of the suggested state values in the listbox triggers the clientListener component—the selection will fire the valueChange event—and calls the JavaScript function acceptStateSuggestion(), which copies the current value in the listbox to the stateIT inputText field. Note that this is a strictly client-side operation.

The real autosuggest functionality starts with the clientListener on the stateIT component. This listener is triggered by the keyUp event that fires whenever the user releases a key  on the keyboard and the value in the State field  has probably changed; the client-side handleStateChange() method is called as a result. It queues a custom event of type stateChange that is sent to the server (side) listener implemented by the filterStatesOnState() method in the stateManager bean. Note that the custom stateChange event carries a payload property that contains the value entered by the user into the stateIT component.

        < af:panelLabelAndMessage id="statePanel" label="State" for="stateIT">
                
       < af:panelGroupLayout layout="vertical" halign="left">
           
        < af:inputText id="stateIT" value="#{advertisement.state}">
                
         < af:serverListener type="stateChange" 
                 
                   method="#{stateManager.filterStatesOnState}"/>
                                   
         < af:clientListener method="handleStateChange" type="keyUp"/>
                 
        < /af:inputText>
                
        < af:selectOneListbox id="stateSuggestions" valuePassThru="true" size="5"
                
                   binding="#{stateManager.stateSuggestionListBox}">
                                   
         < f:selectItems value="#{stateManager.statesSelectItems}"/>
                 
         < af:clientListener type="valueChange" method="acceptStateSuggestion"/>
                 
        < /af:selectOneListbox>
                
       < /af:panelGroupLayout>
           
      < /af:panelLabelAndMessage>


Here is the JavaScript code we use on our page:

< af:form id="form">

    < trh:script text="
        
        function handleStateChange(event) {
                
          component = event.getSource(); /* the stateIT component */
                  
          AdfCustomEvent.queue( component, 'stateChange'
                  
                    , {payload:component.getSubmittedValue()}, true);     
                                                    
          event.cancel();          
                  
        }
        
        function acceptStateSuggestion(event) {
                
          var source = event.getSource(); /* the stateSuggestions listbox */
                  
          var state = source.findComponent('stateIT');
                  
          state.setValue( source.getValue());
                  
        }
     "> < /trh:script>
   

The filterStatesOnState() method in the stateManager bean is fairly simple: it sets the filter property of the stateManager bean, which takes effect when the getStates() method is called next. This method creates a list, a sublist of the entire list of states, that contains only those states whose names are ranked equal to or later alphabetically than the filter value.

A critical step in the filterStatesOnState() method is when it instructs the Oracle ADF Faces RC framework to refresh the stateSuggestions listbox in the client. This is done through a call to adfFacesContext.addPartialTarget() in which the stateSuggestions component is added to the list of components that will be refreshed at the end of the current PPR cycle.

public class StateManager {

  private List < String> states = new ArrayList();
  
  private String stateFilter;
  
  private RichSelectOneListbox stateSuggestionListBox;
  
  public List< String> getStates() {
  
    if (stateFilter == null) {
        
      return states; // return all states 
          
    } else {
        
      List< String> filteredStates = new ArrayList();
          
      for (String state : states) {
          
        if (state.compareTo(stateFilter.toUpperCase()) > -1) {
                
          // build list with all states that alphabetically follow the filter
          filteredStates =
                  
              states.subList(states.indexOf(state), states.size() - 1);
                          
          break;
                  
        }
                
      }
          
      return filteredStates;
          
    }
        
  }
  
  public void filterStatesOnState(ClientEvent clientEvent) {
  
    // get the payload value from the stateChange event: that value is the 
        
    // string entered by the user in the state field
        
    stateFilter = (String)clientEvent.getParameters().get("payload");
        
    // have the stateSuggestions listbox refresh at the end of this PPR cycle
        
    // when refresh takes place, the getStates() method is called to produce the
        
    // state selectitems to show in the listbox; the new stateFilter value is then applied
        
    AdfFacesContext adfFacesContext = AdfFacesContext.getCurrentInstance();
        
    adfFacesContext.addPartialTarget(this.stateSuggestionListBox);
        
  }

On running the page, we see the State field with its associated listbox for state suggestions, as shown in Figure 10.

Figure 10 The State field with its associated listbox for state suggestions

When the user types a character in the State field, the listbox is refreshed nearly instantaneously. Only state names that follow the string in the State field alphabetically are listed. In Figure 11, the user typed O.

Figure 11 The refreshed listbox for the state field with the value ‘O’

Additional characters further filter the suggestions list. In Figure 12, the user typed OR:

Figure 12 The listbox after further filtering by an additional character

The user can select the list element corresponding to the desired value. That value is then copied to the State field, as shown in Figure 13.



Figure 13 The selected value appears in the state field

This last action is a purely client-side affair: a clientListener calling a JavaScript function that uses the Oracle ADF Faces RC JavaScript API to update (the client-side representation of) an inputText component.

Run the ProductAdvertismentWithAutoSuggest.jspx page in the sample application for this article for a demonstration of the auto suggest functionality.

Using Google Maps to Visualize the Location of Selected Product Offerings

Using maps to visualize geographical information is a great way of not only providing your end users with useful information they can more easily interpret than textual data but also increasing the attractiveness of your application. We will add a map to our product advertisement page. The map will show the location of the product advertisement, based on the address and state entered. Whenever the values of Address and State change, the map—an integrated widget from Google Maps—is updated accordingly.

The first step is to add a layout container to the page to embed the Google map:
< af:panelWindow id="mapPW" title="Map with locations for selected Product Offerings">

   < af:panelHeader id="mapPH" text=" " inlineStyle="width: 700px; height: 400px">
   
   < /af:panelHeader>
   
 < /af:panelWindow>

This will render as a DIV on the HTML page (see Figure 14), with an ID of mapPH. The Google Maps JavaScript code will use that DIV element to merge the map into.

Figure 14 The panelwindow with the invisible panelheader that will contain the Google Map

To initialize the map, we need to add a JavaScript snippet into the page that will load the Google Maps library and run the startup code right after the page has loaded. This JavaScript is embedded in the metaContainer facet of the document component. To integrate Google Maps into your Web applications, you need to make use of a key you can acquire at http://code.google.com/apis/maps/signup.html. Using this key, the first script component attaches the Google Maps JavaScript library to the page. In the next script component, the Google Maps library is activated. The initialize function, which creates a map object and merges it into the mapPH DIV element, is called when the page has loaded as a result of the call for this function to the google.setOnLoadCallback() function.

  < af:document id="doc">
  
   < f:facet name="metaContainer">
   
    < af:group>
        
     < trh:script source="http://www.google.com/jsapi?key=GOOGLE_MAPS_KEY" />
         
     < trh:script>
         
      google.load("maps", "2.x"); 
          
      var map; 
          
      function initialize() {
          
       map = new google.maps.Map2(document.getElementById("mapPH"));
           
       map.setCenter(new google.maps.LatLng(34, -100), 3);
           
       map.addControl(new GLargeMapControl()); 
           
       map.addControl(new GMapTypeControl()); 
           
      }
      
      google.setOnLoadCallback(initialize); 
          
      function addMarker(long, lat, text) { 
          
       map.clearOverlays(); 
           
       var latlng = new GLatLng(lat,long); 
           
       marker = new GMarker(latlng); 
           
       map.addOverlay(marker);
           
       marker.openInfoWindowHtml(text);
            
      }
     < /trh:script>
         
    < /af:group>
        
   < /f:facet>

The addMarker() method will be invoked at the end of PPR cycles whenever the address or state has changed for a product posting—inputText elements for both State and Address will autosubmit. This addMarker() method clears existing markers from the map and creates a new marker with the text specified, at the position defined through the long and lat parameters (see Figure 15). The values for these parameters are determined in the server from the address and the state, using a geocoding service.

Figure 15 The map is showing, initially focused on the US

The ProductPosting class—of which the advertisement bean is an instance—has a few new methods for creating the new map marker. The refreshMapMarker() method is called from both setAddress() and setState()—in other words, whenever the address or the state has been modified. This method uses getCoordinates() to retrieve the longitude and latitude of the location described by Address and State. Given these coordinates, the text to be displayed in the balloon for the marker is derived from the name and description in the product posting.

Then a new trick is pulled out of the Oracle ADF Faces RC hat: using ExtendedRenderKitService, a JavaScript snippet is added for execution on the client at the end of the PPR cycle. This snippet invokes the client-side addMarker() method, which creates a new map marker through the Google Maps API.

    private void refreshMapMarker() {
                
    String location = ((getAddress() != null && getAddress().length() > 0) ? 
                
               (getAddress().replace(' ','+') + ",") : "") 
                                                   
            + ((getState() != null && getState().length() > 0) ? 
                                                
               (getState().replace(' ', '+') + ",") : "");
                                                   
    float[] coordinates = getCoordinates(location);
                
    if (coordinates != null && coordinates.length == 2) {
                
      String text =
            
        "Product offered " + (getName() != null ? getName() : "")
                        
        + " < br/>< p > " +
                                
        (getDescription() != null ? getDescription() : "") + "< /p>";
                                
      FacesContext context = FacesContext.getCurrentInstance();
                  
      ExtendedRenderKitService erks =
                  
       Service.getRenderKitService(context, ExtendedRenderKitService.class);
           
      erks.addScript(context,
          
                        "addMarker(" + coordinates[1] + "," + coordinates[0] +
                                                          
              ",'" + text + "');");
                          
    }
        
  }
  

The getCoordinates() method in the ProductPosting class uses the Google Maps geocoding service, with the same key mentioned before. The service takes a location description such as Sacramento, California, or The Mall, London, UK, and returns, among other things, the latitude and longitude for the location(s) that match the location description. The getCoordinates() method returns an array of two float values: the latitude and longitude for the location input parameter.

public static float[] getCoordinates(String location) {
    URL geoCodeUrl;
    try {
      geoCodeUrl = new URL("http://maps.google.com/maps/geo?q=" + location +
             "oe=utf8&sensor=true_or_false&key="+GOOGLE_MAPS_KEY+"&output=csv");
    } catch (MalformedURLException e) {
      return null;
    }
    BufferedReader in;
    String coord = null;
    try {
      in = new BufferedReader(new InputStreamReader(geoCodeUrl.openStream()));
      coord = in.readLine();
      in.close();
    } catch (IOException e) {
      return null;
    }
    if (coord != null) {
      String[] segments = coord.split(",");
      float[] coordinates =
        new float[] { Float.parseFloat(segments[2]), Float.parseFloat(segments[3]) };
      return coordinates;
    }
    return null;
  }
  public void setState(String state) {
    this.state = state;
    refreshMapMarker();
  }

  public void setAddress(String address) {
    this.address = address;
    refreshMapMarker();
  }

When we run the page with all this code in place, the map will show a marker for the location details we’ve entered in the address and state components, as shown in Figure 16.

Figure 16 Google map showing a location details marker for the current product

Whenever we make a change to those components, the map will immediately be refreshed, thanks to autoSubmit, which, through the setter methods, calls refreshMapMarker(), which uses the Google Maps geocoder to retrieve the longitude and latitude of the location and then instructs ExtendedRenderKitService to call the client-side addMarker() function that creates the new map marker. Note in Figure 17 that the Google Map zoom and focus controls are available.

Figure 17 Google map synchronized with the changed product details

Run page ProductAdvertismentWithMap.jspx in the provided demo application to see the integrated Google Maps widget in action.

Conclusion

Modern Web applications use Ajax technology to provide the user with attractive features that enable dynamic interaction. Oracle ADF Faces RC makes it very easy for developers to unleash the Ajax-based richness. Part 1 of this series demonstrated how, with largely declarative means, many simple yet effective use cases—such as instant validation and conversion, cascading list of values, implementing a show/hide toggle, and performing table column total calculation—can be implemented. This second part showed how with a little bit of additional client-side programming, some advanced use cases are still rather straightforward to put together. It discussed the implementation of an autosuggest feature, creation of a context-sensitive pop-up, development of an autorefreshing user interface, and integration of Google Maps. The Oracle JDeveloper 11g application that is available with this article contains the source code for all these examples.

This article is loosely based on a unit in the manual for the Oracle ADF 11g training jointly developed by SAGE Computing (Australia) and AMIS (The Netherlands).


Lucas Jellema is Technology Manager with AMIS in Nieuwegein, The Netherlands, and is an Oracle ACE Director (Oracle Fusion Middleware). Lucas is a frequent blogger, author, and presenter at international conferences and workshops. He earns most of his money doing teaching and consulting around the Oracle SOA Suite and ADF technology.

Chris Muir ( http://one-size-doesnt-fit-all.blogspot.com) is an Oracle ACE Director (Oracle Fusion Middleware) and a senior consultant and Oracle trainer for SAGE Computing Services in Australia. With a combined 40 years of experience in Oracle development and database technology, both show battle scars with years of experience in the RDBMS, traditional Oracle development tools, as well as Oracle Application Express, Oracle JDeveloper, and good old SQL*Plus.