Rebecca Searls' Blog

pageicon Thursday Mar 05, 2009

Importing resource files with resImport in openinstaller

I've been playing with openinstaller because I am considering using it in a project.   I have been proto-typing several application specific screens.  I want to keep my screen's  text separate and localizeable from openinstaller's.  The view file schema, policytemplate.xsd provides element resImport for this purpose.  It enables the importing of resource files into the view.  In openinstaller 0.9.4, resImport is not supported.  The element is parsed but ignored.  Fortunately openinstaller is an open source project and I could access the source code.  I found the way to enable this element with a simple code fix.  Two files, CLIWizard and GUIWizard need the follow code addition in method flipGroup.  In reviewing the code I also found there are some rules you must follow when specifying the path and file name in the element attribute.

Simple Code Fix

//- this is the existing code
            if (null == gAPOCTemplate) {
                  throw new EnhancedException("APOC_TEMPLATE_NULL", "page=" + thePageName);
            }

            //- Adding this if-clause to enable sresource file processing
else {
                //- process user provided .prefs files for I18n substitution
                List<APOCResImport> gAPOCResImportList = gAPOCTemplate.getResImports ();
                for (APOCResImport gAPOCResImport: gAPOCResImportList){
                        TemplateProcessor.getInstance().loadModelFromResource(gAPOCResImport.getPackagePath());
                }
            }

Path and file naming rules 

 Here is the DTD definition for resImport

                <!ELEMENT resImport EMPTY>
                <!ATTLIST resImport
                        apt:packagePath NMTOKEN #REQUIRED
                >

example:

            <apt:resImport apt:packagePath="/templates/LocalStrings"/>          
            <apt:resImport apt:packagePath="iHome/resources/MyInstallHome"/>

  • The file must reside in your metadata directory. It can be in a subdirectory of this directory.
  • Give the full pathname from the metadata root directory to the property file.
  • The pathname may start with a '/' slash but it is not required.
  • The file must use the extension .prefs.
  • The file extension must not be specified in packagePath. Openinstaller appends
    the extension to the filename.

Directory structure for example above

            /prjA/pDemo/metadata/template/LocalStrings.perfs
            /prjA/pDemo/metadata/template/ui.perfs
            /prjA/pDemo/metadata/iHome/resources/MyInstallHome.perfs
            /prjA/pDemo/metadata/view
            /prjA/pDemo/metadata/model
            /prjA/pDemo/metadata/dependency/index.xml
            /prjA/pDemo/metadata/pagesequence.xml
            /prjA/pDemo/metadata/pagesequence.properties


pageicon Wednesday Feb 25, 2009

Mobilizing a Web Service Using JAX-RS connector in Sun GlassFish Mobility Platform 1.1 - Part III

Part II examined the JAX-RS connector interface.  This segment describes some of the design challenges and explores some of the solutions that were implemented in this connector.

First, let's look at the data that is going to be transmitted.  If you examine your Salesforce Developer account, you'll notice a few predefined customer accounts.  Although there are a variety of subcategories for each account, you are only interested in the Contacts and Tasks categories and the basic account information.

The following design decisions determined how the connector was implemented:

  • Selecting the data that the client will present
  • Deciding how to implement the BusinessObjects
  • Identifying the entity that changed the data of a BusinessObject since the last sync session

What Data Will the Client Present?

The first design decision was selecting the data that the client would present.  Because this was a demonstration project, we wanted to keep the data set small and representative.  For an account, we chose fields from the Account category: account  name and number, company address, and some sales information.  Contact information included just name, title, email, and phone number.  Title, description, dates, and status information were selected for Tasks.

How Will the BusinessObjects Be Implemented?

The second design decision was to decide what the BusinessObject [1]  was going to look like.  Should we transmit all of an account's contacts, tasks, and account information as a single BusinessObject or should there be a separate BusinessObject for each data type?  Each option had advantages and disadvantages.

The first option was desirable because you could define a single BusinessObject to hold all the data for a single account.  There would be a relatively small number of files for the client and connector to process, making the design similar to that of Personal Information Management (PIM) software.  There were several disadvantages to this design. Only with greate difficulty could the connector detect newly created tasks and tasks tagged for deletion by the client.  Every change to the data, either from the client or the repository, required the connector to evaluate all of the contents of the BusinessObject during sync cycles, resulting in very high processing overhead.

