Tuesday, October 16, 2007

Starting to publish thesis

After a long time for thinking and deciding what I'm doing with my thesis, I decided to go ahead and start publishing it little by little. Overall, I could post the whole thing in one swooop; however, I have to do some conversion into HTML; thus, I plan to publish it chapter by chapter.

Here (as a test) is the content of the thesis abstract:

Friday, September 14, 2007

Grapestry view and controller resolvers

This is a posting that I had started quite a while ago (right when I was starting w/ Grapestry), and I never posted it. Anyway, I thought that the content might be useful to someone trying to use hivemind with Tapestry 4. It would be very interesting when I will get a chance to try the same using the new and shiny Tapestry 5.

--------------------

The first couple of steps from the Grapestry wish list is to make the templates and pages be located in the same spot where the standard Grails gsp-s and controllers are (e.g. in grails-app/views and grails-app/controllers respectively).

First to set up the stage to what I'll talk about. For starters, I knew that HiveMind is what makes Tapestry tick. Secondly, I knew that Tapestry is very flexible and customizable, and I expected to fairly easy be able to make it look in all the right places.

So, first stop was the Tapestry User Guide configuration section. At the bottom of the page it says that there is a "configuration point" for org.apache.tapestry. specification-resolver-delegate and org.apache.tapestry. template-source-delegate - the explanations seem to point out that this is exactly what I need. So, I start thinking : how hard could it be to implement a couple of interfaces to just point to the directories I want and have it do it's magic. Well, it turns out, not as easy as it sounds.


  1. So, for the template resolver, I only need to implement this one interface, that should be easy

    public ComponentTemplate findTemplate(IRequestCycle cycle, IComponent component, Locale locale) { }






  2. Alright, I got right on it. Now, I only need to figure out how to produce a ComponentTemplate. I start digging through the Tapestry core source code, So, the constructor for ComponentTemplate looks like this:


    public ComponentTemplate(char[] templateData, TemplateToken[] tokens) {}


    OK, I can figure out how to produce a char array from a file, but these TemplateToken-s... At this point, I started realizing that this is not something that I should try to figure out from the top of my head, but rather go out and find an example that already implements the ITemplateSourceDelegate interface, surely somebody has done this before.



  3. So, I start digging around for an example. This is where it started getting scary. The first couple of links that come up talk about how nobody has really posted a good example of how this is done. Overall, the problem is that there is a boatload of "helper" objects (e.g. DefaultParserDelegate, TemplateParser, ComponentSpecificationResolverImpl, etc. the list goes for quite a bit) that you need to create before you can actually create an instance of this ComponentTemplate. On one hand, it all makes sense: Tapestry is a very modular framework, and you could potentially replace each one of it's pieces with some other component that implements the contract. On the downside, if you don't know much about the guts of Tapestry (e.g. for someone like me), digging into the guts is not that much fun.



  4. So, I say, I'll give the code that was posted on the mailing list, that should certainly work, especially since the responders say that they do work. Great, I copy-paste-compile.. and when I run my test Tapestry app (with a similar structure to a grails app - e.g. with WEB-INF/grails-app/views, etc)... a NullPointerException.. Luckily, Tapestry is open source, I dig into the source and I realize that I get the NPE when it's trying to log something.. So, do I need to inject a log into every object that I create ???



  5. This is when I realized that I just need to use the facilities that Tapestry uses to inject it's dependencies - namely, Hivemind. The thing is, I know nothing about it.. After reading up about it, it turns out that it uses these configuration points that you can use as well. So, my task moved from "copy and paste an example from the mailing list" to "figure out how this HiveMind thing works and then make it work for my ITemplateSourceDelegate implementation. I have read 2 books on Tapestry, but neither of them says anything about HiveMind (I admit, one was older, on Tapestry 3.0, the other one more focused on Tapestry itself, not its infrastructure). So, here is what I ended up using for my hivemind config file:
    <module id="id" version="0.0.1" package="package">
    <implementation service-id="tapestry.page.SpecificationResolverDelegate">
    <invoke-factory>
    <construct class="com.troymaxventures.grapestry.framework.ViewsSpecificationResolverDelegate">
    <set property="pagePath" value="grails-app/controllers"/>
    <set property="componentPath" value="grails-app/views"/>
    </construct>
    </invoke-factory>
    </implementation>

    <implementation service-id="tapestry.parse.TemplateSourceDelegate">
    <invoke-factory>
    <construct class="com.troymaxventures.grapestry.framework.ViewsTemplateSourceDelegate">
    <set property="grailsAppPath" value="WEB-INF"/>
    <set-service property="parser" service-id="tapestry.parse.TemplateParser" />
    <set-object property="contextRoot" value="infrastructure:contextRoot" />
    <set-service property="componentSpecificationResolver" service-id="tapestry.page.ComponentSpecificationResolver" />
    <set-object property="componentPropertySource" value="infrastructure:componentPropertySource" />
    <set-object property="rootOverride" value="app-property:grapestry-webapp-root-override" />
    </construct>
    </invoke-factory>
    </implementation>
    </module>


