Monday, August 27, 2007

Grails + Tapestry = Grapestry ? Part 1 (of n)

I've been quite intrigued by the approach Grails takes to developing web apps. It really is very nice that Grails offers and end-to-end solution that provides the framework for the front end, services, and back end.

At the same time, I've been a big Tapestry fan, as it seems that it is the best web framework that I know about. I did read up about how Grails handles the front end, and although it provides decent support for developing the front end (with some cool integration into the whole Grails framework), but still not as nice as what Tapestry has. After all, the Grails front end is just a part of the puzzle; whereas, with Tapestry, that is it's primary goal (not to mention the whole difference between developing a "page-oriented" application with Grails compared with developing an application with a component based framework like Tapestry).

The bottom line is that Tapestry is perfect for quickly developing the front end of the app, and Grails is excellent in quickly developing everything else. The primary draw of Grails is it's use of GORM; yet, the whole integration with Spring, is also very nice. So, bottom line is, I need to have a Tapestry front end and a Grails back end.

I kinda had this idea in my head for a while, but the lucky event was that I stumbled on a blog post by Grame Rocher about integrating Wicket into Grails. It seemed straightforward enough, I asked him if he thought if Tapestry would be much different, he said "no", so, I thought, "Great, I'm going to rock on and build a cool Tapestry plugin for Grails".

As usual, it's easier said than done. It's probably been a couple of weeks since I've been able to get even close to having Grails and Tapestry work together. So, here are the steps, that I took along the way. When I come close to rounding this up, I'll probably release it somewhere (dev.java.net, sourceforge, google code, I'll have to see, I'm open to suggestions). Btw, my preliminary name for the plugin is Grapestry, it's temporary, but I have this idea about a logo that has a big juicy grape on top of a cake or something like that (get it, "Grape Pastry"? :-) ) . Btw, just to mention that the work so far really did take about half an hour to do (just like Graeme said). The "other stuff" is what took me much longer that I thought it would: maybe another couple of hours to understand where each grails-app subdirectory ends up when the app is packaged, a couple of hours on researching existing Grails plugin and figuring out how the whole Grails magic works , and then a LARGE number of hours actually doing the integration between Tapestry and Grails (the stuff that I'm going to blog about in the next posting)...