The second option, creating separate BusinessObjects for each data type, resolved the problem of detecting data changes and lowered the processing overhead, but it added complexity to both client and connector implementations because many more classes were required.  This design presented a new issue as well.  We needed a consistent and unique identifier for each data type for as long as the data existed in the repository.  We also needed a means to convey the relationship of the objects to each other without having to open and read every BusinessObject.  This was solved with a file naming algorithm.  We discovered that we could uniquely identify each data type by using the database's own table id.  This value is unique and exists as long as the data does.  Using the id, we created the following file naming format that conveyed the data associations:

  • Account ECBO files were named <id>
  • Associated contact ECBOs were named <id>_C_<id>
  • Task ECBOs were named <id>_T_<id>

When the client creates a new task, the file is named <id>_T_<x>, where x is an integer starting with 0. Each new task is given an incremental number.  This allows the connector to easily identify tasks to be created in the repository.  Deleted and updated BusinessObjects are handled by the normal sync mechanisms in the connector, that is, deleteBusinessObject() and putBusinessObject() are called.  This format makes it quite easy to parse the filenames and process the data appropriately.

    This pseudocode shows how the client application parses and processes the files.

Hashtable masterTable ...
    List fList = get list of files on device.
    for (File f : fList){
        String fName = f.getName();
        if (fName.length() > 24){
          int loc = fName.indexOf("_T_");
          if ( loc > -1){
            String tmpN = fName.substring(0,loc);
            SFAccount sfa = masterTable.get(tmpN);
            if (sfa == null){
                //- parent object does not exist yet.  Create it.  The parent
                //- will be found later.
                sfa = new SFAccount();
                masterTable.put(name, sfa);
            }
            sfa.addTaskFilename(fName); //- Just put in list; process array
                                        //- contents on 1st user selection of account
          }
           //- same as above for Contact
        } else {
            //- an account
            String name = fName.substring(0, fName.length -4);
            SFAccount sfa = masterTable.get(name);
            if (sfa == null){
                sfa = new SFAccount();
                masterTable.put(name, sfa);
            }
            //- If sfa was not null, a Contact or Task child created it because it was
            //- earlier in the list.  Fill it in now.
            sfa.deserialize(f);
        }
    }



One disadvantage to this format is that each id string is 18 characters long, making the filenames longer than we would have liked.  This disadvantage was easily offset by the advantages.

Three BusinessObject classes, SFAccount, SFContact and SFTask, were created for the connector.  SFAccount implements the required BusinessObject methods serialize() and deserialize(), that write and read the data passed between the connector and client.  This class only generates business objects that contain the account information.  The generation of Contact and Task business objects is performed by SFContact and SFTask, respectively.

The connector makes a call to the Salesforce web service requesting account, contact, and task records.  Here is the query statement.

SELECT  Id,AccountNumber,Name,
                    BillingCity,BillingPostalCode,BillingState,BillingStreet,
                   Website,Phone,Fax,NumberofLocations__c,NumberOfEmployees,Type,
                   Industry,AnnualRevenue,SLA__c,SLAExpirationDate__c,SLASerialNumber__c,
          (SELECT  Id,AccountId,FirstName,LastName,Title,Email,MobilePhone,Phone,LastModifiedDate FROM Account.Contacts),
          (SELECT  Id,AccountId,Subject,Description,ActivityDate,Priority,Status  FROM Account.Tasks)
    FROM Account



The connector then creates a new SFAccount object for each returned account and calls method extractData() to process the record.  Method extractData() extracts the account information and writes it to the local fields.  It creates and calls SFContact and SFTask objects, which extract their information from the record.  SFAccount keeps a list of the SFContact and SFTask objects for later processing.


public byte[] serialize() throws IOException {
        //- Serialize the local fields of basic account information.
        //- DO NOT serialize the Contract or Task objects here.  Each class
        //- serialized itself when called.
    }

    public void deserialize(byte[] array) throws IOException {
        //- Deserialize basic account information only
        //- into the local fields.
    }


    public void extractData(Account a) {
        //- pseudocode for processing data of a single account requested
        //- by a query to the backend repository
        Extract account data; write into local fields
        setName(a.getId());  //- set filename
        foreach Contact in list of contacts{
            new SFContact
            call SFContact to extract data
            add SFContact to contactList
        }
        foreach Task in list of tasks{
            new SFTask
            call SFTask to extract data
            add SFTask to taskList
        }
    }


    public List<SFContact> getSFContactList(){
        return contactList;
    }

    public List<SFTask> getSFTaskList(){
        return taskList;
    }



BusinessObjects SFContact and SFTask have implementations of methods serialize(), deserialize(), and extractData().  They also have a method getSObject(), which creates a Salesforce web service object that contains the values of the local fields of the object.  The SObjects are used when data in the backend repository is created or updated.