This hivemind configuration almost works for what I needed it to do in Grapestry. Unfortunately, it didn't get too far as the Grails test / development configurations work slightly differently than in production.

The general conclusion on the technology / Hivemind : it definitely seems like a cool dependency injection solution, especially in the way that it can dynamically aggregate modules and create a configuration on the fly (e.g. compared to Spring, where you have to explicitly say what goes into the config and how the different configs will interact). However, as with anything else, the high level of decomposition, where each class does only one job, and the many dependencies between the classes, make it sometimes difficult to figure out : e.g. if for a simple change like this, you have to track down 10 dependencies, that might have dependencies on their own, it sometimes makes you wish that the software you're trying to use wasn't as clean.

Saturday, September 08, 2007

Grapestry progress and wishlist

I've been making some progress in making the Grapestry Grails plugin more integrated into Grails. Overall, it hasn't been quite a walk in the park, mostly due to the way Tapestry works. Here are a couple of things that would really be nice-to-have before Grapestry can really be usable as a Grails plugin:


  1. Tapestry templates (pages and components) should show up directly under the grails-app/views directory. Overall, the idea is that that's where Grails GSPs typically show up, and it would be most natural for Grapestry to do the same

  2. Tapestry page and component classes should live directly under grails-app/controllers. Overall, in Grails, the controllers are what process the requests. Since the page and component classes have the corresponding duty in Tapestry, so it would make sense to keep them there

  3. Since Grails emphasizes convention over configuration, creating pages should default to no page specification files. Everything that a page spec does should be accomplished using annotations (totally acceptable in regular Tapestry)

  4. Add groovy scripts and templates that would set up a default page template and page classes, e.g. something like 'grails create-grapestry-page' and 'grails create-grapestry-component'

  5. Add scaffolding similar to the one that exists in Grails controllers. It seems totally possible that there could be default methods like 'list', 'edit', etc on a grapestry page that would do the equivalent job of the Grails controllers with dynamic or static scaffolding

  6. Have some NetBeans support (hopefully coming in NetBeans 6.0) for Grails and Tapestry to make editing the Tapestry components in Groovy at least on par with doing them in Java



I did make some progress in accomplishing the first couple of bullets above. I kinda thought I had that down; however, it turns out that the "Development" grails configuration doesn't quite obey the same rules (it uses some resouce loaders that load resources directly from the ${app_dir}/grails-app/view and controllers directory), so I'm working on some workarounds for that.

Just yesterday, I got the Grails plugin account, but I still need to take a look at a standard grails plugin structure before I put anything out there.

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.


Saturday, August 04, 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))

Friday, June 15, 2007

CDJUG June meeting

I was at the Capital District Java User Group (CDJUG) meeting yesterday and it was quite an interesting experience. For starters, I wasn't sure if I was going to the meeting until the last minute, and I didn't even know if I was going to stay the whole time. Secondly, at the last minute I was pleasantly surprised that there were going to be 2 presentations at the meeting.

Anthony will be posting the presentation slides in the next few days on the site, but in the meantime, I'll write a few notes from the meeting.

