Tuesday, September 30, 2008

The ABCs of Google Health Data API

Recently, I have been involved in a small project to aggregate medications from various Personal Health Records (PHR), including Google Health.

The documentation for the Google Data APIs Client Library and the Health Data API is quite complete and extensive. But it can be also be quite complex for someone who want to develop quickly a prototype or just test a proof of concept using these APIs.

Since I was going to develop my project in Java, I first downloaded he most recent GData Java Client Library (version 1.22 with samples - 6.8 MB).

The sample code can be used out of the box for a simple stand-alone java testing program.

I did import the sample code and the library in my IDE (eclipse) and just changed the credentials (sample.credentials.username and sample.credentials.password) in gdata/java/build-samples/build.properties using my google health account login and password.

To run the Google Health sample program, at the prompt under ./gdata/java just run: ant -f build-samples.xml sample.health.run
The next step for me was to reuse the sample code to develop a web application (I am creating small Liferay JSP based portlets).

Google offers two platforms for the developers to test their applications:
For my small web project I initially tested my code against H9, as recommended by Google.
The main reason is because the authentication with H9 is more simple than with Google Health production.

The application I wanted to build is supposed to connect to various PHRs (ICW LifeSensor, Microsoft Health Vault and Google Health/H9) and retrieve the medication lists. For Google I don't need to enter a login and password in my web application since I am going to use Google AuthSub mechanism:



By clicking on the Google 'connect' button, I am redirected to the Google AuthSub web site:

     <input id="btn_connect_google" name="Disconnect" type="submit"
         value="Connect" onClick="connectToGoogle();return false;"/>

Here is the Javascript function I use for URL redirection:

     function connectToGoogle() { 
       if (btn_connect_google.value == "Connect") {
          window.location.href = googleAuthSubURL.value;
       }
       else {
          btn_connect_google.value = "Connect"; 
       }
     }
I construct the AuthSub URL in advance in my JSP java code:
public String getGoogleAuthSubURL(HttpServletRequest request) {
     
       gh9s = new H9Service("ICW-H9Medications-1.0");

       String nextUrl = "http://localhost:8080/sample/hello.jsp";
       String scope = "https://www.google.com/h9/feeds/";
     
       boolean secure = false;
       boolean session = true;
    
       String authSubLink = AuthSubUtil.getRequestUrl(nextUrl, scope, secure, session);
       authSubLink += "&permission=1";
       authSubLink = authSubLink.replaceFirst("/accounts/AuthSubRequest", "/h9/authsub");
     
      return authSubLink;
   }
I store the URL in an hidden field:

   <input type=hidden id="googleAuthSubURL" value="<%= getGoogleAuthSubURL(request) %>" >

You can refer to the Google Data API and Google Health API, including security settings for AuthSub Request and application identification (for logging purpose).

I am redirected to the Google AuthSub WebSite where I can choose which H9 account I want to use.




Then I am redirected back to my original application, with a single use token associated to the request. I can then parse the request and extract the token that I exchange for the token session:
  http://localhost:8080/sample/hello.jsp?token=1%2FBtOEh.....lsYenOFQ&permission=1
  if (gh9s != null)  {        
      try {
        
         // Extract Single Use Token for the session
         String queryString = request.getQueryString();
                  if (queryString != null) {
           
                      String singleUseToken = AuthSubUtil.getTokenFromReply(queryString);
                 
                      if (singleUseToken != null) {
                        String sessionToken = AuthSubUtil.exchangeForSessionToken(URLDecoder.decode(singleUseToken, "UTF-8"), null);
                        gh9s.setAuthSubToken(sessionToken);
                        System.out.println("Google SetAuthSubToken");
                          
                        URL profile_url = new URL(PROFILE_DEFAULT_PATH);
                        System.out.println("Google ProfileURL:"+profile_url);
                        gh_feed = gh9s.getFeed(profile_url, ProfileFeed.class);
               
                        createReferences(gh_feed.getEntries());
                      }
                  }
            } catch (AuthenticationException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            } catch (ServiceException e) {
                throw new RuntimeException(e);
            }
        }

For my portlet version (Liferay), I just had to replace the definition of the current URL:
   String singleUseToken = AuthSubUtil.getTokenFromReply(getCurrentURL(request).getQuery());

   private URL getCurrentURL(HttpServletRequest request) {
    try {
       return(new URL(request.getScheme(), request.getServerName(), request.getServerPort(), request.getAttribute("CURRENT_URL").toString())); 
    } catch (MalformedURLException e) {
    throw new RuntimeException(e);
    }
   }
The next step is to obtain medication entries by parsing the Google CCR data feed:

  private void createReferences(List entries) { 
     if (entries != null) {
        for (ProfileEntry entry : entries) {
           System.out.println("Google CCR:"+entry.getContinuityOfCareRecord().getXmlBlob().getBlob());
                 GoogleCCRWrapper ccrWrapper = getDomRepresentation(entry);
                 if (ccrWrapper.containsMedications()) 
                    medication_list.add(ccrWrapper.getMedication());
                    source_list.add(SRC_GOOGLE_HEALTH);
         }
      }
   }

The GoogleCCRWrapper java class parses the CCRg XML data feed:

    public GoogleCCRWrapper(String xml) {
        DocumentBuilder builder = null;
        try {
            builder = documentBuilderFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException("Error creating DocumentBuilder.", e);
        }
        
        try {
            document = builder.parse(new ByteArrayInputStream(xml.getBytes()));
        } catch (SAXException e) {
            throw new RuntimeException("Error parsing XML.", e);
        } catch (IOException e) {
            throw new RuntimeException("Error parsing XML.", e);
        }
    }
public boolean containsMedications() {
        return document.getElementsByTagName("Medications").getLength() > 0;
    }

    public String getMedication() {   
     Element e = getElementFromDocument("Medications");
     e = getElement(e, "Medication");
     e = getElement(e, "Product"); 
     e = getElement(e, "ProductName");
     e = getElement(e, "Text"); 
     return getText(e);
    }

Here is the resulting portlet (Google Health and Lifesensor Medications):