SFContact
    public byte[] serialize() throws IOException {}
    public void deserialize(byte[] array) throws IOException {}
    public void extractData(Contact c){
        //- Extract the record data; write data to local fields.
        setName(c.getAccountId() + "_C_" + c.getId());  //- set filename
    }
    public SObject getSObject(){
        //- put local values in a Salesforce contract class
    }

    SFTask
    public byte[] serialize() throws IOException {}
    public void deserialize(byte[] array) throws IOException {}
    public void extractData(Task t){
        //- Extract the record data; write data to local fields.
        setName(t.getAccountId() + "_T_" + t.getId());  //- set filename
    }
    public SObject getSObject(){
        //- put local values in a Salesforce task class
    }



Which Entity Changed the Data of the BusinessObject Since the Last Sync?

The third design decision involved identifying the entity that has changed the data of a BusinessObject since the last sync session.  This identification determines which data takes precedence.  Three cases need to be handled:

  • The client changed the data since the last sync session, so the repository needs to be updated.
  • The repository changed the data and the client needs to be updated.
  • Both the client and repository changed the data, in which case the client is updated with the repository data.


For each data type, account, contract, and task the repository retains a last-altered timestamp.  This timestamp is a piece of data sent in the BusinessObject.  The client application treats it as a read-only field.  The connector uses it to determine if the repository data was altered since the last sync. When the timestamps differ, the repository data takes precedence.  Any changes made to the data by the client are ignored.  If the timestamps for the client and the repository objects are the same, the repository data has not changed, but the client data may have changed.  When this occurs, the connector compares the data sets and updates the repository with client data if differences are found.  Here is the algorithm.


SFTask masterT  //- Data from the repository
    SFTask t        //- Data from the client

    //- Check for change to the repository timestamp
    if (masterT.getLastModifiedDate().equals(t.getLastModifiedDate())) {
        //- check if the client data changed.
        //- Method getDataAsString() returns a simple string concatenation of all the data
        //- minus the id and timestamp.
        if (masterT.getDataAsString().hashCode() != t.getDataAsString().hashCode()) {
            // The client changed not the master; put in the update list
            upDateList.add(t.getSObject());
        }
    }



Lastly, we encountered performance issues while connecting to the the Salesforce web service.  It takes a relatively long time for the Salesforce web server to establish a connection.  This became a performance concern when we connected to the repository for each call to the JAX-RS interfaces. Fortunately, the web service connection remains open for a long time as well, so we implemented a connection pool.  The (user+password, connection) object pair were cached in a hashtable, then, rather than requesting a new connection on each call, we performed a lookup and only created a new connection when one was needed. This improved performance.



Part IV discusses using the URLs for debugging the connector.

-----
[1] Developing MEP Connectors - Part 1
     http://weblogs.java.net/blog/spericas/archive/2008/07/developing_mep.html

    Developing MEP Connectors - Part II
     http://weblogs.java.net/blog/spericas/archive/2008/08/developing_mep_1.html


resImport use in OpenInstaller

I'm creating a custom panel to be displayed in OpenInstaller.  I wanted to put my text in a separate properties file.  The documentation directed me to use XML element 'resImport'.  This element allows the user to define the path to a properties file to use for text substitution.  Unfortunately OpenInstaller does not currently support it.  The XML is parsed correctly but no code references the file for substitution.  All text must go into the ui.pref file.  You must copy the default provided ui.pref file to you directory metadata/templates/ and add your text.

pageicon Tuesday Feb 24, 2009

Setting default values in OpenInstaller

I've been playing with openinstaller.  In particular I was trying to understand how the 'eval' and 'eval-engine' attributes worked.  With some effort I was disappointed to find that the functionality for eval-engine is not currently implemented nor is there is no way to pass the classpath of my class to the engine.

I was able to get 'eval' to work.  I discovered that there is a strict format for it.  The word 'target' is a keyword and MUST be used in conjunction with any of the predefined variables (see below).  The format MUST be ":[target:<predefined varable>]"  If this format is not used there are 3 possible results.  One, the text itself is considered the default value and displayed.  Two, the text resolves to "".  Three, the text causes a silent error and the default value '/tmp/x' is used.

  example
        <prop oor:name="TRG_DOMAIN" oor:type="xs:string">
            <value if:eval=":[target:sys.homeDir]"  >/tmp/x</value>
                :
                :
        </prop>


  Another keyword is 'component'  it is used when identifying a component name.

  example
        <prop oor:name="TRG_DOMAIN" oor:type="xs:string">
            <value if:eval=":[component:SomeComponent.group.username]"  >/tmp/x</value>
        </prop>


  There is also a keyword 'engine'.  It is unclear how this is to be used.