THe first presentation was on the Sleep (JVM) scripting language by Raphael Mudge. He was a very energetic guy, and it was quite a bit of fun just listening to him. Good work Raphael ! Now, the presentation was great, but I personally am far from being convinced that this is something that I will be investing my time into. Here's the deal:
  1. I've already dabbled into at least a couple of JVM scripting languages (Jython in the past, and now Groovy). THere is a couple of dozen more scripting languages on the JVM which are all interesting to mess around with. Thus, in order to convince a developer to spend time with the language, a scripting language needs to have a number of different characteristics. Granted, Raphi has quite an interesting character, the conversation with him before the presentation was very engaging. At the same time, as with any open source project, the key factor in whether the project will survive is the size of the community. Now, I know Raphi said that there were a bunch of people using the scripting language; however, as it is with a bunch of other open source projects, typically the top 1-2 contenders in the space gather 90+% of the developer attention and community. Although the other languages are not necessarily doomed to fail, they have to find their own niche and a set of committed developers in order to survive. Granted, it seemed like SLeep already has a niche by being used in jIRCii; however, it appeared that Raphi was the sole significant developer. So, the question is, what happens when/if he loses interest ?
  2. Raphi specifically mentioned that he's not trying to be all things to all people, and considering that it is a much smaller project and community than the other scripting languages, that is a very reasonable goal. However, in order to win developer mindshare, it seems to me that having a decent size standard library with utility functions and being able to call into the JVM for everything else, it's not enough any more. I'm judging from my personal experience: one of the major selling points of Groovy to me was that it DID have the convenient, easy xml processing, JDBC access, command line interface, etc. etc. etc, all on top of its seamless integration with the JVM.
  3. Now, this is just me, but I never grokked the Perl syntax. Seriously, this was probably the first language that I looked at when I was first trying to understand what web programming was all about (10 years ago).. and today, I'm still looking at the syntax and it can never fit into my skull...
  4. A couple of notes on the presentation:
  • I didn't quite get it if slee was able to extend the classpath at runtime. Raphi showed thate there was an interactive console (big plus in my opinion, Groovy still kinda sucks in this area); however, one big selling point for me was that at any point I could throw in a couple more jars to the classloader and start loading classes from there.
  • Raphi mentioned that it was his intent to have a clear separation between the sleep program and where it calls into Java; however, for me personally, that was a downside. I found the method calls to be kinda cryptic ( I couldn't quite follow what was happening in a couple of nested method calls - I admit, it might just be my fault, as I haven't seen Objective-C syntax on which the Sleep syntax is based). At the same tie, I didn't see any type of bean properties (e.g. instead of doing foo.setBar('baz'), doing foo.bar='baz'), which for a scripting language is a must in my opinion.
  • The documentation seemed to be excellent
  • All the time when I was looking at the presentation, I was thinking that after NetBeans 6.0 comes out, he should have a Schliemann module for Sleep syntax. :-)
  • Runtime size was pretty small , big plus for embedding uses...

Now, the second presentation was the main reason why I went to the meeting. It was about NetBeans by the author of Pro NetBeans IDE 5.5 Enterprise Edition, Adam Myatt. I was really thrilled to meet him as I had heard from Anthony that he lives in the Capital District area, and that he's a big NetBeans supporter. I was also really looking forward to checking out his book as I had read a couple of good reviews. Adam did an excellent presentation, he was on his game, and I have rarely seen someone present so many things in such a tight and organized manner. He did an excellent job at presenting NetBeans to the people who had gathered and received a very positive response - one of my coworkers actually re-installed NetBeans the next day and was trying stuff out with it. Way to go , Adam ! One thing that totally blew me away during the presentation was the relatively small footprint of NetBeans. When he was doing the demo of NB 5.5.1, the memory rarely went above 100 MB, and I was quite impressed since he was doing quite a bit (and his laptop was not the greatest to begin with). Considering that he did everything from UML diagrams to Visual Web Pack demos, I was VERY impressed. Then, when he was doing the NetBeans 6.0 preview, I was even more impressed as it hovered around 50 MB - AMAZING !