So, first things first. I followed Graeme's instructions on how to set up a plugin and how to do the basic plugin setup.

  1. Do the grails create-plugin to set up the basic directory structure, etc.

  2. Add the jars from the tapestry distribution into the plugin lib directory. Interesting problem that I had to deal with there was that Grails (the actual distribution, inside of $GRAILS_HOME/lib) had some common jars with Tapestry. Unfortunately, Tapestry 4.1.2 required later versions of those jars, so I had to copy those particular jars from the tapestry distribution into $GRAILS_HOME/lib, and remove (or temporarily rename the jars inside of the Grails lib directory). From the feedback that I got on the Grails forum, it seems like Grails doesn't have a way to dealing with dependency conflicts between what the plugin requires and what Grails requires. I am slightly negatively surprised by this, as Grails comes with a whole bundle of dependencies (it's 20+ Megs), and the chance that Grails might conflict with another jar version seems quite high. Oh, well, moving on for now, this is just one more item on my Grapestry ToDo list

  3. I edited the canned Groovy file that configures the plugin, and gives it a chance to do it's modifications inside of web.xml, the spring config, and whatever else (there are a bunch of ToDos here as well, I'll write more about this later). A couple of things to point out in the source:

    • The ejection of the controllers plugin : I'm not sure if this is necessary, it implies that if someone is using this plugin, they are totally not interested in using the Grails standard action handling. It seems that most Grails plugins are complementary to Grails, so, is this the right way to go ? I don't know, I'm not convinced.... Also, it seems that if this is a correct assumption, the whole Grails web layer (e.g. controllers, taglibs, AJAX) can be ripped out since it will not be necessary any more, all handled by Tapestry

    • The setup inside of web.xml is pretty standard, it's just a translation of a standard Tapestry web.xml into the Groovy xml builder format

    • The other interesting method that will most likely get some action is the doWithApplicationContext and doWithDynamicMethods. I looked at the controllers plugin, and that's where a lot of the Grails magic happens (e.g. dynamic scaffolding, a lot of default methods, etc), all things that are a must for my Grapestry plugin.





    class Grapestry2GrailsPlugin {
    def version = 0.1
    def dependsOn = [:]
    // This removes the Grails standard controllers plugin, which means that standard Grails actions and such would not work anymore.
    def evicts=['controllers']

    def doWithSpring = {
    // TODO Implement runtime spring config (optional)
    }
    def doWithApplicationContext = { applicationContext ->
    // TODO Implement post initialization spring config (optional)
    }
    def doWithWebDescriptor = { xml ->
    def servlets = xml.servlet[0]

    servlets + {
    servlet {
    'servlet-name'('tapestryapplication')
    'servlet-class'('org.apache.tapestry.ApplicationServlet')

    'init-param' {
    'param-name'('org.apache.tapestry.disable-caching')
    'param-value'('true')
    }


    'init-param' {
    'param-name'('org.apache.tapestry.application-specification')
    'param-value'('tapestryapplication.application')

    }


    'load-on-startup'(1)
    }
    }


    def mappings = xml.'servlet-mapping'[0]
    mappings + {
    'servlet-mapping' {
    'servlet-name'('tapestryapplication')
    'url-pattern'('/app')
    }
    'servlet-mapping' {
    'servlet-name'('tapestryapplication')
    'url-pattern'('*.html')
    }
    'servlet-mapping' {
    'servlet-name'('tapestryapplication')
    'url-pattern'('*.direct')
    }
    'servlet-mapping' {
    'servlet-name'('tapestryapplication')
    'url-pattern'('*.sdirect')
    }
    'servlet-mapping' {
    'servlet-name'('tapestryapplication')
    'url-pattern'('*.svc')
    }
    'servlet-mapping' {
    'servlet-name'('tapestryapplication')
    'url-pattern'('/assets/*')
    }
    }

    def filter = xml.filter[0]

    filter + {
    'filter-name'('redirect')
    'filter-class'('org.apache.tapestry.RedirectFilter')
    }

    def filterMapping = xml.'filter-mapping'[0]
    filterMapping + {
    'filter-name'('redirect')
    'url-pattern'('/')
    }



    }



    def doWithDynamicMethods = { ctx ->
    // TODO Implement additions to web.xml (optional)
    }
    def onChange = { event ->
    // TODO Implement code that is executed when this class plugin class is changed
    // the event contains: event.application and event.applicationContext objects
    }
    def onApplicationChange = { event ->
    // TODO Implement code that is executed when any class in a GrailsApplication changes
    // the event contain: event.source, event.application and event.applicationContext objects
    }
    }




  4. The next step is to actually, build some Tapestry artifacts to get the puppy going: a Tapestry page in Groovy, a page specification, and an html template

    • First, the Tapestry page implementation. Not much to talk about, just one persistent property to make sure that the annotations work, one simple action that makes sure that event dispatching works OK, and that one last action to make sure that GORM style object retrieval, etc works. Here is the pudding:

      package com.troymaxventures.grapestry.pages;

      /**
      *
      * @author akochnev
      */
      import org.apache.tapestry.annotations.Persist;
      import org.apache.tapestry.html.BasePage;



      public abstract class Home extends BasePage {
      @Persist
      public abstract int getCounter();
      public abstract void setCounter(int counter);


      public void doClick(int increment) {
      int counter = getCounter();

      counter += increment;

      setCounter(counter);
      }

      public void doClear() {
      setCounter(0);
      }

      public void saveSomething() {
      /*
      def b = new Foo(name:"Foo",url:"http://foo.bar.baz")
      b.save()

      println "Saved Bookmark2: " + Foo.get(1)
      */
      println "Called saveSomething"
      }
      }




    • The Tapestry page template , just some trivial markup with something to call into Tapestry:


      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

      <html>
      <head>
      <title>My First Tapestry Page</title>
      </head>
      <body>

      <h1>My First Tapestry Page 3</h1>


      Date: <div jwcid="@Insert" value="ognl:new java.util.Date()">June 26 2005</div>
      <p>
      The current value is:
      <span style="font-size:xx-large"><span jwcid="@Insert" value="ognl:counter">37</span></span>
      </p>

      <p>
      <a href="#" jwcid="clear@DirectLink" listener="listener:doClear">clear counter</a>
      </p>

      <p>
      <a href="#" jwcid="@PageLink" page="Home">refresh</a>
      </p>

      <p>
      <a href="#" jwcid="by1@DirectLink" listener="listener:doClick" parameters="ognl:1">increment counter by 1</a>
      </p>

      <p>
      <a href="#" jwcid="by5@DirectLink" listener="listener:doClick" parameters="ognl:5">increment counter by 5</a>
      </p>

      <p>
      <a href="#" jwcid="by10@DirectLink" listener="listener:doClick" parameters="ognl:10">increment counter by 10</a>
      </p>

      <p>
      <a href="#" jwcid="saveSomething@DirectLink" listener="listener:saveSomething" >Save Something</a>
      </p>
      </body>
      </html>





    • Finally, the page spec:



      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE page-specification PUBLIC "-//Apache Software Foundation//Tapestry Specification 4.0//EN"
      "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">
      <page-specification class="com.troymaxventures.grapestry.pages.Home" >
      <!--property name="counter" persist="true" /-->
      </page-specification>








  5. Add a tapestryapplication.application application specification file to the web-app/WEB-INF folder, here's what it looks like for me:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE application PUBLIC "-//Apache Software Foundation//Tapestry Specification 4.0//EN"
    "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">
    <application name="tapestryapplication">
    <meta key="org.apache.tapestry.page-class-packages" value="com.troymaxventures.grapestry.pages"/>
    </application>





  6. OK, so far so good, this is all the right stuff we need to get it up and running. I was initially not looking forward to the magic that I'd have to do in order to get Tapestry work with the Groovy classloaders (as the Groovestry project (that might be dead) seems to do). Fortunately, Grails takes care of all that by compiling the Groovy classes into Good-Old-Java .class files, and so Tapestry doesn't have to know that the page is done in Groovy. Beautiful, isn't it ?

    I'm just going to wave my hands at this a little bit and just say that temporarily, we'll place the Home.java class in the com.troymaxventures.grapestry.pages package (and also mentioned in a tapestryapplication.application application config file). We'll also drop the Home.page specification, and the Home.html template into the $GRAPESTRY_HOME/web-app/WEB-INF directory. I know, that doesn't sound particularly fitting to the Grails philosophy of putting pages in the grails-app/views and controllers in grails-app/controllers , but there will be more on that in another blog post.



  7. Finally, do 'grails run-app' on the command line to get the app running, and go to http://localhost:8080/grapestry/app . That should pop a window that looks like this:


    Beauty divine !!! The standard Tapestry app should work, you should be able to click on some links, and see the persistent counter being updated.


3 comments:

  1. Looks like a good start. You should add it to the Grails plug-in repository:

    http://svn.grails-plugins.codehaus.org/

    Give me a shout if you want an account

    Cheers
    Graeme

    ReplyDelete
  2. Thanks Graeme, Peter Ledbrook already responded on the mailing list with detailed instructions on getting the developer account up and running. I'll ping you if there is anything else, thanks for checking in on this. I'll post further updates here, as the thing develops and comes to life.

    ReplyDelete
  3. FYI the svn repo is http://svn.codehaus.org/grails-plugins/

    ReplyDelete