NOTE:

     I found some out of date JavaDoc information in TargetToken about the supported predefined variables.  Two variables are incorrectly stated.  Here is the correct information.

correct  incorrect
        sys.homeDir       sys.raHomeDir       displays the system property user.dir
        sys.tmpDir          sys.raHomeDir       displays the system property java.io.tmpdir


correct as listed
        sys.hostName                                    Host name of host
        sys.ipAddress                                    IP Address of host
        sys.OSName                                     OS name
        sys.OSVersion                                  OS version
        sys.OSArch                                      OS architecture
        sys.userName                                   Current user's login account name
       *sys.userDir                                      The current directory from which the engine was invoked
(sys.userDir always returns '/'.  This may because the engine is invoked from a shell script)

pageicon Thursday Feb 19, 2009

Mobilizing a Web Service Using a JAX-RS Connector in Sun GlassFish Mobility Platform 1.1 - Part II

In Part I, we took a cursory look at the Salesforce sample application.  Now let's take a closer look at the JAX-RS connector interface.

The JAX-RS connector template is included in the Sun GlassFish Mobility Platform 1.1 bundle.  It is provided as a Maven archtype [1].  To install it in your repository and make it available to your IDE, do the following:

       1. Download Sun GlassFish Mobility Platform 1.1 and the client bundle, available here.

       2. Install Sun GlassFish Mobility Platform 1.1.

       3. Change to the lib directory in your Sun GlassFish Mobility Platform domain:

cd $AS_HOME/domains/mep/lib

       4. Install the maven archetype in your repository:

mvn install:<install-file>
           -DgroupId=com.sun.mep
           -DartifactId=mep-connector-jaxrs-archetype-rar
           -Dversion=3.1.39
           -Dpackaging=jar
           -Dfile=mep-connector-jaxrs-archetype-rar-3.1.39.jar


I use Netbeans IDE 6.5.  This is how I create a project with this archtype:

  1. Start NetBeans IDE.
  2. Select File -> New Project.
  3. Select category Maven, project Maven Project, and press the Next  button.
  4. From the Maven Archtype list, select the node, Archtypes from remote Maven Repositories.
  5. Locate and select MEP Connector Archtype (JAX-RS) then press the Next button.
  6. Type the name of the project in the Name field, accept the  default settings for the remaining fields, and then press Finish.

Two Java files are provided in the Maven archtype: MyBusinessObjectsResource and MyBusinessObjectResource. File MyBusinessObjectsResource is equivalent to the BusinessObjectProvider  class that Santiago Pericas-Geertsen describes in his blog [3].  It contains the entry point method to the connector, lifeCycle() and the method for the R or retrieve operation, getBusinessObjects().

The lifeCycle() method is called at the start and end of each synchronization session.  In the Salesforce connector, it is used to connect to and disconnect from the Salesforce web serverice.  As a convenience, this method returns the EXTENSION variable, which identifies the 3-character file extension that is used by all business objects returned by the connector. The default extension is obj but often a connector-specific extension is defined, making it easier for the client application to unambiguously distinguish objects.  This connector uses the default value, as shown.

@POST
    @Produces("text/plain")
    public String lifeCycle(
            @QueryParam("username") @DefaultValue("username") String user,
            @QueryParam("password") @DefaultValue("password") String password,
            @QueryParam("sessionId") @DefaultValue("") String sessionId,
            @QueryParam("operation") String operation)
    {
        if (operation.equals("initialize")) {
            sfws.createConnection(user, password);
        }
        else if (operation.equals("terminate")) {
            sfws.logout(user, password);
        }
        else {
            throw new RuntimeException("Lifecycle operation " + operation + " not understood");
        }
        return EXTENSION;
    }



The getBusinessObjects() method retrieves the appropriate records from the backend repository.  In Salesforce connector, the method queries the SalesForce backend database for all account data from the identified user's Developer account.  A list of serialized BusinessObjects is returned. [2]

