Latest Posts

Archives [+]

Archive for 'August 2010'

    Posted by Kas Thomas AUG 25, 2010

    Comment 1

    In previous posts, I've shown how to load movie data into CRX and how to render data for individual movies via HTML, SVG, and PDF. What I'd like to do now is show how easy it is to build interactivity into an app using a bit of AJAX combined with Sling's support for RESTful XPath-based search.

    It turns out that all we have to do to query the repository for, say, all nodes that have a value of "Hitchcock" under the property named "Director" is put together an XPath expression like

    //*[jcr:contains(@Director,'hitchcock')]

    and pass it to Sling in a URL that looks like:

    http://localhost:7402/content.query.json?queryType=xpath&statement=//*[jcr:contains(@Director,'hitchcock')]

    (assuming the repository is on port 7402 of localhost). This request will invoke a Lucene search of all nodes stored under the /content subtree. The results will come back as a JSON-formatted array:

    [
        {
            "name": "notorious",
            "jcr:path": "/content/films/notorious",
            "jcr:score": 3331
        },
        {
            "name": "under_capricorn",
            "jcr:path": "/content/films/under_capricorn",
            "jcr:score": 3331
        },
                . . .
    ]

    This is perfect, because it means we can use the JSON data to populate a dropdown menu (a "select" control in an HTML form) showing the names of films; and we can arrange things so that when the user clicks a "Show Details" button, the form updates to show detail information (title, director, year, genre, actor, actress, etc.) for the film in question. To get the detail information, of course, we can perform a behind-the-scenes AJAX query to the server. I already showed, in a previous post, how to render detail information for a given movie in an HTML page. All we really need to do at this point is put that HTML page into its own iframe, and (right next to it) add search controls to the page.

    The following form shows one possible way of handling things.

    file

     

    Basically, we have an HTML form in which there are two action buttons: One is a Search button ("Search Films by") that initiates an XPath-based search of the repository based on a user-chosen criterion of Title, Director, Year, Genre, Actor, or Actress. The other is a Show Details button, underneath a picklist of films. Clicking the Search button populates the picklist with hits. When the user chooses a hit from the list and clicks Show Details, the left side of the page updates with detail information.

    The form consists of 200 lines of JavaScript and markup, as follows:

    <html>
    <head>
    <script>

    var CRX_BASE_URL = "http://localhost:7402";

    function addEventListeners( ) {

            document.getElementById( "_Query_" ).addEventListener(
            "keypress", function( e ) {
                    if ( 13 == e.keyCode )
                    handleClick( null );
            },
            false );

            document.getElementById( "_QueryButton_" ).addEventListener(
            "click", handleClick, false );



            document.getElementById( "_Fetch_" ).addEventListener(
            "click", handleFetch, false );

    }

    function getSearchMode( ) {

            return document.getElementById("_Select_").value;
    }

    function handleFetch( e ) {

            var list = document.getElementById("_Hits_");

            if (list.value) {
                    var url =  CRX_BASE_URL + list.value + ".html";
                    var iframe = document.getElementsByTagName("iframe")[0];

                    // force a reload of the iframe:
                    iframe.src = url;
            }
    }

    // get user's input and call server
    function handleClick( e ) {

            var userData =
            document.getElementById( "_Query_" ).value;

            if ( !userData )
                return;   // nothing to do

            var CRX_QUERY_PATH = "/content.query.json?queryType=xpath&statement=";
            var GETheader = {
                    "Accept": "application/json",
            };

            var query = createXPathQuery( userData );
            var url = CRX_BASE_URL + CRX_QUERY_PATH + query;

            myHttpGet( url, GETheader, handleResponse ); // hit server
    }


    function myHttpGet( url, header, handler ) {

            try {
                    request = new XMLHttpRequest();
                    request.open("GET", url, true);
                    for (i in header)
                    request.setRequestHeader( i, header[i] );
                    request.onreadystatechange = handler;

                    request.send("");
            }
            catch(e ) {
                    alert("Problem sending request: " + e.toString());
            }
    }

    function handleResponse( ) {

            if (request.readyState == 4) {
                    showResults( request );
            }
    }

    function showResults( request )  {

            var json = request.responseText;

            var hits = eval ( json );

            if ( null == hits ) {
                    alert( "No hits were found." );
                    return;
            }

            display( hits );
    }


    function display( hits ) {

            var div = document.getElementById( "_Hits_" );

            if ( null == div )
                throw( "Problem getting div for hitlist." );

            showHitCount( hits.length );

            var markup = "";

            for (var i = 0; i < hits.length; i++) {
                    markup += "<option value=\"" + hits[i][ "jcr:path" ] + "\">";
                    markup += fixName( hits[i].name );
                    markup += "</option>";
            }
            div.innerHTML = markup;
    }

    function fixName( name ) {
            var tmp = name.split("_");
            for (var i = 0; i < tmp.length; i++)
                tmp[i] = capitalize( tmp[i] );
            return tmp.join(" ");
    }

    function capitalize(a) {

            return typeof a[0] == 'undefined'?
               "":a[0].toUpperCase() + a.substring(1);

    }

    function showHitCount( numberOfHits ) {
            var div = document.getElementById( "_hitcount_" );
            if ( null != div )
                div.innerHTML = ("Total hits: " + numberOfHits).italics();
    }

    // build xpath query url
    function createXPathQuery( userString ) {

            var xpathTerms = [];

            var querySemantics = " and ";

            // trim leading & trailing spaces off query
            var terms = userString.replace(/^\s+/,"").replace(/\s+$/,"");

            // split on whitespace
            terms = terms.split(/\s+/);

            var _queryBasis =  "//*[_#_]/@location";
            var mode = getSearchMode( );

            for ( var i = 0; i < terms.length; i++ )
                xpathTerms.push( "jcr:contains(@" + mode + ",'" + terms[i] + "')" );

            var query = _queryBasis.replace( '_#_', xpathTerms.join( querySemantics ) );

            return query;
    }

    </script>
    </head>


    <body onload="addEventListeners()">

    <iframe width="380" height="410" style="border:none" src="http://localhost:7402/content/films/wild_at_heart.html"></iframe>

    <span style="font-size:small;position:absolute;right:80px;top:7px;">
    <input type="text"     id="_Query_" size="25"/>
    <input type="button"   id="_QueryButton_" value="Search Films by:"/>

    <select id="_Select_">
    <option value="Title">Title</option>
    <option value="Director">Director</option>
    <option value="Actor">Actor</option>
    <option value="Actress">Actress</option>
    <option value="Year">Year</option>
    <option value="Subject">Genre</option>
    </select>
    <br/>

    <select id="_Hits_" size="10"></select><div id="_hitcount_"></div>
    <br/>
    <input type="button" id="_Fetch_" value="Show Details"/>


    </span>
    <div id="hitlist"></div>
    </body>
    </html>
     

    This form (movieForm.html), along with the data for 1700 films (and scripts and PDF files discussed in prior posts), is available in the zip file below, which can also be downloaded from Day Package Share. After installing the package, go to http://localhost:7402/apps/films/movieForm.html to see the form in action (assuming your CRX is on port 7402).

    * MovieApp-1.zip
    Sample code and data for MovieApp.

    Posted by Jean-Christophe Kautzmann AUG 24, 2010

    Add comment

    Apache Sling enables you to manage events within your application. Events can be used for example to trigger workflows or to run a business process like sending an email or managing assets that have been ingested into your application.

    The event mechanism is leveraging the OSGi Event Admin Specification. The OSGi API is very simple and leightweight. Sending an event is just generating the event object and calling the event admin. Receiving the event is implementing a single interface and declaring through properties which topics one is interested in.
    Sling introduces a special category of events called job events: unlike basic events, job events are garanteed to be processed. In other words: someone has to do something with the job event (do the job).

    For more details on the eventing mechanisms in Sling you can refer to the Eventing, Jobs and Scheduling page in the Sling documentation
    and to the javadocs for the OSGI and Sling API, especially the packages org.osgi.service.event and org.apache.sling.event.

    Another resource to get your hands on code is a step-by-step tutorial I created for the Sling documentation: it shows you how to listen to files uploaded to a temporary location in your web application and to move them to a specific location depending on their MIME types.

    Have fun eventing with Sling!

    Posted by Kas Thomas AUG 16, 2010

    Comments 4

    The first version of this post originally was published here.

    One of the things that gives Apache Sling a great deal of power and flexibility is the way it resolves script URLs. Consider a request for the URL

    /content/corporate/jobs/developer.html

    First, Sling will look in the repository for a file at exactly this location. If such a file is found, it will be streamed out as is. But if there is no file to be found Sling will look for a repository node located at:

    /content/corporate/jobs/developer

    (and will return 404 if no such node exists). If the node is found, Sling then looks for a special property on that node named "sling:resourceType," which (if present) determines the resource type for that node. Sling will look under /apps (then /lib) to find a script that applies to the resource type. Let's consider a very simple example. Suppose that the resource type for the above node is "hr/job." In that case, Sling will look for a script called /apps/hr/job/job.jsp or /apps/hr/job/job.esp. (The .esp extension is for ECMAScript server pages.) However, if such a script doesn't exist, Sling will then look for /apps/hr/job/GET.jsp (or .esp) to service the GET request. Sling will also count apps/hr/job/html.jsp (or .esp) as a match, if it finds it.

    Where things get interesting is when selectors are used in the target path. In content-centric applications, the same content (the same JCR nodes, in Sling) must often be displayed in different variants (e.g., as a teaser view versus a detail view). This can be accomplished through extra name steps called "selectors." For example:

    /content/corporate/jobs/developer.detail.html

    In this case, .detail is a selector. Sling will look for a script at /apps/hr/job/job.detail.esp. But /apps/hr/job/job.detail.html.esp will also work.

    It's possible to use multiple selectors in a resource URL. For example, consider:

    /content/corporate/jobs/developer.print.a4.html

    In this case, there are two selectors (.print and .a4) as well as a file extension (html). How does Sling know where to start looking for a matching script? Well, it turns out that if a file called a4.html.jsp exists under a path of /apps/hr/jobs/print/, it will be chosen before any other scripts that might match. If such a file doesn't exist but there happens to be a file, html.jsp, under /apps/hr/jobs/print/a4/, that file would be chosen next.

    Assuming all of the following scripts exist in the proper locations, they would be accessed in the order of preference shown:

    /apps/hr/jobs/print/a4.html.jsp
    /apps/hr/jobs/print/a4/html.jsp
    /apps/hr/jobs/print/a4.jsp
    /apps/hr/jobs/print.html.jsp
    /apps/hr/jobs/print.jsp
    /apps/hr/jobs/html.jsp
    /apps/hr/jobs/jobs.jsp
    /apps/hr/jobs/GET.jsp

    This precedence order is somewhat at odds with the example given in SLING-387. In particular, a script named print.a4.GET.html.jsp never gets chosen (nor does print.a4.html.jsp). Whether this is by design or constitutes a bug has yet to be determined. But in any case, the above precedence behavior has been verified.

    For more information on Sling script resolution, be sure to consult the (excellent) Sling Cheat Sheet as well as Michael Marth's previous post on this topic. (Many thanks to Robin Bussell at Day Software for pointing out the correct script precedence order.)

    Posted by Alexander Saar AUG 16, 2010

    Comments 2

    CRXDE Lite is a web-based repository browser for CRX's JCR repository and a development environment for CQ5 Platform in CRX, based on Apache Sling content delivery and development platform and Apache Felix OSGi runtime framework.

    In contrast to the CRX 1.x Content Explorer, which maintains a server-side CRX session, CRXDE Lite handles all modifications directly within the browser and uses the JCR remoting interface to retrieve content and persist changes.

    This article looks behind the scenes of how this rich set of functionalities was implemented in the browser. CRXDE Lite functionality and tips&tricks for using it were presented in the previous blog entry on CRXDE Lite.

    CRXDE Lite Design Goal. The most important design goal for CRXDE Lite was providing rich functionality with a near-desktop experience in a web application. There were three main architectural decisions we had to make while designing CRXDE Lite:

    • Which web Javascript framework to use for the user interface?
    • Where to host the CRXDE Lite web application?
    • How to implement remote access to JCR repository & server-side features from the browser user interface code?

    For the user interface framework the natural choice was the ExtJs library, which provides good user experience and is used in CQ5 Platform hosted in CRX. It also has a good internal architecture, separating the underlying model from the view.

    For the deployment model, we decided to host the CRXDE Lite application in CRX's web application. It minimizes the dependencies on other parts of the system, like OSGi container & Apache Sling content delivery platform. CRXDE Lite is available also when Apache Sling does not run, which helps in cases of system troubleshooting, recovery, etc.

    Transient space architecture. We were considering a number of approaches of providing remote interface to the JCR repository to the in-browser implementation of the user interface. In the end we decided to leverage CRX's JCR Remoting Server based on Jackrabbit JCR WebDAV Server, which provides an end-point for remotely accessing the repository. The JCR remoting protocol, extending WebDAV and adding DAVEx batch operations, is used as one of the possible remoting layers in the overall Apache Jackrabbit client-server SPI Architecture, upon which CRX is built. As the protocol is based on HTTP and uses JSON format, it is a good match for the user interface code written in browser's Javascript.

    The only, somewhat challenging, thing left to do was to implement a JCR remoting client on the browser side. We implemented a simplified JCR transient state layer (client code) in Javascript, leveraging ExtJS model classes.

    file

    ExtJS provides a good separation of model and view. In ExtJs, list-style content, like properties of a node, is stored in records which are maintained by so-called stores. Stores are responsible for retrieving and persisting the content and defines the serialization format and the server endpoint. For tree-style data, like JCR nodes, ExtJS defines a tree node type that handles the common properties of a node like display text, parent node or child nodes. When a tree node is assigned to a tree the rendering of that node is delegated to a separate configurable view class. Retrieval of data is managed by an instance of the tree loader class.

    For CRXDE Lite we make use of both, JCR nodes are represented as tree nodes and properties of a node are handled as records. To integrate with the JCR remoting server endpoint a custom tree loader was implemented that is able to deal with the JSON format that is used by JCR remoting and which creates nodes and records for the properties of a node.

    The records for properties can be displayed and edited in the property grid at the bottom. Once a record was edited it automatically gets marked dirty so we can easily find modified records when we want to persist our changes.

    Handling content changes. If a new node is created, it gets marked as transient and gets added to a list of transient nodes that is maintained by the store. Like with dirty records this allows for easy finding of new nodes in order to persist them. Deleting a node that is already persisted does not remove it automatically from the tree but just hides the according node and all its children. This allows to easily revert such changes by just displaying it again. We don't need to keep the path somewhere. If changes are persisted those nodes are deleted on the server first and only if this was successful they are deleted locally.

    When the changes that a user has made should be persisted, the transient storage generates a multi-part message body that is send via an AJAX call. Once all changes are persisted and the call returns successfully all dirty flags are removed from the according records, the list of transient nodes gets cleared and deleted nodes are removed from the tree.

    Note: The current version of CRXDE Lite persists all changes since the last save operation at once, which is similar to saving the changes made in a CRX Session. Future version might support more fine grained support for saving so you can just save single files like with a Desktop based IDE. While this is technically possible with JCR remoting, it will require some more research on display models and user feedback. Imagine you want to save a file whose parent is a transient node and not stored in the repository yet. In this case you either need to persist the parent automatically or prevent saving only that file in which case we need some means to find the parent that can be saved.

    Plugins. CRXDE Lite architecture is internally based on a plugin concept. The plugin architecture helped us to develop and maintain CRXDE Lite code in a clean, modular manner.

    Plugins are plain Javascript files that are loaded during CRXDE Lite loading phase. The implementor of a plugin is responsible for registering the plugin with the statically available plugin registry. This can be done by calling the following method:

    CRX.PluginRegistry.reg(ID, CRX.ide.MyAction);

    The first parameter has to be the ID of the widget that will be extended (e.g., the ID of a menu or toolbar) and the second will be the plugin class.

    Each plugin has to provide 2 static methods:

     

    • canHandle(context): check if plugin is active for the current context (e.g., menu item active for selected node
    • getInstance(context, args): return instance of the plugin

    Note: CRXDE Lite Plugin API is not yet a supported CRXDE Lite feature as of CRX 2.1 and is only used internally by the implementation. It may change or be removed in the next versions.

    At the moment plugins are not loaded from the repository but have to reside in the web-apps working directory and added to the index.jsp manually. One of the possible extensions in the future future would be to allow loading of the plugins directly from the repository so you could modify and adapt CRXDE Lite according to your needs.

    Posted by David Nuescheler AUG 12, 2010

    Comments 5

    For the past number of years I always found myself in the situations where I wanted to exchange fine-grained information between a typical current browser and a server that persists the information.

    In most cases for me the server obviously was a "Content Repository", but I think the problem set is more general and applies to any web application that manages and displays data or information.

    It seemed that every developer would come up with an ad-hoc solution to that very same problem of reading or writing fine-grained data at a more granular level than a resource.

    While there are definitely different protocol specifications like WebDAV or AtomPub to address the issue it seems like they are not very "browser" friendly, meaning that it takes a modern browser and a lot of patience with javascript to get to a point where one could interact with a server using either of the two.

    After talking to a number of people it seemed that this would be an interesting area to start a conversation about a specification approach to this.

    Please find the proposal on the IETF WebDAV mailing list here. Comments are very welcome.

    Posted by Florian Schaulin AUG 09, 2010

    Add comment

    I am pleased to announce the availablility of a Cumulative Feature Pack for Performance Improvements for CQ5.3. The Performance Pack is a Roll-up of 61 Performance related Hotfixes and Featurepacks, optimizing the CQ5 administration and usage on various aspects. The Knowledge Base contains an article listing all fixes (customer login required). Please verify if the individual features are related to your implementation before installing it. The Performance Pack is available on Day's Package Share.

    In case you want to receive automated notifications on news like this: there is an RSS feed for product updates.

    We have put together some slides on the Pack. In case you have a question please contact customengineering@day.com

    Posted by Kas Thomas AUG 06, 2010

    Add comment

    According to a survey released yesterday by Accenture (NYSE:ACN), 69 percent of organizations anticipate increased investment in open source in 2010, with 38 percent expecting to migrate mission-critical software to open source in the next twelve months.
     
    Accenture's survey of 300 large organizations (in both private and public sectors) found that half of respondents say they are fully committed to open source in their business. Another 28 percent say they are experimenting with open source and keeping an open mind to using it. Of organizations already using open source software, 88 percent say they will increase their investment in such software in 2010 compared to 2009.
     
    Interestingly, cost saving is no longer the primary driver behind open source adoption. According to the survey:

    • 76 percent of respondents in the UK and US cited quality as the key benefit of open source
    • 71 percent cited improved reliability
    • 70 percent cited better security/bug fixing

    "What we are seeing is the coming of age of open source," said Accenture chief technology architect Paul Daugherty. "Through both our research and our work with clients, we are seeing an increase in demand for open source based on quality, reliability, and speed, not just cost savings. This is a significant change from just two years ago when uptake was driven mainly by cost savings. We can expect to see this trend develop as open source continues to evolve and address even more business critical functions."

    Posted by Kas Thomas AUG 03, 2010

    Add comment

    I recently showed a simple way to render CRX content as PDF. The trick is to use a PDF form as a container, into which form data is imported using XFDF (the latter being Adobe's XML Forms Data Format). Populating a PDF form this way is very easy. But the form we ended up with last time was rather static, with no user interactivity to speak of. It simply showed movie data in a predetermined format.

    What would be good is if the user could, say, choose a film title from a scrolling list, then see the form update (in real time) with data for the selected film title. This turns out to be fairly easy to do.

    In addition to text fields, buttons, checkboxes, and radio buttons, PDF forms support the notion of a list box. A list box can be initialized with

    list.setItems( [ [Item1, exportValue1], [Item2,

       exportValue2 ] ... ] );

    where Item1, Item2, etc. are the names of list items that will be seen by the user and exportValue1, exportValue2, etc. are the corresponding values that get sent to the server in a POST request. This is quite handy, because it means we can arrange things so that if the user selects "Terminator 2" from a list of films, the value that gets sent to the server is "terminator_2.xfdf." This is exactly what we want.

    Our new form, containing the list box, looks like this:

    file

     

    The list box (on the right) needs to be initialized with a list of film names (and corresponding export values) at form-load time. For that to happen, we need an onload script that does something like

    var list = this.getField( "list" );
    list.setItems( movies );

    where movies is an array of arrays (containing items and export values) per the previous code snippet (above). The question is how to get the data for movies into the form. Either this information has to be hardcoded into the form to begin with (which is certainly possible), or it has to come from the server. We've seen before that it is easy to pull data into a PDF form with XFDF. In this case, we have a script on the server that is dynamically generating our XFDF. We can set things up so that the script generates the array of movies (and export values) as part of the XFDF.

    Unfortunately, XFDF won't populate a list box directly. But what we can do is suck movie data into an invisible text field, then have our onload script slurp the data from the text field and programmatically populate the list box at onload time. The onload script looks like this:

    // if hiddenText data is in JSON format . . .
    movies = eval( this.getField( "hiddenText" ).value );

    // populate the list of movies
    var list = this.getField( "list" );
    list.setItems( movies.sort( ) );

    The Acrobat JavaScript API doesn't directly support a notion of onload events per se, but in practice all we have to do to get a script to execute at load time is attach it to the form as a document-level script. In Acrobat 9 Pro, you can create such a script using the Document JavaScripts command under Advanced > Document Processing.

    In addition to the onload script, our PDF form needs one additional script. Underneath the list box, we have a button that says "Click here to view details for the selected movie." When that button is clicked, we want a script to fire. The script will request the XFDF containing film data for the selected film (causing the form to repopulate). This script looks like:

    var f = this.getField("list");
    var a = f.currentValueIndices;
    selection= f.getItemAt(a, true);
    getURL( "/content/films/" + selection + ".xfdf" );

    The first line simply gets a reference to the list box. The second and third lines fetch the user's selection (whatever is highlighted in the list box). The fourth line sends a properly formed GET request to the server.

    Recall from last time that in our repository, we have content nodes under /content/films that look like:

    In my earlier post, I presented a script (xfdf.esp) that produces XFDF-formatted movie data. The script looked like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
    <f href="/apps/films/FilmSummary.pdf"/>
    <% response.setContentType("application/vnd.adobe.xfdf" ); %>
    <fields>
       
        <field name="Director">
            <value><%= currentNode.Director %></value>
        </field>
        <field name="Subject">
            <value><%= currentNode.Subject %></value>
        </field>
        <field name="Year">
            <value><%= currentNode.Year %></value>  
        </field>
        <field name="Title">
            <value><%= currentNode.Title %></value>
        </field>
        <field name="Length">
            <value><%= currentNode.Length %></value>
        </field>
        <field name="Actor">
            <value><%= currentNode.Actor %></value>
        </field>
        <field name="Actress">
            <value><%= currentNode.Actress %></value>
        </field>
        <field name="Popularity">
            <value><%= currentNode.Popularity %></value>
        </field>

    </fields>

    </xfdf>
     

    We can shorten this script quite a bit by creating the XML programmatically using E4X:

    fields = <fields/>;
    names = ["Title","Director","Subject",
        "Actor","Actress","Length",
        "Popularity","Year"];

    for (var i = 0; i < names.length;i++) {
       field = <field><value>{currentNode[names[i]]}</value></field>;
       field.@name = names[i];
       fields.* += field;
    }

    The second change to the script that we need to make is to add a section that generates the movie list that will go in the form's list box. The data needs to be an array of arrays, in JSON format, of the form [ [Title1, exportValue 1], [Title2, exportValue2] ... ]. Thus, we need to add the following block of code:

    parent = currentNode.getParent();
    allFilms = parent.getNodes();
    json = "[ ";
    for ( i in allFilms ) {
       nodeName = i;
       filmTitle = allFilms[i].Title;
       json += "[ \"" + filmTitle + "\",\"" +
        nodeName + "\"],"
    }
    json += "[] ]";
    field = <field><value>{ json }</value></field>;
    field.@name = "hiddenText";
    fields.* += field;


    The complete server-side script, xfdf.esp, now looks like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
    <f href="http://localhost:7402/apps/films/FilmSummary2.pdf"/>

    <%
    fields = <fields/>;
    names = ["Title","Director","Subject",
        "Actor","Actress","Length",
        "Popularity","Year"];

    for (var i = 0; i < names.length;i++) {
       field = <field><value>{currentNode[names[i]]}</value></field>;
       field.@name = names[i];
       fields.* += field;
    }

    parent = currentNode.getParent();
    allFilms = parent.getNodes();
    json = "[ ";
    for ( i in allFilms ) {
       nodeName = i;
       filmTitle = allFilms[i].Title;
       json += "[ \"" + filmTitle + "\",\"" +
       nodeName + "\"],";
    }
    json += "[] ]";
    field = <field><value>{ json }</value></field>;
    field.@name = "hiddenText";
    fields.* += field;

    %>

     <%=  fields.toXMLString() %>

    </xfdf>

     

    And that's all there is to it: two dozen lines of code on the server, and seven lines of code in the PDF form (a 3-line onload script plus a 4-line button click handler). That's all that's required to have an interactive PDF form-driven application on CRX.

    Of course, there's still room for improvement. The way things are now, this form reloads and repaints the listbox data as part of every roundtrip to the server. That's not necessarily the worst thing in the world to do (it causes about two seconds of extra latency), but it would be nice if we didn't have to reload all the movie titles with each request. It would also be nice if the user could look up movies based not just on the title, but on the director, year, starring actor, or other criteria. In other words, it would be nice to have a search-driven app. We'll tackle that in a future blog.