Latest Posts

Archives [+]

Categories [+]

Authors [+]

Entries filed under 'jcr'

    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 Michael Marth JUL 26, 2010

    Posted in jcr and link of the day Comments 2

    It's always good to get a glimpse into the approaches taken by non-OSS JCR implementations: In a recent technical article on the developerworks website Malarvizhi Kandasamy describes how IBM goes about JCR fulltext search. The actual engine is

    Juru, which is a Java library developed by the IBM Haifa research lab

    According to the article Juru is capable of some natural language processing like stemming or finding similar spellings.

    IBM uses a JCR compliant repository in a number of their products, e.g. Lotus Web Content Management or WebSphere Portal.

    Posted by Kas Thomas JUL 06, 2010

    Posted in crx, development, dynamic languages, java content repository, javascript and jcr Comment 1

    Not long ago, I blogged about how, with a little bit of client-side Javascript, it's pretty easy to save browser selections (in Chrome) to Day CRX. It turns out the same sort of thing is not all that hard to implement in OpenOffice.

    The scenario: You're reading a long document in OpenOffice (say, the JCR spec) and you come across a particular page or paragraph (or code snippet, etc.) that you'd really like to save for later, because you know you'll want to come back to it. If you want, you can Cut and Paste such snippets into new documents and save those docs on your local drive. But that's a bit awkward and leaves you with a file-management mess. (How easy or hard will it be to search for a given text string in order to find it again?) As an alternative approach, I offer the following macro, which lets you save any selected (highlighted) span of text from an OpenOffice doc straight to the CRX repository, under whatever pathname you choose.

    The following Javascript code will run as a macro in any version of OpenOffice from 2.0 on (it was tested in 3.2). If you've never created a Javascript macro in OpenOffice before, it's pretty easy: Under the Tools menu, go to Macros > Organize Macros > JavaScript. In the dialog that appears, click into the folder tree on the left and select the folder in which you want to create a macro. Doing so will enable a Create button on the right. Click it. Then select the newly created macro file (in the navtree), which will, in turn, enable an Edit button. Click Edit and you'll be brought to a JavaScript editor. Cut and paste the following code into the editor and Save it.

    // OOo Javascript macro (OOo 2.0 or higher required)
    // POST selected text to CRX repository

    importClass(Packages.com.sun.star.uno.UnoRuntime);
    importClass(Packages.com.sun.star.text.XTextDocument);
    importClass(Packages.com.sun.star.text.XText);
    importClass(Packages.com.sun.star.text.XTextRange );
    importClass(Packages.com.sun.star.text.XTextViewCursorSupplier);

    // utility method: get selected text
    function getSelectedText(controller) {
            var xViewCursorSupplier =
            UnoRuntime.queryInterface(XTextViewCursorSupplier, controller);
            xViewCursor = xViewCursorSupplier.getViewCursor();
            var selectedText = xViewCursor.getString();
            return selectedText;
    }

    // Do a POST
    function doJavaPOST( url, content ) {
            var reply = "";
            try {
                    var URL = new java.net.URL( url );
                    var urlConn = URL.openConnection( );
                    urlConn.setDoInput ( true );
                    urlConn.setDoOutput ( true );
                    urlConn.setUseCaches( false );
                    urlConn.setRequestProperty ("Content-Type",
                      "application/x-www-form-urlencoded" );
                    var printout =
                      new java.io.DataOutputStream ( urlConn.getOutputStream ( ) );
                    printout.writeBytes ( content );
                    printout.flush ( );
                    printout.close ( );
                    var input =
                      new java.io.DataInputStream ( urlConn.getInputStream ( ) );
                    var str = "";
                    while( null != ( str=input.readLine( ) ) ) { reply += str; }
            }
            catch(exception) {
                    java.lang.System.out.println( exception.toString() );
            }
            finally {
                    if ( input != null ) input.close();
            }

            return reply;
    }

    // munge together the form data
    function createRequest( object ){

            var data = [];
            for ( var i in object )
            data.push( i + "=" + object[ i ].toString( ) );

            return data.join( "&" );
    }

    // Modal dialog with OK/cancel and a text field
    function prompt( msg ) {
            var swing = Packages.javax.swing;
            var text = swing.JOptionPane.showInputDialog(
            new java.awt.Frame(), msg );
            return ( null == text ) ? "" : text; // always return a string
    }

    ( function main ( ) {

            var CRX_BASE_URL = "http://127.0.0.1:7402";
            var CONTENT_URL = CRX_BASE_URL + "/content/";

            var oDoc = XSCRIPTCONTEXT.getDocument();
            var oController = oDoc.getCurrentController();

            if ( !getSelectedText( oController) ) return;

            var reply =
              prompt(  "Where do you want to store this item? (e.g. a/b/itemName)" );
            if ( reply != "") {

                    var parts = reply.split("/");
                    var name = parts.pop();
                    var url = CONTENT_URL + reply.substring(0,reply.lastIndexOf("/")+1);

                    var request = {};
                    request[":nameHint"] = name;
                    request.content = getSelectedText(oController);
                    request.source = "OpenOffice.org";
                    request.timestamp = new Date();
                    request.docURL = oDoc.getURL();
                    var postData = createRequest( request );

                    var str =  doJavaPOST( url, postData );
            }

    } ) ( );  // end main( )

     

    We're able to do what amounts to AJAX programming from OpenOffice by dint of the fact that OpenOffice's JavaScript engine (nee Rhino) is implemented in Java and allows "reachthrough" calls  from JavaScript to Java. Basically, any JRE method available to a Java program is available to your script. Thus it is easy to call openConnection on a java.net.URL, set request headers, and do a POST programmatically, with java.io.* methods, all from JavaScript.
     
    The 90-some lines of code are fairly self-explanatory, save (perhaps) for the half dozen or so arcane OpenOffice-API calls. When the user fires the macro, a Swing dialog appears, asking for a (repository) pathname under which to store the snippet. The path you give doesn't have to already exist: CRX (or Sling, under the covers) will automatically create a node at the path you specify if one does not exist there already.

    As with my Chrome script of a few days ago, we set the :nameHint parameter in our data stream so as to tell CRX to canonicalize the node name (replace non-alphanumeric characters with underscores and do other fixups), guaranteeing a Sling-legal name for the new node, but without that name simply ending up being some randomly generated number.

    The code expects your repository to be available at http://127.0.0.1:7402, and the macro is hard-coded to use a root of /content/. The relevant lines of code are easy to change if you need to use a different repository location.

    After running the macro with text selected in OpenOffice, check your repository with CRX's Content Manager and you will see a new node (of type nt:unstructured) at the path you specified. If you entered "a/b/c," the new content will be a 'c' node under /content/a/b.

    And there you have it: OpenOffice-CRX integration in less than 100 lines of code!

    Posted by Michael Marth JUL 02, 2010

    Posted in jcr, jsr-170 and jsr-283 Comment 1

    One particular strength of Java Content Repositories is that they provide so much infrastructure for developing content centric applications. Today, I discovered another hidden gem in JCR2 (JSR-283) that can come in very handy for app development:

    In JCR1 the class ObservationManager used to manage EventListeners that get triggered immediately when an event like a property change occurs. Starting from JCR2 the ObservationManager also provides an EventJournal for each node that can be retrieved without having to register a listener first. The EventJournal is a list of events, e.g. addition, moves or removal(!) of child nodes, complete with user id and timestamp.

    Attached to this post is a little CRX package with a servlet that renders the events for a given node. The relevant lines are:

    ObservationManager om = session.getWorkspace().getObservationManager();
    EventJournal eventJournal = om.getEventJournal(
        Event.NODE_ADDED | Event.NODE_MOVED | Event.NODE_REMOVED  | Event.PROPERTY_CHANGED
        | Event.PROPERTY_REMOVED | Event.PROPERTY_ADDED, path, false, null, null);

    Install the package and point your browser to http://localhost:7402/apps/eventy.html?path=/content (the path parameter specifies the node you are interested in). You should see a list of entries like:

    Event: Path: /content/n3, NodeAdded: , UserId: admin, Timestamp: 1272975189833, UserData: null, Info: {}
    Event: Path: /content/n3, NodeRemoved: , UserId: admin, Timestamp: 1272975212725, UserData: null, Info: {}
    ...

    And from the Javadoc:

    Events returned in the EventJournal instance will be filtered according to [...] the current session's access restrictions

    I am delighted.

    * eventjournal-1.0.zip
    CRX package: sample for EventJournal

    Posted by Kas Thomas JUN 30, 2010

    Posted in crx, crx gems, development, java content repository, jcr, jsr-283, package and sling Comments 3

    Bookmarks have their place, but sometimes what you'd really like to be able to do is save actual snippets of content from a given web page rather than just keep a reference to the page's URL in your Bookmarks menu. Saving the whole page is easy, but usually impractical, because usually you're not interested in saving all the text (and photos and advertisements, etc.) on a page. Usually you're just interested in a particular span of text (a code snippet, or a particular paragraph, or a list of links) and nothing more. There ought to be an easy way to cache just the portion of the page that interests you. And there is -- if you have Day CRX and Google's Chrome browser.

    It turns out that with only around 200 lines of JavaScript, you can modify Chrome's behavior so that when you make a text selection (or in fact any selection) on a web page, a button appears on the page, and when you click the button, your selection is saved to your Sling repository. (In this case, we're using CRX instead of Sling per se, but the same code should work with either one.)

    The following code (stored in a file called clip2crx.user.js) shows how it's done. Note that the code is formatted as a Greasemonkey script (but has no Greasemonkey dependencies). It turns out that Chrome version 4-and-up supports Greasemonkey scripts: All you have to do to load such a script (assuming its name ends in ".user.js") is Open the script in Chrome and say yes to the "Are you sure you want to install this script?" warning that appears. No need to restart Chrome. Once the script is loaded, simply go to any web page and make a selection on the page (by click-dragging the mouse). When you let your finger off the mouse, a "Save Selection" button will appear in the lower right corner of the window. Click that button. A small dialog will pop up, asking you to give the selection a name. After supplying a name, click OK and the selection is saved to CRX.

    // ==UserScript==
    // @name           clip2crx
    // @namespace      clip2crx
    // @description    Save user-selected items in web page to a CRX node
    // @include        *
    // ==/UserScript==

    // Kas Thomas <kas.thomas@day.com>
    // 28 June 2010
    // Tested in Google Chrome 5.0.375.70 beta

    // *************************** Button ******************************
    // This is the "Save Selection" button (sans event handlers).

        Button = new function( ) {    // Button is a singleton object

            var _ID = "SAVESELECTION";
            var _domNode =
                createFixedPositionNode( "div", _ID, 50, 10,
                '<input type="button" value="Save Selection..." />' );
       
            this.getDomNode = function ( ) { return _domNode; }
       
            function createFixedPositionNode( nodeName, id, x, y, innerhtml ) {
           
                var node  =  document.createElement( nodeName );
                var style = 'position: fixed; ';
                style +=    'bottom: ' + y + 'px; ';
                style +=    'right: ' + x + 'px; ';
                style +=    'z-index:100; ';
                style +=    'font-size: small;';
                node.setAttribute( "style", style );
                node.setAttribute( "id", id );
                node.innerHTML = innerhtml;
           
                return node;
            }

                // This will add the Button to the page
            this.attach = function ( doc ) {
       
                if ( !nodeExists( doc, _ID ) )
                    doc.body.insertBefore( _domNode , doc.body.firstChild );
            }

            // This removes the Button from the page
            this.detach = function ( doc ) {
       
                if ( nodeExists( doc, _ID ) )
                    _domNode = doc.body.removeChild( _domNode );
            }

            function nodeExists( doc, id ) {
       
                return doc.getElementById( id ) != null;
            }

            };  // Button


        // called by buttonHandler( ) further below...
        function saveToRepository() {

                // Repository storage URL
                var base_url  = "http://localhost:7402/content/myapp/";
               
            // Obtain the content we want to post
                var params = "content=" + getContent( );

                // prompt the user to give the new node a name
                var name = prompt( "Name for this entry:" );
                if ( !name || name.length == 0 )
                    throw "No name provided.";

            params += "&:nameHint=" + name;

                // Prepare for AJAX POST
                http = new XMLHttpRequest( );
                http.open( "POST", base_url, true );

                // Send the proper header information along with the request
                http.setRequestHeader( "Content-type", "application/x-www-form-urlencoded" );
                http.setRequestHeader( "Content-length", params.length );
                http.setRequestHeader( "Connection", "close" );

                // Show whether we succeeded...
                http.onreadystatechange = function( ) {
                        if ( http.readyState == 4 )
                     ;// alert( "http.status = " + http.status ); DEBUG ONLY
                }

                // do the AJAX POST
                http.send( params );
            }


        function getContent() {

            var selection = window.getSelection( );
            var markup = serializeSelection( selection );  
            var finalMarkup = formatPage( markup );
            return finalMarkup;
        }


        function formatPage( pageContent ) {

            return "<br>" + fixRelativeURLs( pageContent );
            }

        // Try to expand relative URLs to absolute paths
        // so they'll continue to work when viewed from CRX
            function fixRelativeURLs( text ) {
         
                var url = window.parent.location.href;
                var lastSlash = url.lastIndexOf("/");
                var basisURL = url.substring( 0, lastSlash );

                function convertURL( hit, offset, text ) {

                    var output = hit;

                    if ( text[ offset + hit.length ] == "/"  )
                        output += window.parent.location.protocol +
                            "//" + window.parent.location.host;
     
                    else if ( text[ offset + hit.length ].match( /\w|#|\./) != null )
                            output += basisURL + "/";

                    return output; // all other cases, nop
                }

            var regex1 = /src="(?!http)/gi;
                var regex2 = /href="(?!http)/gi;
                var newText =
                    text.replace( regex1, convertURL ).replace( regex2, convertURL );

                return newText;
            }


        function serializeSelection( selection ) {

                var xmlFragment = "";
                try {
                  var n = 0, ranges = selection.rangeCount;
                  while ( n != ranges ) {
                        var range = selection.getRangeAt( n++ );
                        var content = range.cloneContents( );
                        var serializer = new XMLSerializer( );
                        xmlFragment += serializer.serializeToString( content );
                    }
                }
                catch( msg ) { }

                return xmlFragment;
            }

    // ******************************** main( ) ********************************

    ( function main( ) {

        // Button mouseup handler
        function buttonHandler( e ) {

            Button.detach( document );      
          saveToRepository();      
        }
       
        // document.body mouseUpHandler
        function mouseUpHandler( e ) {

            var selection = window.getSelection( );

            if ( selection.toString( ).length > 0 ) { // a selection exists        
                Button.attach( document );          
            }
            else  
                Button.detach( document );
             
        }

        function clickHandler( e ) {  // need to handle page clicks too, because
                                      // if user clicks inside a selection,
                                      // browser eats the mouseup event...    
            mouseUpHandler( e );
        }

        // now add event handlers to the page...
        addEventListener( "mouseup", mouseUpHandler, false );
        addEventListener( "click", clickHandler, false );

        // add event handler to the Button...
        Button.getDomNode( ).addEventListener( "mouseup", buttonHandler, false );
       
    }) ( );   // end main( )
     
     

    There are several things to note.

    By default, selections are stored in CRX at a URL of:

       http://localhost:7402/content/myapp/[name]

    where name is the item name you provided in the popup dialog. The name is provided to CRX as a :nameHint parameter value in the POST data. This ensures that the item gets stored with the name you want (rather than an autogenerated number created by Sling) while also ensuring that the name is normalized to a legal form (with non-alphanumeric characters converted to underscores). The fact that it is stored under /content/ means that Sling will look for application scripts to run against your content, under http://localhost:7402/apps. In this example we're not using any such scripts, but it would be easy to add some.

    Content gets stored in CRX with all markup intact, so that when you decide to view the snippet later, it will have more or less than same appearance that it had on the original web page from which it was taken. Also note that, thanks to a function called fixRelativeURLs(), any relative URLs in the saved selection are expanded to full-path form before content is moved into the repository. That way, embedded links should still work properly when you view the stored snippet later even though its root URL has changed.

    The rest of the code is fairly self-explanatory.

    To obtain your own ready-to-run copy of clip2crx.user.js, check out the package stored here.