AnsweredAssumed Answered

An approach to making AJAX services available cross Domain

Question asked by rdanner on Sep 25, 2007
Latest reply on Sep 26, 2007 by davidc
Gents, Ladies,

I recently did a proof of concept for a restful web service with a companion set of java script libraries on top of it that I wanted to be cross domain.  As you may know AJAX has a security constraint that ensures you cannot make asynchronous calls to domains from which the original page was not served. 

I have found an approach (not mine) on the web which works for this and I wanted to post it here both to validate amongst the technical excellence which exists in our community and also to share.

The restful service part is a snap an the URLS and services themselves are not set in stone.  I am not overly concerned with these with the POF.  If we actually create a service – I will no doubt obsess over the right url and debated endlessly whether or not to use this for or that etc…

The challenge was to create the javascript library such that:
*  The consumer could use the library on any web page on any host/domain.

* They would only need to add a single <script> tag: like yahoo, google, etc.  Of course I need to use prototype and a number of other UI libraries etc.. but I don't want my consumers to sweat that – I just want them to think about using my services and creating new and interesting mash ups.


What does the service do?  It pulls quotes from a Bible search engine. 

The API has two non UI services retrieveCitatiationWithContext and retrieveCitation (without context).  So one will bring back the citation with marked up but with all of the surrounding text and the other will just bring back the citation.

The UI library creates popup bubbles for the citation (again – one with context the other without)
popupCitation("I Sam. 3:1 (to 1st ;)");
popupCitationWithContext("I Sam. 3:1 (to 1st ;)");


So a sample web page looks like the following:
<html dir="ltr">

  <head>
    <title>Sample use of Book JS library</title>
  </head>

  <body>
    <script src="http://MYSITE/bookservice/csbooks-ui.js" type="text/javascript"></script>

    <b>mouse over the citations</b><br/><br/>


    <b>with context</b><br/>
    <a onmouseover='popupCitationWithContext("LUKE3");' >Luke 3:1</a><br/>
    <a onmouseover='popupCitationWithContext("I Sam. 3:1 (to 1st ;)");' >1st Sam. Chapter 3, Line 1 to the first ;</a><br/>

    <br/><b>without context</b><br/>
    <a onmouseover='popupCitation("LUKE3");' >Luke 3:1</a><br/>
    <a onmouseover='popupCitation("I Sam. 3:1 (to 1st ;)");' >1st Sam. Chapter 3, Line 1 to the first ;</a><br/>

  </body>
</html>




The first task was to make sure there was only one script to include:
<script src="http://MYSITE/bookservice/csbooks-ui.js" type="text/javascript"></script>

This script needs to take care of importing:
        *   csbooks.js (The non UI book javascript library)
   *   wz_tooltip.js (tool tips library)
   *   tip_centerwindow.js (more tool tips)
   *   tip_balloon.js (bubble extension – oh definitely, the citation has to pop up in a bubble – anything else would be anti-web 2.0)
   *   prototype.js


It is pretty easy to include other javascript files dynamically. You simply write the script tag in to the DOM at execution time.



