HowTo:Create a Data Handler to Add Data to a Package

From AgileApps Support Wiki
Revision as of 20:40, 31 January 2012 by imported>Aeric
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

For:   Developers
Level: Advanced
Time: 20 minutes

See more:
    ◾ HowTo Guides

A Data Handler is a class that provides package-ready data for publication. It cooperates with the GUI to allow for interactive selection of data, it handles data upgrades, and it provides for special handling of data when a package is being deleted.

PackageItemType Interface

A Data Handler class implements the PackageItemType interface to perform the following functions:

getDataList()
Give the packager a list of data items, so they can interactively select the items to include (or choose 'All).
packageItem()
During the publication process, this method produces a string-representation of the data for inclusion in the package. (The data generally comes from an object, but it can come from anywhere. Similarly, the most common data format is XML, but it could be anything.)
subscribeItem()
To import data exported by packageItem(), this method parses the data-string and puts it where the application expects to find it. (Typically, that means parsing XML and populating an object, but not necessarily.)
upgradeItem()
Parse the XML representation of data in an upgraded version of the package, and modify the local data appropriately.
deleteItem()
This method lets you do something with the data when a package is deleted. For example, if object records were added by subscribeItem() and later modified by the user, this method could conceivably be used to save the data for a later re-install.

Sample Data Handler

This annotated class provides a prototype for a Data Handler. In this case, it handles the packing and unpacking of data that maps zip codes into city/state locations.

It assumes the existence of a ZIPMAP table with four fields:

Field Type Length Notes
publishers_id String 50 Filled in for subscriber's copy. Empty in publisher's copy.
zip_code Number 5 Always Required
city String 30 Always Required
state String 2 Always Required
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
 
import com.platform.api.PackageItemType;
import com.platform.api.Result;
import com.platform.api.Functions;
import com.platform.api.Parameters;
import com.platform.api.ParametersIterator;
 
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
 
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

public class ZipMapDataHandler implements PackageItemType
{
  /** 
   * Returns a list of data elements to the GUI, where the packager 
   * can select which elements to include in the package. This version
   * of the method returns elements from a platform object (table),
   * but the data could come from anywhere.
   */
  @Override
  public Map<String, String> getDataList()
  {
    Map<String, String> map = new HashMap<String, String>();      
    try
    {
        // Note: 
        // A maximum of 5,000 entries is returned by the short form 
        // of searchRecords. For a longer list of entries, use the
        // long form:
        //   Functions.searchRecords(id, fields, filter, "", "", "", "", 0, 5000);
        // where:
        //  a) Empty strings: sortBy, sortOrder, sortBy2, sortOrder2
        //  b) An initial value of 0 is used for the search offset. 
        //     (Bump it up for subsequent reads.)
        Result result = Functions.searchRecords("ZIPMAP", "id,zip_code,city,state", 
                                      "");
        int resultCode = result.getCode();
        Logger.info(resultCode, "ZipMap");
        if(resultCode < 0)
        {
            String msg = "Data could not be retrieved";
            Logger.info(msg + ":\n" + result.getMessage(), "ZipMap"); // Log details
            Functions.throwError(msg + ".");                          // Error message
        }
        else if(resultCode == 0)
        {
            Logger.info("No records found", "ZipMap");
        }
        else
        {
          ParametersIterator iterator = result.getIterator();
          while(iterator.hasNext())
          {
            // These values are returned to the GUI:
            //  * recordID is an internal value that will be hidden
            //  * recordName will be displayed to packagers, 
            //    who will use it to identify records to include.    
            Parameters params = iterator.next();
            String recordID = params.get("id");
            String recordName = params.get("zip_code");
            map.put(recordID, recordName);
          }
        }
    }
    catch(Exception e)
    {
    }
    return map;
  }
  
  /** 
   * Returns an XML string to be inserted into an Application Package.
   * Contents of the string are totally up to the class, since it
   *   will only ever be seen by the subscribeItem() method. 
   * The only requirement is that the XML must be well-formed.      
   * 
   * In this example, the XML string has the format: 
   *   <DATA>
   *      <ITEM>
   *         <ID>...</ID>        -- ID of the data element. 
   *         <ZIP>...</ZIP>      -- Zip Code  
   *         <CITY>...</CITY>    -- City associated with the zip code
   *         <STATE>...</STATE>  -- State associated with the zip code
   *      </ITEM>
   *      ...
   *   </DATA>
   *
   * Notes: 
   *  * The element ID comes is unique to this platform instance. 
   *    Including it in the data stream allows it to be stored as
   *    "publisher ID" on the target system, where it can be used
   *    for efficient updates (when the data set is large enough
   *    to warrant it.   
   * 
   *  * There is no limit to the number of data elements that can 
   *    be included, or how fields they can contain.
   *
   * @param map Contains key/value pairs:
   *    a) The ID of the package data item (key="package_id")
   *    b) A list of Key/value pairs consisting of a recordID and 
   *       corresponding recordName. (key="keys")
   *        Note: 
   *        recordNames were needed in the GUI, but here,
   *        we only need the IDs that correspond to the records
   *        the packager selected. 
   */
  @Override
  public String packageItem(Map<String, Object> map)
  {
    StringBuilder data = new StringBuilder("<DATA>\n");    
    try
    {
      // package_id is the ID of the Package Data item.
      // It isn't used here, but here's how to get it if you need it:
      String package_id = (String)map.get("package_id");
      
      // The map's payload is the list of recordIDs selected by the 
      // packager. The record IDs are used to retrieve the data to
      // be inserted into the XML stream.
      ArrayList<String> keys = (ArrayList<String>)map.get("keys");
      
      for (int i=0; i<keys.size(); i++)
      {
          String id = keys.get(i);
          data.append("<ITEM>\n");
          data.append("<ID>").append( id ).append("</ID>\n");
          
          Result result = Functions.getRecord("ZIPMAP",
                            "zip_code,city,state", id);
          int resultCode = result.getCode();
          if(resultCode != 1)
          {
              String msg = "Data could not be retrieved";
              Logger.info(msg + ":\n" + result.getMessage(), "ZipMap"); // Log details
              Functions.throwError(msg + ".");                          // Error message
          }
          else
          {
            Parameters resultParameters = result.getParameters();
            String zip = resultParameters.get("zip_code"); 
            String city = resultParameters.get("city"); 
            String state = resultParameters.get("state"); 
            data.append("<ZIP>").append(zip).append("</ZIP>\n"); 
            data.append("<CITY>").append(city).append("</CITY>\n"); 
            data.append("<STATE>").append(state).append("</STATE>\n"); 
          }
          data.append("</ITEM>\n");
      }        
    }
    catch(Exception e)  
    {
    }
    data.append("</DATA>\n");  
    return data.toString();
  }
  
  /** 
   * Parses the XML string created by the packageItem() method and
   * puts the data wherever the application expects to find it.
   *
   * In this case, the string contains data that needs to be loaded 
   * into an object table.
   *
   * @param map contains a key/value pair with key="data" and a 
   *            value that is the XML data created by packageItem()
   *       
   */
  @Override
  public void subscribeItem(Map<String, Object> map)
  {
    Logger.info(map, "ZipMap"); 
    try
    {
      // Get the XML string created by packageItem()
      String xml_code = (String)map.get("data");
      StringReader s = new StringReader(xml_code);

      // Parse the XML
      DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
      docFactory.setNamespaceAware(true);
      DocumentBuilder docbuilder = docFactory.newDocumentBuilder();
      InputStream is = new ByteArrayInputStream(xml_code.getBytes());

      Document doc = docbuilder.parse(new InputSource(is));
      if (doc != null)
      {
        // Get a list of <ITEM> nodes. (Avoid the empty text 
        // nodes created by the NLs added for readability in
        // the packageItem() method.)
        NodeList itemList = doc.getElementsByTagName("ITEM");
        if (itemList.getLength() == 0) return;  

        // For each item in the list
        for (int i = 0; (i < itemList.getLength()); i++)
        {
          // Get the id, zip, city, and state elements
          Node item = (Node)itemList.item(i);
          NodeList itemChildren = item.getChildNodes();        
          HashMap<String,String> fieldsMap = new HashMap<String,String>();
          
          // Extract XML data for each field
          for (int j = 0; (itemChildren != null) 
          && (j < itemChildren.getLength()); j++)
          {
            Node childNode = (Node)itemChildren.item(j); 
            String child_node_name = childNode.getNodeName(); // ID, etc.
            Node textChild = childNode.getFirstChild();            
            String str = "";            
            if (textChild != null 
            &&  textChild.getNodeType() == Node.TEXT_NODE)
            {
              str = textChild.getNodeValue();        
              if(str == null)
              {
                str = "";
              }
            }                            
            fieldsMap.put(child_node_name, str);
          }
          Logger.info(fieldsMap, "Fields Map");
          
          // Add a new record, with all defined fields.
          // Save the recordID from the publishing site so
          // it's available for efficient updates, when needed.
          Parameters params = Functions.getParametersInstance();
          params.add("publishers_id", fieldsMap.get("ID")); 
          params.add("zip_code", fieldsMap.get("ZIP"));
          params.add("city", fieldsMap.get("CITY"));        
          params.add("state", fieldsMap.get("STATE"));        
          Result result = Functions.addRecord("ZIPMAP", params);
          
          int resultCode = result.getCode();        
          if(resultCode < 0)
          {
            String msg = "Data could not be added";
            Logger.info(msg + ":\n" + result.getMessage(), "ZipMap"); // Log details
            Functions.throwError(msg + ".");                          // Error message
          }
        }
      }
    }
    catch(Exception e)
    {
    }
  }
          
  /** 
   * Handle upgrades to newer versions of the package, which may 
   * have modified data. New data elements need to be added, 
   * existing data elements need to be updated, and any elements
   * that don't belong in the new set need to be removed.
   *
   * If there is a large number of data items, and relatively few
   * need to be updated at any one time, the "publisher's ID" field
   * can be used for efficient updates. In that case, only the 
   * modified records need to be included in the data stream.
   * 
   * In this case, there is a small number of data items. So it 
   * makes more sense to include the entire set in the data stream. 
   * The code here can then take the simple expedient of deleting 
   * the existing data elements, followed by
   * subscribeItem() to add the contents of the new set.
   */
  @Override
  public void upgradeItem(Map<String, Object> map)
  {
    deleteItem(map);
    subscribeItem(map);
  }
  
  /** 
   * This method is invoked when a package is deleted. It can be used
   * in several ways:
   * <ul>
   *   <li>For an object that is part of the package (and therefore
   *       created by the package), this method can be used to do 
   *       something with the data before the object is deleted. 
   *       (The object will be deleted along with the package.)
   *
   *   <li>For an object that is not included in the package, but 
   *       which already exists on the subscriber's system when the
   *       package is installed, this method can remove the records
   *       added by the package, or do something else with that data.
   *
   *   <li>For data that lives outside of any object, this method
   *       can be used to do the cleanup.
   *   </ul>
   *
   * In this class, the method is also called by upgradeItem().
   * So we need to implement it to clear out existing data, before
   * new data is added to the table.
   *
   * @param map 
   */
  @Override
  public void deleteItem(Map<String, Object> map)
  {
    Logger.info("deleting  " + map, "ZipMap");
    try 
    {
      Result result = Functions.searchRecords("ZIPMAP", "id", "");
      ParametersIterator iterator = result.getIterator();
      while(iterator.hasNext())
      {
        Parameters params = iterator.next();
        String recordID = params.get("id");
        Functions.deleteRecord("ZIPMAP", recordID);
      }
    }
    catch(Exception e)
    {
      Logger.info("Error during Package Data delete", "ZipMap");
    }
  }
}