I was personally a bit disappointed that I didn't learn too much from the presentation. I understand that I was not the target audience for it, as I've been a NetBeans user for quite a long time, and I knew 99% of the stuff that Adam talked about. He was covering a lot of ground (more width than depth) as was necessary to present to the majority of the audience. But all in all, I liked the presentation and I'm looking forward to the next time we meet up : maybe we might even get a chance to talk a bit..

Thursday, June 14, 2007

I posted the same message to the NetBeans users mailing list and so far I haven't gotten any responses on it (http://www.netbeans.org/servlets/ReadMsg?list=nbusers&msgNo=93210). I'm re-posting it here again, just in case somebody follows my blog and maybe has ideas about how this is handled best ...

----------------- NetBeans Users maling list post ----------

Can anybody point me to any resources that describe best practices in
managing dependencies between multiple NetBeans modules and suites ?

Overall, here is my problem:

I have a module suite (e.g. MySuite1) that uses a couple of external
libraries e.g. foo1.jar and foo2.jar . I know that NB provides a library
wrapper module, so, I create library wrappers for each one of these
external jars e.g. foo1-lib and foo2-lib . As I understand, when I create
the library wrapper modules, the foo1.jar and foo2.jar are copied into a
private directory for the module e.g.
${foo1-lib}/release/modules/ext/foo1jar .

Then, I create a second module suite : MySuite2, which happens to need the
same two external jars.
- Should I create 2 new library wrapper modules for foo1.jar and foo2.jar
so that I can add the new library wrapper modules to MySuite2, or should I
just use the library wrapper modules that I created for MySuite1 ?
- The problem there is that if I don't create new library wrapper modules
and simply add foo1-lib and foo2-lib modules to MySuite2, then they will
be removed from MySuite1 (and when I want to work on MySuite1, I have to
move them back to MySuite).
- If I create new library wrapper modules for MySuite2 (e.g. foo1-lib2
and foo2-lib2 modules), then there is going to be yet another copy of the
original jars in the new module's releas/modules/ext/foo1.jar, which then
becomes a pain to manage (e.g. if a jar is used in 10 different modules,
each one will have a private copy. Then if I build a new version of the
jar, I need to update all 10 private copies in each module).
- Finally, if I have multiple modules in different suites wrapping the
same jar, if I install 2 different modules suites into the same IDE, amd I
going to have multiple copies of the same jar, or will the IDE figure out
that they are the same (e.g. if all have the same version numbers and code
name base) and only install one of them ?

I would appreciate any tips on how this is handled best.

Thursday, May 31, 2007

JUnit results in free form projects

At work, I'm working with a couple of colleagues on a project. Now, as is probably usual in many other environments, we all use different tools to work on the project: I use NetBeans, and the other two guys use Eclipse and Emacs. Thus, we have agreed that the Ant build scripts will be "the truth" : they have to be maintained and kept as the main tool for building, testing, and running the application.

So, all of that is great; however, I really like my NetBeans IDE, and I just couldn't continue living life without being able to use all of it's goodness. The best thing about NetBeans is that it is Ant based and it has the smarts/hooks to understand what you're trying to do (even in a freeform project), so that it can help you best. Here are the steps that I took to get my testing configuration going for a freeform project:

1. The first thing I did is to review the NetBeans Advanced Free Form Project Configuration

2. I added a compile selected item and debug project tasks (the debug task was quite useful since the code I was trying to understand was kinda convoluted and the debugger was invaluable in understanding how it works):

Here is what I added to my nbproject/project.xml (the ide-actions section):

<action name='debug'>
<script>nbproject/ide-file-targets.xml</script>
<target>debug-nb</target>
</action>
<action name='compile.single'>
<script>nbproject/ide-file-targets.xml</script>
<target>compile-selected-files-in-test</target>
<context>
<property>files</property>
<folder>test</folder>
<pattern>\.java$</pattern>
<format>relative-path</format>
<arity>
<separated-files>,</separated-files>
</arity>
</context>
</action>
<action name='test.single'>
<script>nbproject/ide-file-targets.xml</script>
<target>run-selected-files-in-test</target>
<context>
<property>classname</property>
<folder>test</folder>
<pattern>\.java$</pattern>
<format>java-name</format>
<arity>
<one-file-only></one-file-only>
</arity>
</context>
</action>



