Barnaby James

December 07, 2006

Acrobat / Reader 8.0 REST Web Service support

Acrobat / Reader 8.0 has improved support for JavaScript networking which makes it easy to communicate with XML web services when combined with new support for XML processing. SOAP Web Services have been supported since Acrobat / Reader 6.0 however support for HTTP networking makes it possible to develop clients for a wider array of protocols - for example WebDAV, the Atom Publishing Protocol and other types of RESTive services like the Approver.com API. ECMAScript for XML (E4X) greatly simplifies producing and consuming XML messages which is really useful when interacting with XMLweb services.

Many of the collaboration workflows in Acrobat (for example the Review Initiation wizards) are developed in JavaScript because it's a more rapid development environment. As a result, we are also a customer of the JavaScript APIs that we add and the new Net.HTTP object is a good example of this.

E4X Support

Reader uses the Mozilla Spidermonkey JavaScript engine (it's also used by the Firefox web browser) and they recently added support for E4X - ECMAScript For XML. It allows you to imbed XML directly in your JavaScript, parse XML to a DOM, manipulate an XML DOM and serialize a DOM back to XML. E4X is also supported by the Flash Player ActionScript engine since version 9.0.

In the past, creating XML messages in JavaScript involved concating strings together like this:

var msg = "This is a bad way to generate XML!";
var myXML = "<element>" + msg + "</element>";

Things get more complicated because you need to entity encode the string to make sure it is valid XML and if you want to manipulate the XML later, you would need to write an XML parser. By contrast, with E4X you can use an XML "template" that will substitute in JavaScript values:

var msg = "This is a better way to generate XML!";
var myXML = <element>{ msg }</element>

The result is an XML object that you can manipulate and then serialize to a string. A more complicated example:

var dogML = <dogs>
   <dog gender="male">
      <name>Rover</name>
      <breed>Labrador</breed>
      <color>black</color>
   </dog>
   <dog gender="male">
      <name>Spot</name>
      <breed>Jack Russell</breed>
      <color>brown</color>
   </dog>
</dogs>

// Access the DOM with JavaScript expressions
console.println("Name: " + dogML..dog[0].name);
console.println("Color: " + dogML..dog[1].color);

for each (var h in dogML..breed)  // Iterate over an XML Element
{ 
  console.println("Breed: " + h) 
};

// Evaluate a predicate over the DOM
console.println("Rover's Color: " + dogML.dog.(name=="Rover").color);

// Serialize back to an XML string
console.println("XML: " + dogML.toXMLString());

The output:
Name: Rover
Color: brown
Breed: Labrador
Breed: Jack Russell
Rover's Color: black
XML: <dogs>
   <dog gender="male">
      <name>Rover</name>
      <breed>Labrador</breed>
      <color>black</color>
   </dog>
   <dog gender="male">
      <name>Spot</name>
      <breed>Jack Russell</breed>
      <color>brown</color>
   </dog>
</dogs>

Alright - you get the idea. There are lots of tutorials about using E4X - I've put some links at the end of this article. One important thing to keep in mind when adding JavaScript to PDF documents - E4X language features cannot be processed by pre-8.0 Acrobat / Reader so Document JavaScript using E4X will most likely not run in older versions.

HTTP Networking support

Much like the varous XMLHttpRequest objects in the browser, Reader now has support for asynchronously invoking Web Services using HTTP. It works more or less as you would expect and is an extension of the SOAP support that has been in Acrobat for a while. The Net.HTTP.request() method allows you to specify the HTTP verb, the request headers and the body and will return the response headers and response body. For security reasons, this method is only available when executed outside of a document (such as the JavaScript console, a JavaScript .js file or a Tracker application) - the intention is to make it possible to extend Reader / Acrobat without resorting to writing a C++ plugin.

Allowed HTTP verbs
HTTP (RFC 1945, 2616)
GET, POST, PUT, DELETE, OPTIONS, HEAD
WebDAV (RFC 2518)
PROPFIND, PROPPATCH, MKCOL, LOCK, UNLOCK, COPY, MOVE, ACL, SEARCH
WebDAV Versioning Extensions (RFC 3253)
CHECKIN, CHECKOUT, UNCHECKOUT, MERGE, VERSION-CONTROL, REPORT, MKWORKSPACE, UPDATE, LABEL, MERGE, MKACTIVITY
CalDAV Calendaring Extensions
MKCALENDAR

As a real world use case, here is an example of invoking the WebDAV PROPFIND method on a web server. PROPFIND is an request for the contents of a WebDAV collection - it more or less asks for the contents of a directory with various properties of the contents.

var EnumerateCollection = function(cURL)
{
    var params = 
    {
        cVerb: "PROPFIND", // Use the WebDAV PROPFIND Verb
        cURL: cURL,
        oHandler: // This is the handler for when the response is returned.
        {
            cURL: cURL,
            oBaseURL: util.crackURL(cURL),
            response: function(response, uri, e)
            {
                try
                {
                    if(e != undefined)
                    {
                        console.println("An error occurred: " + e);
                    }
                    else 
                    {
                        console.println("PROPFIND of " + cURL);
                        
                        // We get passed a stream object - convert it into a string
                        var string = util.stringFromStream(response);
                        
                        // E4X only parse XML fragments so we have to remove the
                        // XML Declaration
                        var xmlDeclMatcher = /^<\?xml version[^>]+?>/; 
                        string = string.replace(xmlDeclMatcher ,'')
                        string = string.replace(/\n/g ,'')

                        var msg = XML(string);

                        // WebDAV responses are all in the DAV XML Namespace so
                        // we when we refer to a node, we need to use the XML QName
                        var ns = new Namespace("DAV:");
                        var responses = msg..ns::response;

                        for(var i in responses)
                        {
                            var resp = responses[i];
                            var prop = responses[i].ns::propstat.ns::prop;
                            var resource = {};

                            // ensure that the resource URL begins with the base URL
                            var href = resp.ns::href;
                            // A better check here would be to parse the URL 
                            // and check it's scheme
                            if(href.charAt(0) == '/')
                            {
                                // This is a relative URL so append it to the base URL
                                with(this.oBaseURL)
                                {
                                    // Relative to base href
                                    resource.url = cScheme;
                                    resource.url += "://";
                                    resource.url += cHost;
                                    if(nPort != undefined)
                                        resource.url += ":" + nPort;

                                    resource.url += href;
                                }
                            }
                            else
                            {
                                // Absolute URL
                                resource.url = href.toString();
                            }

                            // Extract properties from the resource element
                            resource.displayName = prop.ns::displayname;
                            resource.lastModified = prop.ns::getlastmodified;
                            resource.contentLength = prop.ns::getcontentlength;
                            resource.contentType = prop.ns::getcontenttype;
                            resource.etag = prop.ns::getetag;
                            resource.type = prop.ns::resourcetype;

                            if(resource.url == this.cURL)
                            {
                                // Don't include the collection we want the contents of
                            }
                            else if(resource.type.ns::collection == undefined)
                            {
                                console.println(resource.displayName + " / " + 
                                resource.contentLength + " bytes / " + 
                        resource.contentType);
                            } else console.println("/" + resource.displayName);
                        }
                    }
                }
                catch(e)
                {
                    console.println("EXCEPTION: " + e);
                    e.printStackTrace();
                }
            }
        },
        aHeaders: // HTTP Request Headers
        [
            { name: "Depth", value: "1" } // Set the depth so it's not recursive
        ]
    };

    Net.HTTP.request(params); // Invoke the service asynchronously
}
EnumerateCollection("http://example.org/webdav/");

You can run this code (with your own real WebDAV server URL) in the Acrobat JavaScript debugger. I used an Apache Tomcat server and it produced the following:

PROPFIND of http://example.org/webdav/
/SubCollection
Sample.xml / 2106 bytes / text/xml
Sample.pdf / 285856 bytes / application/pdf

In this case, there were three items in the WebDAV collection:

File Description
SubCollectionWebDAV Collection
Sample.xml2k XML file
Sample.pdf285k PDF document

You could use the Net.HTTP.request() method again with the HTTP GET verb to download one of these file into Acrobat.

Resources

Posted by Barnaby James at 09:38 AM on December 07, 2006

Trackbacks

TrackBack URL for this entry:
http://blogs.adobe.com/cgi-bin/mt-tb.cgi/280

Comments

John Dowdell — 02:31 PM on December 07, 2006

Thanks, Barnaby. Is this related to today's Lifehacker tip on using Reader 8 as an RSS reader?
http://lifehacker.com/software/adobe-acrobat/use-adobe-reader-8-as-an-rss-reader-220060.php

jd

Barnaby James — 03:07 PM on December 07, 2006

Thanks - I had not seen that link. The Tracker is actually built on top of the Synchronizer (which I wrote about yesterday - http://blogs.adobe.com/barnaby.james/2006/12/the_adobe_synch.html) to allow it to work disconnected. The JavaScript networking support is what we used to develop the Shared Review initiation wizards that talks to WebDAV and Sharepoint workspaces.

Robert Arnold — 07:31 AM on December 11, 2006

If I read this correctly, to use these methods I must "install" my JavaScript on the client machine (in the JavaScript directory) in order for them to work. Additionally, the client application (let's say, Reader) must be restarted. And that means the user must close out all instances of their Web browser and wait some amount of time for Reader to close out. If true, I see no practical use for these methods and it would require distributing an installer along with a PDF document. While I appreciate the security issues, the alternative of requiring an installer seem more precarious from a user’s point of view.

Barnaby James — 09:20 PM on December 11, 2006

You are correct that the methods cannot be called from document JavaScript. I think there are at least two deployment solutions for this type of code. If you are installing Reader into an Enterprise then you can use the Reader Installer Tuner which allows you to add JavaScript files to the install. The other possibility is to write a Tracker application that can add an initiation workflow. This is a more dynamic process and doesn't require an application restart. I'm planning on writing a post about how to do this in the near future.

Add your comments

Remember Me?

(You may use HTML tags for style.)