@GET
    @Produces("text/xml")
    public BusinessObjects getBusinessObjects(
            @QueryParam("username") @DefaultValue("username") String user,
            @QueryParam("password") @DefaultValue("password") String password,
            @QueryParam("sessionId") @DefaultValue("") String sessionId)
    {
            //- Get salesforce connection from the connection pool
        try {
            BusinessObjects result = new BusinessObjects();
            List<BusinessObject> resultList = result.getBusinessObject();

            //- call local method to get records from the web service
            //- Serialize data as a (EBCO) BusinessObject.
            List<BusinessObject> aList = sfws.getSFBusinessObjects();
            for (BusinessObject s : aList) {
                byte[] tmpB = ((BaseObjCommon)s).serialize();
                resultList.add((BusinessObject)s);
            }
            return result;
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


This method returns the requested business object, identified by parameter, id.

@Path("{id}")
    public MyBusinessObjectResource getBusinessObject(@PathParam("id") String id) {
        return new MyBusinessObjectResource(context);
    }

The other class, MyBusinessObjectResource, defines the interfaces for  CURD (create, retrieve, update, and delete) operations.  It is equivalent to the BusinessObject abstraction. [2]


This is a retrieve method.  The requested business object is identified by parameter, id.  A serialized BusinessObject is returned.

@GET
    @Produces("application/octet-stream")
    public byte[] getBusinessObject(
            @QueryParam("username") @DefaultValue("username") String user,
            @QueryParam("password") @DefaultValue("password") String password,
            @QueryParam("sessionId") @DefaultValue("") String sessionId,
            @PathParam("id") String id)
    {
        //-  setup code
        bObj = null;
        byte[] b = new byte[0];
        BusinessObject bObj = sfws.getSFObject(id);

        if (bObj != null){
            b= bObj.serialize();
        }
        return b;
    }


This method creates or updates a business object sent from the client application in the backend repository.  In the Salesforce connector, I deserialized the data and put it into a Salesforce web service-accessible format for updating.

@PUT
    @Consumes("application/octet-stream")
    public void putBusinessObject(
            @QueryParam("username") @DefaultValue("username") String user,
            @QueryParam("password") @DefaultValue("password") String password,
            @QueryParam("sessionId") @DefaultValue("") String sessionId,
            @QueryParam("lastModified") @DefaultValue("0") long lastModified,
            @PathParam("id") String id, byte[] object)
    {
        try {
            ByteArrayInputStream in = new ByteArrayInputStream(object);
            DataInputStream dIn = new DataInputStream(in);
            String filePrefix = dIn.readUTF();

            SFObject sfo = new SFObject();
            sfo.deserialize(dIn);
            sfws.updateSFObject(sfo);

            dIn.close();
            in.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


This method deletes a business object, which the client application tagged for deletion, from the backend repository.  I deserialized the data and put it into a Salesforce web service-accessible format for deletetion.

@DELETE
    public void deleteBusinessObject(
            @QueryParam("username") @DefaultValue("username") String user,
            @QueryParam("password") @DefaultValue("password") String password,
            @QueryParam("sessionId") @DefaultValue("") String sessionId,
            @QueryParam("lastModified") @DefaultValue("0") long lastModified,
            @PathParam("id") String id, byte[] object)
    {
        try {
            ByteArrayInputStream in = new ByteArrayInputStream(object);
            DataInputStream dIn = new DataInputStream(in);
            String filePrefix = dIn.readUTF();

            SFObject sfo = new SFObject();
            sfo.deserialize(dIn);
            sfws.deleteSFObject(sfo);

            dIn.close();
            in.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


This method can be called to resolve an update conflict.  The Salesforce connector does not currently make use of it.

@POST
    @Produces("application/octet-stream")
    public byte[] mergeBusinessObjects(
            @QueryParam("username") @DefaultValue("username") String user,
            @QueryParam("password") @DefaultValue("password") String password,
            @QueryParam("sessionId") @DefaultValue("") String sessionId,
            @PathParam("id") String id, BusinessObjects objects)
    {
        // INSERT CODE: merge entries 0 and 1 in objects.getBusinessObject()
        return null;
    }



All of these methods, except for the one mentioned next, are accessed via the URL http://server:port/context-root/businessObjects.  Method MyBusinessObjectsResource.getBusinessObject() is accessed by the URL http://server:port/context-root/businessObjects/{id}, where id identifies the specific object to retrieve.

In Part III, I'll discuss some of the design challenges that we encountered as we developed this connector and explain how we resolved them.

-----
[1] Developing MEP Connectors - Part IV
http://weblogs.java.net/blog/spericas/archive/2008/10/developing_mep_3.html

[2] Developing MEP Connectors - Part 1
http://weblogs.java.net/blog/spericas/archive/2008/07/developing_mep.html

    Developing MEP Connectors - Part II
http://weblogs.java.net/blog/spericas/archive/2008/08/developing_mep_1.html

[3] Developing MEP Connectors - Part III
http://weblogs.java.net/blog/spericas/archive/2008/10/developing_mep_2.html


pageicon Wednesday Feb 18, 2009

Mobilizing a Webservice using JAX-RS connector in Sun GlassFish Mobility Platform 1.1 - Part I

Sun GlassFish Mobility Platform 1.1 has just been released and it has many  great new features.  One particularly noteworthy feature is the addition of a new type of connector, the JAX-RS connector.  This new framework is based on the RESTFul Web Service (JAX-RS) standard.  By design, it works with any type of business object, making it possible to implement the connector as a single web application that can be accessed by the gateway for synchronization or by the client directly for any other reason.  This design makes it quite simple to tap into existing corporate data and make that data mobile.

I was excited to try out this new API.  In my search for some pre-existing  data that would interest a wide audience, I found Salesforce, a company that provides a free 90-day developer account for evaluation and a web service to  their repositories.  This was ideal because it enabled me to implement the connector so anyone with a Salesforce developer account who is running the Sun GlassFish Mobility Platform 1.1 software can access their own account data either by writing their own application to talk to the connector or by running our client application.

The Salesforce connector is pre-installed in the Sun GlassFish Mobility Platform 1.1 release.  It is a demonstration of the JAX-RS connector features; it is not production grade. Only a subset of Salesforce account, contact, and task data is made available to the client application by this connector.  The connector supports creating, updating, and deleting task data, as well as updating contact data and reading, but not modifying or deleting, account data.

Before we examine the connector design and explore some of the implementation challenges, let's take the connector and client application for a test drive.  Here's what you need to do:

  1. Download Sun GlassFish Mobility Platform 1.1 and the client bundle available here.
  2. Install Sun GlassFish Mobility Platform 1.1.  The installation process first installs GlassFish v2.1 and then Sun GlassFish Mobility Platform.  The Salesforce connector is a default component in this release.  There are no special steps you need to take to activate it.  It is running when the installation completes.
  3. Get a Salesforce Developer account by registering at  http://SalesForce.com/developer.
  4. Get a security key for your Developer account.  The security key is required to be used with the web service.
      • Login to your Salesforce Developer account.
      • Select the "Setup" link at the top of the page.
      • Select the "Reset your security token" text on this page.
      • Follow the directions.  A security key will be emailed to you.
  5. Unzip the client bundle and find the salesforce-ws.jad and salesforce-ws.jar files.
  6. Follow the directions provided in the section, "Deploying the Salesforce Sample Client Application" of the Deployment Guide available here  to run the client.

Here are some screen shots of the Salesforce client application.

            Login Screen                                   List of accounts  in my developer account
SalesForce mobile client login screen

Company GenePoint account details
                                                                         A GenePoint  Contact

 A Task on GenePoint




   In Part II, I'll discuss the JAX-RS connector interfaces.



pageicon Monday Apr 16, 2007

Jaxb: Adding Behaviors

You can add behaviors to jaxb generated classes by subclassing and overriding methods.  This technique is useful when integrating the generated classes into an existing codebase, and when data alteration is needed post unmarshalling or during marshalling.   Currently there is no mechanism to alter data during the  unmarshalling process.  The unmarshalling process entails writing the data into the jaxb generated class object (i.e. superclass) of any subclass you provide, however each subclass  will be able to access the  data from its superclass post unmarshalling.

 

There are several implementation rules that must be followed when adding behaviors to jaxb generated classes.

  • You must create a subclass of the jaxb generated ObjectFactory  class and it must be tagged with the @XmlRegistry annotation.
  • You must override any of the methods which are to provide a new subclass.
  • Your subclasses may use the same package name as the jaxb generated classes however it is recommend to put them in a separate package and it not be the empty package.
  • Your new ObjectFactory class will be identified to the JAXContext  by setting property "com.sun.xml.bind.ObjectFactory" to an instance of this class.
Here is a simple example.  We have a simple schema for a book inventory, books.xsd.  xjc is used to generate the classes from the schema. A package name of gbooks.inv is defined for these classes.  The output is written into the b directory.

                 mkdir b
                 xjc -d b -p gbooks.inv books.xsd

 


   books.xsd

      <?xml version="1.0"?>
      <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
         xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
         jxb:version="2.0">

         <xsd:element name="BookInventory">
             <xsd:complexType>
                 <xsd:sequence>
                     <xsd:element name="Book" type="bookType"
                         minOccurs="0" maxOccurs="unbounded" />
                 </xsd:sequence>
             </xsd:complexType>
         </xsd:element>

         <xsd:complexType name="bookType">
             <xsd:sequence>
                 <xsd:element name="isbin" type="xsd:string" />
                 <xsd:element name="title" type="xsd:string" />
                 <xsd:element name="price" type="xsd:double" />
             </xsd:sequence>
         </xsd:complexType>
      </xsd:schema>



Three classes are generated from this schema, ObjectFactory, BookInventory, and BookType.


gbooks/inv/ObjectFactory.java

      @XmlRegistry
      public class ObjectFactory {

         public BookInventory createBookInventory() { return new BookInventory(); }
         public BookType createBookType() { return new BookType(); }
      }


   gbooks/inv/BookInventory.java

      public class BookInventory {
         public List<BookType> getBook() {
            if (book == null) { book = new ArrayList<BookType>(); }
            return this.book;
         }
      }


   gbooks/inv/BookType.java

      public class BookType {

          public String getIsbin() { return isbin; }
          public void setIsbin(String value) { this.isbin = value; }
          public String getTitle() { return title; }
          public void setTitle(String value) { this.title = value; }
          public float getPrice() { return price; }
          public void setPrice(float value) { this.price = value; }
      }


This example adds behaviors to BookType.  Class RM_BookType is extended from  the jaxb generated class gbooks.inv.BookType and the methods of interest are overridden.  Text is prepended to the isbin string and written back into the superclass, BookType.  Any future references to this value in object BookType will be the new value.  10% is added to the original price. This change occurs only when class object RM_BookType is referenced.


   RM_BookType

      package read.more;

      public class RM_BookType extends gbooks.inv.BookType {

          @Override
          public void setIsbin(String value) {
                //- check for initial instance of this class;
                //- parent would not have a value for isbin.
                if (super.getIsbin() == null)
                        super.setIsbin(value);
                else
                        super.setIsbin(value + super.getIsbin());
          }

          @Override
          public double getPrice() {
                // add a 10% handling fee to book
                return  super.getPrice() * 0.1 ;
          }
      }


Next we must subclass from the jaxb generated ObjectFactory.  RM_ObjectFactory extends gbooks.inv.ObjectFactory.  It is annotated with @XmlRegistry as is required.  It has a package name of read.more. Method createBookType is overridden in order to provide an instance of RM_BookType.


   RM_ObjectFactory

      package read.more;

      import javax.xml.bind.annotation.XmlRegistry;
      import gbooks.inv.BookType;

      @XmlRegistry
      public class RM_ObjectFactory extends gbooks.inv.ObjectFactory {

          @Override
          public BookType createBookType() {
                return new RM_BookType();
          }
      }


Here is a program to demonstrate the use of these new classes.  We have hardcoded the input file name, line 2.  Line 4, the JAXBContext is defined with the jaxb generated classes as one normally would; the JAXBContext is not define pointing to the package with your subclasses.  Line 6 identifies RM_ObjectFactory for jaxb to use. Line 8 we add a new book to the inventory. Lines 14 and 15 demonstrates the new functionality that is implemented.


1      public class Test {
2        public static final String XML_TEST_FILE = "BookList.xml";
3        public static void main(String[] args) throws Exception {

4           JAXBContext context = JAXBContext.newInstance("gbooks.inv");
5           Unmarshaller u = context.createUnmarshaller();

6           u.setProperty("com.sun.xml.bind.ObjectFactory",new RM_ObjectFactory());

7           BookInventory bi = (BookInventory)u.unmarshal(new File(XML_TEST_FILE));

            //- add something new to the list
8           RM_BookType rmb = new RM_BookType();
9           rmb.setIsbin("9932-3423");
10          rmb.setTitle("Partial Math");
11          rmb.setPrice(0.40);
12          bi.getBook().add(rmb);

13          for(BookType b: bi.getBook()){
14             b.setIsbin("ACCT_");
15             System.out.println("isbin: " + b.getIsbin() + "\tprice: " +
               b.getPrice());
            }
         }
      }


Here is some sample data


    BookList.xml

      <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
          <BookInventory>
              <Book>
                  <isbin>1209-01</isbin>
                  <title>New Math</title>
                  <price>40.00</price>
              </Book>
              <Book>
                  <isbin>2209-01</isbin>
                  <title>Old Math</title>
                  <price>4.00</price>
               </Book>
           </BookInventory>


The output should look like this.

 


      isbin: ACCT_1209-01      price: 44.0
      isbin: ACCT_2209-01      price: 4.4
      isbin:ACCT_9932-3423     price:0.44000000000000006


A zip file containing the sources for this example can be found here

The provided ant file requires you set 3 properties in order to resolve several Jar file references.

pageicon Tuesday Feb 06, 2007

Jaxb custom accessor for marshalling and Hibernate.

Levi Purvis a user of both Jaxb and Hibernate asked for hooks to com.sun.xml.bind.v2.runtime.reflect.Accessor in order to have better interaction between the two technologies in the area of field and property access.  In particular Levi wanted a way to enable Jaxb marshalling to interact better with Hibernate's lazy proxy strategy.  Currently Jaxb marshalling with an active lazy proxy strategy will trigger one of two responses,  a LazyInitializationException or a graph of objects will be marshalled; the graph has the potential for being large.

Levi provided the base implementation for Jaxb API hooks to support this enhancement.  I integrated his code into the Jaxb 2.1 branch with minor changes.  The code design consists of an interface, an annotation, and a propery for JAXBContext.  AccessorFactory is the interface a user must implement.
It consists of two methods createFieldAccessor and createPropertyAccessor.  Each returns an object of type com.sun.xml.bind.v2.runtime.reflect.Accessor.  Accessor is an abstract class (see the JAXB API).  The user extends it and implements the desired behavior.  The user provides an implementation of the AccessorFactory and through it provides his custom accessors to the Jaxb marshaller.

   package com.sun.xml.bind.v2.runtime.reflect;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    public interface AccessorFactory {
       /**
        * Access a field of the class.
        *
        * @param bean the class to be processed.
        * @param f the field within the class to be accessed.
        * @param readOnly  the isStatic value of the field's modifier.
        * @return Accessor the accessor for this field
        */
        Accessor createFieldAccessor(Class bean, Field f, boolean readOnly);

        /**
         * Access a property of the class.
         *
         * @param bean the class to be processed
         * @param getter the getter method to be accessed. The value can be null.
         * @param setter the setter method to be accessed. The value can be null.
         * @return Accessor the accessor for these methods
         */
        Accessor createPropertyAccessor(Class bean, Method getter, Method setter);  
}

   /**    MyCustomFieldAccessor code snippit **/
   import com.sun.xml.bind.v2.runtime.reflect.Accessor;
  public class MyCustomFieldAccessor<BeanT,ValueT>
        extends Accessor<BeanT,ValueT>{
        public MyCustomFieldAccessor() { }
        public ValueT get(BeanT bean) { return hideLazy(bean); }
        protected ValueT hideLazy(ValueT value) {
             if (Hibernate.isInitialized(value)) { return value; }
                return null;
        }

   }


   /**  MyAccessorFactoryImpl  code snippit **/
   import com.sun.xml.bind.AccessorFactory;
   public class MyAccessorFactoryImpl implements AccessorFactory {
        public MyAccessorFactoryImpl(){}
        Accessor createFieldAccessor(Class bean, Field f, boolean readOnly){
            //- use my custom accessor
            return new MyCustomFieldAccessor();
        }
        Accessor createPropertyAccessor(Class bean, Method getter, Method setter){
             //-- Use Jaxb's default accessors.
             if (getter == null) {
                return new Accessor.SetterOnlyReflection(setter); }
             if (setter == null) {
                return new Accessor.GetterOnlyReflection(getter);
             }
             return new Accessor.GetterSetterReflection(getter, setter);
        }
    }

XmlAccessorFactory is the annotation to use to designate which of your code is to use the custom accessors.  This is a package level annotation and must reside in a package-info.java file in the appropriate directories.


    package com.sun.xml.bind.v2.runtime.reflect;

    import static java.lang.annotation.ElementType.PACKAGE;
    import static java.lang.annotation.ElementType.TYPE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    @Retention(RUNTIME)
    @Target({TYPE,PACKAGE})
    public @interface XmlAccessorFactory {
        Class<? extends AccessorFactory> value();
    }



    /** example package-info.java **/
    @XmlAccessorFactory(MyAccessorFactoryImpl.class)
    package test.jaxb.hibernate.accessor;
    import com.sun.xml.bind.XmlAccessorFactory;


The JAXBContext property is "com.sun.xml.bind.XmlAccessorFactory".  This string can be referenced via public variable JAXBRIContext.XMLACCESSORFACTORY_SUPPORT.  This property must be set to true on JAXBContext before marshalling in order for the custom
accessor to be used by the marshalling code.

    /** code snippit **/
    Map<String, Object> properties = new HashMap<String, Object>();
    properties.put(JAXBRIContext.XMLACCESSORFACTORY_SUPPORT, true);
    context = JAXBContext.newInstance(new Class[] { PassingUser.class,
              PassingSite.class }, properties);
    Marshaller m = context.createMarshaller();
    PassingUser u = issue.pass();
    m.marshal(u, System.out);



The Jaxb team would like to thank Levi for this solution.  We believe it will not only help Hibernate users but users of other scenarios were accessor control is also desired.

This enhancement is currently available in the jaxb-2_1-branch of jaxb-ri CVS repository and will be in the next binary release (2.1.3) of the ri.






« November 2009
SunMonTueWedThuFriSat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
     
       
Today

Feeds

Search this blog

Links

Weblog menu

Today's referrers

Today's Page Hits: 19