My ide-file-targets.xml additions look like this:

<target name='compile-selected-files-in-test'>
<fail unless='files'>Must set property 'files'</fail>
<mkdir dir='${test.classes.dir}'></mkdir>
<javac srcdir='test' source='1.6' includes='${files}' destdir='${test.classes.dir}'>
<classpath refid='run.test.class.path'></classpath>
</javac>
</target>
<target name='run-selected-files-in-test'>
<fail unless='classname'>Must set property 'files'</fail>
<mkdir dir='${test.classes.dir}'></mkdir>
<junit dir='${test.classes.dir}' printsummary='true' showoutput='true' fork='true'>

<classpath refid='run.test.class.path'></classpath>
<formatter type='brief' usefile='false'></formatter>
<formatter type='xml'></formatter>
<test name='${classname}'></test>
</junit>
</target>
<target name='debug-nb' depends='compile, compile-test'>
<path id='sourcepath'>
<pathelement path='src/'></pathelement>
<pathelement path='test/'></pathelement>
<pathelement path='..\\DeclTypeSys\\src'></pathelement>
<pathelement path='..\\DeclTypeSys\\test'></pathelement>
</path>
<nbjpdastart transport='dt_socket' name='perspective' addressproperty='jpda.address'>
<classpath refid='run.test.class.path'></classpath>
<sourcepath refid='sourcepath'></sourcepath>
</nbjpdastart>
<junit dir='${test.classes.dir}' showoutput='true' printsummary='yes' fork='true'>
<jvmarg value='-Xdebug'></jvmarg>
<jvmarg value='-Xnoagent'></jvmarg>
<jvmarg value='-Djava.compiler=none'></jvmarg>
<jvmarg value='-Xrunjdwp:transport=dt_socket,address=${jpda.address},suspend=y'></jvmarg>

<formatter type='xml'></formatter>
<formatter usefile='false' type='brief'></formatter>
<classpath refid='run.test.class.path'></classpath>
<batchtest>
<fileset dir='${basedir}/test'>

<include name='**/**/*Test.java'></include>
</fileset>
</batchtest>
</junit>
</target>

After I did that, my JUnit results look like this:











So, that was OK, but far from great. I thought that there should be a way to invoke the NetBeans JUnit test runner; however, googling around for it didn't help much. Then, just when I was about to lose hope, I ran upon these couple of posts:

Binding Freeform to Output and
UPortal Develop by Greg Sporar .

3.A slight complication on my end : I really didn't want to mess around with the target name of the original build.xml since the other team members were using that already. Thus, I added the following task to my ide-file-targets.xml:

<target name='test-project'>

<antcall target='junit'></antcall>
</target>

and changed the test single target name to "test-run-selected-files-in-test", so that the target name starts with "test" and changed the corresponding entry in my project.xml to run the right target.

And now, my test results look like this:








BEAUTY !!!

Wednesday, May 23, 2007

NetBeans testing with Groovy

I'm making some progress on my thesis, and as one can expect, when I'm writing code, I need to be writing some unit tests for it. Now, I know that Groovy is an excellent candidate for writing unit tests (better than the traditional JUnit stuff that NetBeans supports out of the box). However, it isn't quite obvious exactly how is one supposed to use and run these Groovy unit tests inside of NetBeans (apart form hacking together a crude solution where you have to add a line to a file, every time you need something to the suite() method). I just want to be able to hit the Alt-F6 button and have all of my tests run like magic : no manual additions to the suite, no tweaking.

So, in the end it worked, with a couple of gotchas:
1. NetBeans only seems to like running JUnit Test cases (when you hit Alt-F6) if and only if the test case name ends with "Test". That's kinda clunky, and as far as I know is not a requirement of JUnit itself. THere is nothing preventing you from executing the unit test individually (e.g. right-click -> run) - it runs like magic, but unless the class name ends with "Test", NetBeans doesn't add it to the bucket of tests to run.