document.write("<script src='"+mBooksServiceUrl+"/csbooks.js'                   type='text/javascript'></script>");
document.write("<script src='"+mBooksServiceUrl+"/lib/tips/wz_tooltip.js'       type='text/javascript'></script>");
document.write("<script src='"+mBooksServiceUrl+"/lib/tips/tip_centerwindow.js' type='text/javascript'></script>");
document.write("<script src='"+mBooksServiceUrl+"/lib/tips/tip_balloon.js'      type='text/javascript'></script>");

   
The next task is getting around the AJAX security constraint.  This is accomplished by not using AJAX – or what is typically thought of as AJAX which is an Asynchronous HTTP request. 
Instead we dynamically write a script tag in to the head of the document and source the script to the service with the name of a call back function as a parameter.  The service then responds in the form of a function:callback(RESPONSE DATA) which will be evaluated by the browser when it receives the script source (remember – it's expecting script - yes, the down side is… this can be used/exploited for evil…)

I am fond of passing 2 call back function names and a ID. The ID is the ID of the script tag you created,  the first call back is the internal callback which should be used to clean up the script tag dynamically, and the second callback is the consumer call back.

I don't have it coded here but in handleCitationResponse(pJsonData) you would get the call id from the response and then get the script tag element by ID and remove it.


What you are doing is kind of simulating a ASYNC call.  Your first function called by the consumer dynamically writes the script tag in to the DOM and exits.  The browsers will then send a request to retrieve the response (ASYNC) which you will return with an instruction to call back a function with the results.  Wah-lah you have an AJAX call without "AJAX" and without the domain limitations of that API.



Rolling over the 1st Sam text will cause the following to be dynamically written in to the head of the document:

<script id="JscriptId3" type="text/javascript" charset="utf-8" src="http://MYSITE/bookservice/citation/nocontextl?reference=I Sam. 3:1 (to 1st ;)&output=json&icallback=handleCitationResponse&callback=popupCitationCallback&noCacheIE=1190706528325">
</script>

which will return:


popupCitationCallback(handleCitationResponse({"ResultSet":{"Result":[{"book":"","reference":"I Sam. 3:1 (to 1st ;)","content":"
   And the child Samuel ministered unto the Lord before Eli. And the word of the Lord was precious in those days;
"}]}}));

Which will first call the inner function handleCitationResponse which parses the response and (should) remove the script tag and the returns the content which is picked up as input to the consumer function which is the popup callback which actually renders the popup.


UI Library
(APIs to create popup bubbles with in context and no context citations)

    
    /**
     * retrieve a given citation and pass the name of call back method which will generate
     * the actual UI
     * @param pReference    book reference to retrieve
     */
    function popupCitationWithContext(pReference)
    {
        retrieveCitationWithContext(pReference, "popupCitationWithContextCallback");
    }

    /**
     * retrieve a given citation
     * @param pContent
     */
    function popupCitationWithContextCallback(pContent)
    {
        Tip(pContent, BALLOON, true, ABOVE, true, OFFSETX, -10, WIDTH, 360, TEXTALIGN, 'justify', FADEIN, 600, FADEOUT, 600, PADDING, 30);
    }


Non UI Library
API for getting the citations in both forms and underlying implementation functions and classes for getting making requests to the actual services – all the script creation, removal machinery

    /**
     * retrieve a given citation
     * @param pReference    book reference to retrieve
     * @param pCallback   method to callback
     */
    function retrieveCitationWithContext(pReference, pCallback)
    {
        fireRetrieveCitation(pReference, pCallback, true, true);
    }


    /**
     * retrieve a given citation
     * @param pReference    book reference to retrieve
     * @param pCallback            method to callback
     */
    function retrieveCitation(pReference, pCallback)
    {
        fireRetrieveCitation(pReference, pCallback, false, false);
    }


    /**
     * Make a call to the underlying book service to get the requested reference
     * – for the moment this is only available as HTML
     * @pReference                book reference to retrieve
     * @pCallback                 function to call back with result 
     * @param pWithContext     with context (surrounding text)
     * @param pWithMarks        with marks [[[ citation ]]]
     */
    function fireRetrieveCitation(pReference, pCallback, pWithContext, pWithMarks)
    {
        var vServiceUrl = "";
        var vQueryString = ""; 
        vJsonRequest = null;

          /* determine URL for service */
          if(pWithContext==true)
          {
              vServiceUrl = mInContextReferenceServiceUrl;
          }
          else
          {
              vServiceUrl = mNoContextReferenceServiceUrl;
          }

          /* build query string */
          vQueryString = 'reference='+pReference+'&output=json&icallback=handleCitationResponse&callback='+pCallback;


        /* create/fire "request" */
        vJsonRequest = new JSONscriptRequest(vServiceUrl+'?'+vQueryString);
        vJsonRequest.buildScriptTag();
        vJsonRequest.addScriptTag();
    }


    /**
     * handles the asynchonous call back from the the underlying book service
     * @param pRequest   access to the request and response text
     * @param pCallback   function to call back
     */
    function handleCitationResponse(pJsonData)
    {
       var vResponseText =  pJsonData.ResultSet.Result[0].content;

       /* TODO I don't agree with using a global – pass the id and remove the element based on the ID */
       //vJsonRequest.removeScriptTag();
 
       return vResponseText;
    }



    /**
     * JSONscriptRequest – a simple class for making HTTP requests
     * using dynamically generated script tags and JSON
     * Author: Jason Levitt
     * Date: December 7th, 2005
     */
 
     /**
      * Constructor – pass a REST request URL to the constructor
      */
     function JSONscriptRequest(pFullUrl)
     {
        this.fullUrl = pFullUrl;                   // REST request path
        this.noCacheIE = '&noCacheIE=' + (new Date()).getTime();           // Keep IE from caching requests
        this.headLoc = document.getElementsByTagName("head").item(0);      // Get the DOM location to put the script tag
        this.scriptId = 'JscriptId' + JSONscriptRequest.scriptCounter++;   // Generate a unique script tag id
     }

     /**
      * Static script ID counter
      */
     JSONscriptRequest.scriptCounter = 1;

     /**
      * buildScriptTag method
      */
    JSONscriptRequest.prototype.buildScriptTag = function ()
    {

        /* Create the script tag */    
        this.scriptObj = document.createElement("script");
   
        /* Add script object attributes */
        this.scriptObj.setAttribute("type", "text/javascript");
        this.scriptObj.setAttribute("charset", "utf-8");
        this.scriptObj.setAttribute("src", this.fullUrl + this.noCacheIE);
        this.scriptObj.setAttribute("id", this.scriptId);
    }

    /**
     * removeScriptTag method
     */
    JSONscriptRequest.prototype.removeScriptTag = function ()
    {
        /* Destroy the script tag */
        this.headLoc.removeChild(this.scriptObj); 
    }

    /**
     * addScriptTag method
     */
    JSONscriptRequest.prototype.addScriptTag = function ()
    {
        /* Create the script tag */
        this.headLoc.appendChild(this.scriptObj);
    }



This approach was provided by  Jason Levitt on XML.com at http://www.xml.com/pub/a/2005/12/21/json-dynamic-script-tag.html

It seems to work very well.  I thought it might be helpful to some here or that others may know of better solutions.

Outcomes