Sunday, August 26, 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.


Friday, August 3, 2007

Groovy + Jemmy GUI automation

This is something that I came up at work, I thought other people outside of work could make use of it as well. If you're wondering about the obscure references to BizApp and such, this is because I removed the name of the actual app from the text.

OK, so we established that Groovy rocks. Now, I've worked at the company long enough to know that there exists a GUI application that kinda does what I need. I know it works, since I can build the GUI app, run it, push the buttons on it, and it creates all the outputs that I need. Now, the only problem is that I'd like to generate a decent amount of these outputs so that I can have a realistic testing scenario.

Now, I'm certain that there is a "cleaner" and "more proper" way of creating these test outputs. However, my colleague who wrote the BizApp app already figured out how to accomplish what the application needs to do (including all the nitty gritty technical details). Would it be nice if I had access to a simple action that does that ? Certainly. Do I have it now ? Nope. When do I need the outputs to test my client app ? YESTERDAY !! So, what should I do ???

Enter the combination of Groovy and Jemmy. We know what Groovy is, now what is Jemmy??? From their site, it's a library that allows GUI driven testing. In effect, it allows me to programmatically specify the actions that I need on the GUI, and execute them. Great !! Step 1 compete !

Now, step 2. Jemmy is a Java library, I don't quite know exactly how to use the API, and I don't quite know the exact sequence of actions that I'll need to perform through Jemmy. So, if I had to write a Java app to exercise BizApp through Jemmy, there'll be a lot of trial and error hindered by compilation. So, what should I do ?? Groovy is an excellent way to explore a particular API, but how can I plug in Groovy and Jemmy together in order to get this job done ?

Alright, alright, enough of the round-about way of explaining things, here is what to do:

* Setup a NetBeans Groovy scripting project. For that, you need a recent NetBeans setup, with the Coyote plugin installed. The same can certainly be achieved with an Groovy Eclipse plugin as well, but since I don't dig Eclipse, I'll show it in NetBeans. After Coyote is installed, you just set up a new project


* Add the BizApp dependencies in the project. In my case, they're in d:JDevelopBuildsBizApp Add all jars from that directory into the project build. Notice the Jemmy libary at the bottom of the screenshot : it can be added either using the NetBeans update center, or by just downloading the Jemmy library and adding the jar as a dependency in the project.



* Add the Groovy/Jemmy bootstrap script and add the content below, inserting the correct main class for the application. This script will launch BizApp, and will then fire up a blank Groovy console where we can execute our little script in. Right click on the Coyote project, and select "Run Project".


import org.netbeans.jemmy.*;
import org.netbeans.jemmy.explorer.*;
import org.netbeans.jemmy.operators.*;

// Fire up the BizApp Application
new ClassReference("com.mycompany.MyAppMainClass").startApplication();

// Get a reference to the BizApp JFrame and put it into the console's script binding.
mainFrame = new JFrameOperator("BizApp");
def bind = new Binding()
bind.setProperty("testScript",this)
bind.setProperty("mainFrame",mainFrame)


// Fire up the Groovy console.
def console = new groovy.ui.Console(this.class.classLoader,bind)
console.run()
Thread.sleep(600000)


* Now, we have both BizApp and the Groovy Console running side by side. Inside of the Groovy console, we can use Jemmy to programmatically manipulate the GUI (e.g. press buttons, select from checkboxes, etc). The Jemmy statements would look something like the script below. You can sometimes get away without firing a new thread, but if you don't the Groovy console script might block the UI and prevent Jemmy from executing it's input on BizApp


t = new Thread() {
// do whatever you need to do with Jemmy, push buttons, select combos, type into text fields, etc.
}
t.start()




* Finally, figure out what you need to do with the GUI app you're working with and script it. Pseudocode looks like this:


select from merchant dropdown
select from vendor dropdown
hit the "Process" button
hit the "Back" button twice (to get to the main screen)


* The Groovy setup script looks something like the one below. I usually execute this first, so that my Groovy console has references to the combos and buttons so that I don't have to import the Jemmy classes every time (e.g. the Groovy console doesn't remember the imports that you might have done in previous executions, but it will remember the variables that you might have declared


// Import Jemmy specific classes
import org.netbeans.jemmy.*;
import org.netbeans.jemmy.explorer.*;
import org.netbeans.jemmy.operators.*;

// Get references to all UI widgets so that we don't have to re-import
// the Jemmy classes every time. The names of these variables will be
// stored in the "binding" and can be accessed in subsequent script/snippet
// executions
merchantOp = new JComboBoxOperator(mainFrame,0)
vendorOp = new JComboBoxOperator(mainFrame,1)
processButtonOp = new JButtonOperator(mainFrame,"Process")
backButtonOp = new JButtonOperator(mainFrame,"Back")


* Write put the script that will actually do the work. The script is typically derived by executing the statements one by one and seeing that they do the right things. The script below processes an order for the first merchant and vendor and then comes back. See the Jemmy API for details on available widgets operators, etc.


t = new Thread() {
merchantOp.selectItem(0)
vendorOp.selectItem(0)
processOpButton.push()
backButtonOp.push()
backButtonOp.push()
}
t.start()


* Finally, the last step is to typically put something together that will do what you need repetitively. My script is below, I have it saved in the Coyote project directory, and I load it up from the Groovy console when it fires up. Note that I typically just hightlight and run the first part of the code first (to get references to all the buttons, etc), and then only run the second part when I need to run BizApp for a long time.


// Import Jemmy specific classes
import org.netbeans.jemmy.*;
import org.netbeans.jemmy.explorer.*;
import org.netbeans.jemmy.operators.*;

// Get references to all UI widgets so that we don't have to re-import
// the Jemmy classes every time. The names of these variables will be
// stored in the "binding" and can be accessed in subsequent script/snippet
// executions
merchantOp = new JComboBoxOperator(mainFrame,0)
vendorOp = new JComboBoxOperator(mainFrame,1)
processButtonOp = new JButtonOperator(mainFrame,"Process")
backButtonOp = new JButtonOperator(mainFrame,"Back")




// Creating orders for a merchant involves creating orders for each vendor for the merchant.
def processMerchantVendor(count,merchants) {
t = new Thread() {
count.times {
try {
merchants.each { merchantIndex ->
merchantOp.selectItem(merchantIndex)
if (merchantOp.selectedItem == "some_merchant") return
(0..vendorOp.itemCount).each { vendorIndex ->
if (merchantOp.itemCount > merchantIndex && vendorOp.itemCount > vendorIndex) {
Thread.sleep(50)
vendorOp.selectItem(vendorIndex)
Thread.sleep(50)
processButtonOp.push()
Thread.sleep(50)
backButtonOp.push()
Thread.sleep(50)
backButtonOp.push()
}
}
}
} catch (Exception e) { }
}
}
t.start()
}

// This creates test orders for all merchants, 10 times in a row.
processMerchantVendor(10,(1..merchantOp.itemCount))

Popular Posts

Related Posts with Thumbnails