2. The coyote module provides some support for testing in Groovy; however, it is not entirely intuitive exactly how that is done. In effect, if you want things to work nicely, you have to do 2 things:
- first, select a class that you want to test, go to Tools - Groovy Tests - Create Tests. That will basically greate a suite and a test in the $PROJECT/groovy-tests directory, as well as a groovy class in there.

A note a couple of months later, after the completion of the thesis project: Groovy worked great for the unit and integration testing of my project. It was fairly easy to script any of the scenarios that I had in mind, and after I had the general template for working with the groovy tests, it became quite easy to have pretty decent test coverage. Actually, the Groovy tests ended up being one of the important reasons for managing to complete the project after losing all of the work that I had done for the last 1.5 years ( yeah, I know i'm dumb not to have a backup, so, if YOU are working on something that important, DO A BACKUP NOW!!!).

Here is what my setup looks like:



1. I have one Java class for each type of tests that I wrote in Groovy. That is useful to be able to say "Test Project" from the project menu, and have all Groovy Tests executed in a meaningful manner.

2. Each Java JUnit subclass, has something like this in it. In effect, that takes the all Groovy files starting with "Service" and makes test suite out of them. One thing to note is that (slightly inconventiently), when the unit tests are run, all test methods from all groovy files show up under the name of the Java test class e.g.





public static Test suite() throws Exception {
TestSuite suite = new TestSuite("HandlerTests");

suite.addTest(AllTestSuite.suite("/home/polrtex/Docs/UofS_SE/Thesis/Implementation/MvpService/test/groovytest","Service*.groovy"));

return suite;
}


3. Finally, the Groovy test has some test methods in it, which get executed when you run the "Test" command on the project.

One final note on the tests themselves. I was using Spring 2.0 for my project, so I wanted to use the same datasource and service classes that the Spring Framework provides. Now, I knew that there were a couple of supporting Spring test classes (e.g. org.springframework.test.AbstractTransactionalDataSourceSpringContextTests), but I wasn't quite sure how to use them when the test are implemented in Groovy. What I ended up doing was to have a base Java class that extended the Spring test class mentioned above, and then have all of my Groovy tests extend that class. One thing that was interesting to note here was that I had to use the Spring autowire by name option, and the population of protected variables e.g.

class DaoTest extends com.troymaxventurs.mvp.test.MvpBaseTest {
//protected ds;);
protected mvpDAO;
protected mvpDataSource;
DaoTest() {
super("DaoTest")
setPopulateProtectedVariables(true)
setAutowireMode(AUTOWIRE_BY_NAME)
}
}


The reason I needed it to populate the protected variables (and not the bean fields) is that when these Groovy tests were instantiated, Spring tried to populate some Groovy specific public properties (e.g. the metaClass property), and thus, it failed along the way. With the population of protected variables, I could specify exactly what I wanted to have populated, without having to worry about any magic that Spring does to discover what dependencies to inject.

Friday, March 30, 2007

Customizing the executable

I'm currently working on an NetBeans RCP based app, spending a huge
amount of my time on the platform. I got around to customizing the
application that I'm working on, and I did recall Geertjan's post about cleaning up
the app from the NetBeans specific stuff. Geertjan did mention that
you could fix the executable icon (on Windows) by using the resource
editor on the built zip file. Well, the altenative is to make a copy
of the executable from your ${harness.dir}/launchers/app.exe to your
local project dir, edit the executable resources by using the Resource
Hacker that Geertjan recommended, and modify the build script to use
the modified executable to use your updated executable for building
your app. So, the changes to my harness build file are as described
below. It is indeed a bit of a hack, as one I'd imagine that it
wouldn't be advisable to edit the global build scripts (that build for
all projects); however, it seems that this is a more universal need
for platform developers - one should be able to totally customize the
branding of the application. While the majority of the work is
possible to be done within NetBeans (with the excellent support of the
platform modules), the ability to customize the icon on the executable
is a very important one as well.

in the body of the build-launchers target ($platform_dir/harness/suite.xml):

right after :

