Latest Posts

Archives [+]

Categories [+]

Authors [+]

Entries filed under 'sling'

    Posted by Kas Thomas AUG 25, 2010

    Posted in ajax, crx, crx gems, development, javascript, rest and sling Comments 5

    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.

    /content/ddc/blog/2010/08/crx_gems_interactiv0/jcr:content/par/image_0/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

    Posted in sling and tutorial Comments 3

    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

    Posted in content management, crx, http, jcr, request and sling Comments 6

    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 Kas Thomas JUL 22, 2010

    Posted in crx, crx gems, development, dynamic languages, javascript, rest and sling Add comment

    A few days ago, I talked about how to "shred and store" a spreadsheet -- i.e., how to push rows of a spreadsheet into individual nodes in CRX (one node per row, with column data stored as properties). I also gave JavaScript code for doing this in an OpenOffice macro. For testing purposes, I used the CSV file a1-film.csv, representing 1741 movies catalogued by Georgia Tech's College of Computing.

    After running my OpenOffice macro on the Georgia Tech CSV file, my CRX repository now contains movie data (Title, Director, Year, etc.) for 1741 films, each film with its own nt:unstructured node under the path /content/films/. In the CRX Content Explorer, a given node (in this case, the node at http://localhost:7402/content/films/terminator_2) looks something like this:

    /content/ddc/blog/2010/07/crx_gems_rendering/jcr:content/par/image_0/file

    Notice that the spreadsheet's column data now show up as properties (Actor, Actress, Director, etc.) with values like "Schwarzenegger, A.," "Hamilton, Linda," and so forth.

    Notice also that I've included a property of sling:resourceType, with a value of "films," for every movie node. This is important, because it tells Sling to look under /apps/films/ for any runtime scripts that may need to be applied in order to render a particular node (such as http://localhost:7402/content/films/terminator_2).

    Let's see how this works in practice. Suppose I want to render a movie node as HTML. I could put a file called html.esp under /apps/films/, containing the following markup:

    <html>
    <head>
    <link rel="stylesheet" type="text/css" href="/apps/films/films.css" />
    </head>
    <body>

    <img src="/apps/films/Film.png" width="95" height="92" />
    <br/>

    <span class="head"> <%= currentNode.Title %> </span><br/>

    <span class="normal">Director:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Director %></span><br/>

    <span class="normal">Year:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Year %></span><br/>

    <span class="normal">Genre:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Subject %></span><br/>

    <span class="normal">Actor:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Actor %></span><br/>

    <span class="normal">Actress:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Actress %></span><br/>

    <span class="normal">Runtime:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Length %> minutes</span><br/>

    <span class="normal">Popularity:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Popularity %></span><br/>

    </body>
    </html>

    As it turns out, I've also got a small PNG graphic, Film.png, located under /apps/films/, as well as a CSS file called (what else?) films.css, which looks like:

    .head {
    font-family:Tahoma;
    font-size:32pt;
    fill:#990000;
    color:#990000;
    }

    .normal {
    font-family:Tahoma;
    font-size:15pt;
    fill:#444444;
    }

    .tdata {
    font-family:Verdana;
    font-size:15pt;
    font-weight: bold;
    fill:#992222;
    color:#992222;
    }

    With these files in place, I can now direct my browser to go to http://localhost:7402/content/films/terminator_2.html, and Sling will automatically detect the need to use the html.esp script to render the node as HTML. The resulting rendition looks something like this:

    /content/ddc/blog/2010/07/crx_gems_rendering/jcr:content/par/image_1/file























    But suppose I want to be able to provide a Scalable Vector Graphics rendition, for browsers (like Firefox) that can render SVG. Not a problem: All I need to do is create a script called svg.esp and place it under /apps/films/. The svg.esp script might look something like this:

    <?xml version="1.0" standalone="no"?>
    <?xml-stylesheet type="text/css" href="/apps/films/films.css" ?>
    <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
    "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

    <svg width="100%" height="100%" version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink">

    <!--  Add a custom filter effect  -->
     <defs>
        <filter id="MyFilter" filterUnits="userSpaceOnUse" x="0" y="0" width="600" height="400">
          <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/>
          <feOffset in="blur" dx="4" dy="4" result="offsetBlur"/>

          <feSpecularLighting in="blur" surfaceScale="5" specularConstant=".75"
       specularExponent="20" lighting-color="#992222"  
       result="specOut">
            <fePointLight x="-5000" y="-10000" z="20000"/>
          </feSpecularLighting>
          <feComposite in="specOut" in2="SourceAlpha" operator="in" result="specOut"/>
          <feComposite in="SourceGraphic" in2="specOut" operator="arithmetic"
       k1="0" k2="1" k3="1" k4="0" result="litPaint"/>
          <feMerge>
            <feMergeNode in="offsetBlur"/>
            <feMergeNode in="litPaint"/>
          </feMerge>

        </filter>
      </defs>

    <image x="20" y="20" width="95" height="92" xlink:href="/apps/films/Film.png"/>

    <!-- Apply the filter to Title -->
     <g filter="url(#MyFilter)" >
        <g  transform="matrix(1.15 0 0 1 0 0)"  >
          <text class="head" x="18" y="160" >
          <%= currentNode.Title %></text>      
        </g>
      </g>

    <text class="normal" x="20" y="200" >Director</text>
    <text class="tdata" x="200" y="200"><%= currentNode.Director %></text>

    <text class="normal" x="20" y="230" >Genre</text>
    <text x="200" y="230" class="tdata"><%= currentNode.Subject %></text>

    <text class="normal" x="20" y="260" >Year</text>
    <text x="200" y="260"  class="tdata"><%= currentNode.Year %></text>

    <text class="normal" x="20" y="290" >Actor</text>
    <text x="200" y="290" class="tdata"><%= currentNode.Actor %></text>

    <text class="normal" x="20" y="320" >Actress</text>
    <text x="200" y="320" class="tdata"><%= currentNode.Actress %></text>

    <text class="normal" x="20" y="350" >Runtime</text>
    <text x="200" y="350" class="tdata"><%= currentNode.Length %></text>

    <text class="normal" x="20" y="380" >Popularity</text>
    <text x="200" y="380" class="tdata"><%= currentNode.Popularity %></text>

    </svg>

    The big <defs> section near the beginning is an (optional) SVG filter effect, designed to provide a little extra visual appeal to the movie title by giving it a drop-shadow. The result (as rendered by Firefox) looks like this:

    /content/ddc/blog/2010/07/crx_gems_rendering/jcr:content/par/image_2/file

























    Of course, in a real-world component or application, you would have logic somewhere (whether on the server side or in client-side code) that detects the type of browser the user has and fetches the SVG rendition only if the user's browser is SVG-capable. 


    Posted by Kas Thomas JUL 19, 2010

    Posted in crx, development, dynamic languages, javascript, request and sling Comments 2

    The first version of this post originally was published here.

    Lately I've been doing a fair amount of server-side scripting using ESP (ECMAScript Pages) in Sling. At first blush, such pages tend to look a lot like Java Server Pages, since they usually contain a lot of scriptlet markup, like:

    <%  // script code here  %>

    and

    <%=  // stuff to be evaluated here  %>

    So it's tempting to think ESP pages are simply some different flavor of JSP. But they're not. From what I can tell, ESP pages are just server pages that get handed to an EspReader before being served out. The EspReader, in turn, handles the interpretation of scriptlet tags and expression tags (but doesn't compile anything into a servlet). Bottom line, ESP is not JSP, and despite the availability of scriptlets tags, things work quite a bit differently in each case.

    Suppose you want to detect, from an ESP page or a JSP page, what kind of browser a given page request came from. In a Sling JSP page you could do:

    <%@taglib prefix="sling" uri="http://sling.apache.org/taglibs/sling/1.0" %>

    <sling:defineObjects/>
    <html><body>

    <%
    java.util.Enumeration c = request.getHeaders("User-Agent");

    String s = "";

    while ( c.hasMoreElements() )
        s += c.nextElement();
    %>

    <%= s %>
    </body></html>

    But what do you do in ESP? Remember, <sling:defineObjects/> is not available in ESP.

    It turns out that Sling automatically (without the need for any directives) exposes certain globals to the JavaScript Context at runtime, and one of them is a request object. Thus, in ESP you'd simply do:

    <%

    c = request.getHeaders("User-Agent");

    s = "";

    while ( c.hasMoreElements() )
        s += c.nextElement();

    %>

    <%= s %>

    Very similar to the JSP version.

    So the next question I had was, what are the other globals that are exported into the JavaScript runtime scope by Sling? From what I can determine, the Sling globals available in ESP are:

    currentNode
    currentSession
    log
    out
    reader
    request
    resource
    response
    sling

    currentNode is the JCR node underlying the current resource; currentSession is what it sounds like, a reference to the current Session object; log refers to the org.slf4j.Logger; reader returns request.getReader(), which allows for reading the request body; request is a reference to the SlingHttpServletRequest; resource is the current Resource; response is, of course, a reference to the SlingHttpServletResponse; and sling is a SlingScriptHelper. All of these are available all the time, throughout the life of any ESP script in Sling.

    The nice part about server-side scripting in Sling (one of many nice parts), incidentally, is that you don't have to choose to do just ESP pages or just JSP; you can write an ESP handler for one situation and a JSP for another, and use ESP/JSP in any combination. You're not locked into one technology or the other.

    For more information, try the Sling Javadocs here or Day's page of resources here (note, in particular, the list of References on the right).