<mkdir dir="${build.launcher.dir}/etc"/>
<mkdir dir="${build.launcher.dir}/bin"/>


Add the following :

<available file="branding/launcher/${app.name}.exe" property="local.launcher.found" />
<antcall target="make-local-launcher" inheritall="true"/>





then, add a new target in the same file

<target name="make-local-launcher" unless="local.launcher.found">
<mkdir dir="branding/launcher" />
<copy file="${harness.dir}/launchers/app.exe"
tofile="branding/launcher/${app.name}.exe" overwrite="false"/>
</target>

Monday, March 26, 2007

Vanished "Sun Smart Ticket" demo app

I'm in the process of scrambling some resources together for my thesis project which has to do with J2ME and video delivery. Now, I've been working on this project (on and off) for the last 1.5 years, so it's been a while since I went back to look at all the resources that I had used at the very beginning.

So, one of the excellent resource that I used from the beginning was the Sun Smart Ticket J2ME & J2EE demo application. I used it to learn the "best practices" when I was getting started, and to a large degree I used it as a template for the first prototype that I built. So, I was quite surprised to find out that the application in question has just vanished from the internet : it was not on the Sun site, it was NOWHERE !!! It was mentioned on a couple of Sun publications, it was in Michael Yuan's excellent Enterprise J2ME book.. but the actual source to the server and j2me client has just vanished.. Evidently, it used to be a part of what the "Wireless Blueprints", which is no more, with nothing to replace it...

Well, to sum it up, I found a copy on a borland site, so I thought I'd give it a mention, just in case somebody else is scrambling to dig up this prescious resource. This just makes me wonder though, why is Sun trying to bury this demo... It was an excellent demo, with some pretty outstanding design ideas (which are a little complex, but after getting over the initial learning curve, they're sheer brilliance).. Well, that's a question that I should probably ask some of the J2ME people... it's open source now, right...

Saturday, March 24, 2007

(Mac vs PC) vs Linux

I'm laughing my head off, the Novel spoofs on the Mac/PC ads (here and here) are hillarious. As I mentioned on a previous post, the originals were quite annoying, and this really neatly hits the spot for me... Rock on, Novell !

Tuesday, March 20, 2007

NetBeans Groovy console

Alright, I didn't think I'd be putting it out there, but since
Geertjan blogged about it, here is the source to what I was doing with
the Groovy console in NetBeans.. I didn't think I'd be putting it
somewhere, since it's a bit of a hack (a proper solution to the
problem would probably use GroovyShell to interpret things and use
purely NetBeans for the creation of the UI end of things). But, since
it's out there in the wild, here is my "New and Improved" NetBeans groovy console.
One cool thing to note is that it picks up the Groovy Console key mappings, thus all shortcuts work exactly the same.



package com.troymaxventures.nbgroovyconsole;

import groovy.lang.Binding;
import java.awt.BorderLayout;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;
import javax.swing.MenuElement;
import org.openide.ErrorManager;
import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerUtils;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;

/**
* Top component which displays something.
*/
final class GroovyConsoleTopComponent extends TopComponent implements
ExplorerManager.Provider, Lookup.Provider {

private static GroovyConsoleTopComponent instance;
/** path to the icon used by the component and its open action */
// static final String ICON_PATH = "SET/PATH/TO/ICON/HERE";

private static final String PREFERRED_ID = "GroovyConsoleTopComponent";
private ExplorerManager manager;
private Lookup lookup;

private GroovyConsoleTopComponent() {
initComponents();
manager = new ExplorerManager();
setName(NbBundle.getMessage(GroovyConsoleTopComponent.class,
"CTL_GroovyConsoleTopComponent"));
setToolTipText(NbBundle.getMessage(GroovyConsoleTopComponent.class,
"HINT_GroovyConsoleTopComponent"));
Binding bind = new Binding();
groovy.ui.Console console = new
groovy.ui.Console(this.getClass().getClassLoader(),bind);
bind.setProperty("console",console);
try {
console.run();
add(console.getFrame().getJMenuBar(),BorderLayout.NORTH);
add(console.getFrame().getContentPane(),BorderLayout.CENTER);
ActionMap am = this.getActionMap();
InputMap im =
this.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
setShortcutMaps(console.getFrame().getJMenuBar(),am,im);
console.getFrame().setVisible(false);
this.revalidate();
lookup = ExplorerUtils.createLookup(manager, am);
} catch (Throwable e) {
e.printStackTrace();
ErrorManager.getDefault().notify(e);
}


// setIcon(Utilities.loadImage(ICON_PATH, true));
}

private void setShortcutMaps(JMenuBar jmb, ActionMap am, InputMap im) {

for (JMenuItem me3 : getMenuItems(jmb)) {
Action a = ((JMenuItem)me3).getAction();
KeyStroke k = ((JMenuItem)me3).getAccelerator();
im.put(k,a.getValue(Action.NAME));
am.put(a.getValue(Action.NAME),a);
}

}

private List getMenuItems(MenuElement me) {
List thisLevelMenuItems = new ArrayList();
if (me!=null && me.getSubElements()!=null &&
me.getSubElements().length>0) {
for (MenuElement me1 : me.getSubElements()) {
if (me1 instanceof JMenuItem && !(me1 instanceof JMenu)) {
thisLevelMenuItems.add((JMenuItem)me1);
} else {
thisLevelMenuItems.addAll(getMenuItems(me1));
}
}
}
return thisLevelMenuItems;
}

public ExplorerManager getExplorerManager() {
return manager;
}
public Lookup getLookup() {
return lookup;
}
// ...methods as before, but replace componentActivated and
// componentDeactivated with e.g.:
public void addNotify() {
super.addNotify();
ExplorerUtils.activateActions(manager, true);
}
public void removeNotify() {
ExplorerUtils.activateActions(manager, false);
super.removeNotify();
}

/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
//
private void initComponents() {
pnlButtons = new javax.swing.JPanel();
pnlMain = new javax.swing.JPanel();

setLayout(new java.awt.BorderLayout());

}//


// Variables declaration - do not modify
private javax.swing.JPanel pnlButtons;
private javax.swing.JPanel pnlMain;
// End of variables declaration

/**
* Gets default instance. Do not use directly: reserved for
*.settings files only,
* i.e. deserialization routines; otherwise you could get a
non-deserialized instance.
* To obtain the singleton instance, use {@link findInstance}.
*/
public static synchronized GroovyConsoleTopComponent getDefault() {
if (instance == null) {
instance = new GroovyConsoleTopComponent();
}
return instance;
}

/**
* Obtain the GroovyConsoleTopComponent instance. Never call
{@link #getDefault} directly!
*/
public static synchronized GroovyConsoleTopComponent findInstance() {
TopComponent win =
WindowManager.getDefault().findTopComponent(PREFERRED_ID);
if (win == null) {
ErrorManager.getDefault().log(ErrorManager.WARNING,
"Cannot find GroovyConsole component. It will not be located properly
in the window system.");
return getDefault();
}
if (win instanceof GroovyConsoleTopComponent) {
return (GroovyConsoleTopComponent)win;
}
ErrorManager.getDefault().log(ErrorManager.WARNING, "There
seem to be multiple components with the '" + PREFERRED_ID + "' ID.
That is a potential source of errors and unexpected behavior.");
return getDefault();
}

public int getPersistenceType() {
return TopComponent.PERSISTENCE_ALWAYS;
}

public void componentOpened() {
// TODO add custom code on component opening
}

public void componentClosed() {
// TODO add custom code on component closing
}

/** replaces this in object stream */
public Object writeReplace() {
return new ResolvableHelper();
}

protected String preferredID() {
return PREFERRED_ID;
}

final static class ResolvableHelper implements Serializable {
private static final long serialVersionUID = 1L;
public Object readResolve() {
return GroovyConsoleTopComponent.getDefault();
}
}

}

Saturday, February 03, 2007

Better Mac/PC ads

Alright, these are hillarious. I'm always really annoyed by the really lame snotty ads that apple runs, so, here are a couple of REALLY good answers to that... Funny as hell here