Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Sunday, October 14, 2007

Tapestry 5.0.6 soon

I'm very much hoping to have Tapestry 5.0.6 out soon. I've been squeezing in some more time to work on Tapestry 5 and just added a BeanDisplay component (to complement the BeanEditor component).

The Ajax story for Tapestry 5 is still up in the air ... the specter of writing an abstraction layer has come back. And I still want better, smarter handling of Hibernate ... it seems to me that the BeanEditor should be capable of handling to-one relationships in a smart way. And Spring WebFlow integration would be a snap if I just had a couple of days to work on it.

I'm personally anxious to get Tapestry 5 solid and shipped ... but not before it's ready.

Monday, October 01, 2007

TheDailyTube: Powered by Tapestry 5

A posting on the Tapestry mailing list led me to TheDailyTube, a video sharing site powered by ... Tapestry 5. It may be "alpha", but people are putting their faith in Tapestry 5.

The site looks sweet and is plenty responsive. Can't really tell too much else about the site though ... they've turned off the default exception page. Still, this is the kind of high volume, outward facing site that I'm proud to have helped facilitate. Viva Tapestry 5!

Friday, September 28, 2007

One of the greatest Worse-Than-Failures, ever ...

Serializalicious, now on WTF is just one example Steve, Ben and I discovered in this application. The article is somewhat misleading, it makes it look like Steve put that code in there. Nope, Steve, Ben and I were all victims of the missing Architect. Funny, scary stuff.

Wednesday, September 26, 2007

Let there be Nightly Docs!

With just a little bit of tweaking, we now have full, nightly documentation for Tapestry 5. This is great news, as the public web site can start staying static between releases (thus all the docs there will reflect just the latest stable release), and the snapshots and documentation on tapestry.formos.com will also be in sync, reflecting what's currently going on in SVN trunk.

That should ease a lot of confusion, when the docs say something exists but it doesn't in the stable (non-snapshot) versions.

Tuesday, September 25, 2007

Dr. Seuss visits the Tapestry Mailing List

Just noticed this posting by Bill Holloway:

Will you work with JSP?

They mess up my MVC.
I will not work with JSP.

We'll put them in an IDE.

I don't want them in an IDE.
They screw up the MVC.
I will not work with JSP.

We'll put them down inside some struts.
You'll never see them,
In the guts.

You cannot hide them in the guts!
They're in your face when they're in struts!
I don't want them on my IDE.
They screw up our MVC!
I will not work with JSP.

We'll put them in some server faces.
Many use them, many places.
That way the MVC's ok.
Will you work with them today?

The XML of server faces
Bogs down the coders in those places!
You cannot hide them in the guts.
I will not work with them in struts.
Get them off my CRT!
They're $@%#*&ing up my MVC!
Give me back my Tapestry.

Tapestry 5 Nightly Builds

I've finally gotten around to setting up nightly builds for Tapestry 5. It just another build on the Bamboo server. You can get access to it by adding:

  <repositories>
    <repository>
      <id>tapestry-snapshot</id>
      <url>http://tapestry.formos.com/maven-snapshot-repository</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
  </repositories>
to your POM.

Eventually, we'll be publishing documentation every night too, but that's waiting on a Maven bug.

Saturday, September 15, 2007

Mac OS X bundles vs. Subversion

If you work on Mac OS X, you may have noticed how cool Macs deal with complex documents, things like Keynote presentations or applications themselves. They're stored as directories. The Finder hides this, making them look and act like individual files. This works nicely, often the contents of a bundle are simple text and XML files ... whereas the equivalent under Windows is either a very proprietary (and potentially fragile) binary format, or multiple files and folders that YOU have to treat as a unit.

Alas, this all breaks down when using Subversion. You can't just check in MyPresentation.key into SVN ... it will create those pesky .svn directories inside the bundle, and those will be destroyed every time you save your presentation.

My solution to this is to convert the bundles into an archive, and check in the archive. The bundle folders are marked as svn:ignore. I guess this reveals that I mostly use SVN as a safe, structured backup.

In any case, manually creating those archives can be a pain. So ... out comes my solution to many problems: Ruby.

The goal here is to find bundles that need to be archived; do it efficiently (only update the archive if necessary) and do it recursively, seeking out bundles in sub-directories.

#!/usr/bin/ruby

# Used to prepare a directory for commit to Subversion. This is necessary for certain file types on Mac OS X because what appear to be files in the Finder
# are actually directories (Mac uses the term "bundle" for this concept). It is useless to put the .svn folder inside such a directory, because it will
# tend to be deleted whenever the "file" is saved.  
#
# Instead, we want to compress the directory to a single archive file; the bundle will be marked as svn:ignore.
#
# We use tar with Bzip2 compression, which is resource intensive to create, but 
# compresses much better than GZip or PKZip.
#
# The trick is that we only want to create the acrhive version when necessary; when 
# the archive does not exist, or when any file
# in the bundle is newer than the archive.

require 'optparse'

# Set via command line options

$extensions = %w{pages key oo3 graffle}
$recursive = true
$dry_run = false

# Queue of folders to search (for bundles)

$queue = []

def matching_extension(name)
  dotx = name.rindex('.')
  
  return false unless dotx
  
  ext = name[dotx + 1 .. -1]
  
  return $extensions.include?(ext)
end


# Iterate over the directory, identify bundles that may need to be compressed and (if recursive) subdirectories
# to search.
#
# path: string path for a directory
def search_directory(dirpath)
  
  Dir.foreach(dirpath) do |name|
    
    # Skip hidden files and directories
    next if name[0..0] == "."
    
    path = File.join(dirpath, name)
        
    next unless File.directory?(path)
                  
    if matching_extension(name)
      update_archive path
      next
    end
    
    if $recursive
      $queue << path
    end
    
  end
  
end


def needs_update(bundle_path, archive_path)
  
  return true unless File.exists?(archive_path)
  
  archive_mtime = File.mtime(archive_path)
  
  # The archive exists ... can we find a file inside the bundle thats newer?
  # This won't catch deletions, but that's ok.  Bundles tend to get completly
  # overwritten when any tiny thing changes.
  
  dirqueue = [bundle_path]

  until dirqueue.empty?
    
    dirpath = dirqueue.pop
    
    Dir.foreach(dirpath) do |name|
      
      path = File.join(dirpath, name)
      
      if File.directory?(path)
        dirqueue << path unless [".", ".."].include?(name)
        next
      end
      
      # Is this file newer?
      
      if File.mtime(path) > archive_mtime
        return true
      end
      
    end
    
  end
  
  return false
end

def update_archive(path)
  archive = path + ".tar.bz2"
  
  return unless needs_update(path, archive)

  if $dry_run
    puts "Would create #{archive}"
    return
  end

  puts "Creating #{archive}"
    
  dir = File.dirname(path)
  bundle = File.basename(path)
    
  # Could probably fork and do it in a subshell
  system "tar --create --file=#{archive} --bzip2 --directory=#{dir} #{bundle}"

end

$opts = OptionParser.new do |opts|
  
  opts.banner = "Usage: prepsvn [options]"

  opts.on("-d", "--dir DIR", "Add directory to search (if no directory specify, current directory is searched)") do |value|
    $queue << value
  end

  opts.on("-e", "--extension EXTENSION", "Add another extension to match when searching for bundles to archive") do |value|
    $extensions << value
  end
  
  opts.on("-N", "--non-recursive", "Do not search non-bundle sub directories for files to archive") do
    $recursive = false
  end
  
  opts.on("-D", "--dry-run", "Identify what archives would be created") do
    $dry_run = true
  end
  
  opts.on("-h", "--help", "Help for this command") do
    puts opts
    exit
  end
end

def fail(message)
    puts "Error: #{message}"
    puts $opts
end

begin
    $opts.parse!
rescue OptionParser::InvalidOption
    fail $!
end

# If no --dir specified, use the current directory.

if $queue.empty?
  $queue << Dir.getwd
end

until $queue.empty? 
  search_directory $queue.pop
end

I do love Ruby syntax, it is so minimal, and lets me follow my personal mantra less is more.

I'm sure there's some edge cases that aren't handle well, such as spaces in path names and maybe issues related to permissions. But it works for me.

You do need to have tar installed, in order to build the archives. I can't remember if that's built in to Mac OS X (probably) or whether I obtained it using Fink.

In any case, you need to remember to execute prepsvn in your workspace, to spot file bundles that need archiving, before you check in. It would be awesome if Subversion supported some client-side check-in hooks to do this automatically.

Saturday, September 08, 2007

Itch scratching: Even better feedback for all thumbs typists

I was reading through Matt's rundown of Struts 2 (does it suck?) and he strayed into one of my most passionate areas: feedback.

He gave an example of an incorrect property name, and how that would be reported to the user. He showed examples from all the major frameworks, and the Tapestry 4 version, even without its proper CSS styles, won hands-down.

However, as I was reading and responding, it struck me that while other framework can barely tell you what you've done wrong, Tapestry 5 should be telling you how to fix it. In this case, advising you about the possible properties you could have specified, which I've added as TAPESTRY-1737 and fixed.

Here's the result. Not bad for ten minutes work. And remember: property names in Tapestry 5 are already case insensitive, which wipes out probably 50% of your typical typing errors anyway.

Thursday, September 06, 2007

Biled!

Well, Hani has finally Biled the Bearded One. But what a cheap and meaningless shot ... he's upset about a switch from commons-logging to SLF4J? What is it with Hani and commons-logging? Did commons-logging steal his prom date? Did it kick sand in his face at the beach? Maybe it touched him inappropriately as a child. In any case, he's strangely obsessive about anything that touches on the subject.

On a serious note ... people have come out of the woodwork to defend the use of JDK logging, especially inside Websphere (God help them), which does justify a pluggable approach. Again, Log4J does not quite fit all, and because it is based on classes, not interfaces, it gets in the way of mock testing.

Friday, August 31, 2007

The Blindness of James Gosling: Java as A First Language

I think Java is an excellent all-around-language. It is truly ubiqitous, well specified, highly performant and well accepted by the industry.

But there's a big difference between those credentials, and the credentials of the first language a developer learns. Regardless, James Gosling feels obligated to recommend Java as the first language anyone learns.

Java as a first language? Please! Yes, Java is simpler than C++, C, Lisp, assembler and all the languages that seasoned veterans, such as James Gosling, are familiar with. But the fact that Java shields you from pointers, malloc() and zero-terminated strings does not make it a good first language!

Java is extremely monolithic: in order to understand how to run a simple Hello World program, you'll be exposed to:

  • Classes
  • Java packages
  • Static vs. instance methods
  • Source files vs. compiled classes
  • Editing vs. Execution
  • Using a compiler or an IDE
  • Method return types and method parameter types
  • The magic (for newbies) that is "System.out.println()"

... in fact, this list is far from exhaustive. A lot of that has fallen off our collective radar ... we're blind to it from seeing it so much over the last ten years or more.

What's important to understand is that people new to programming don't really have a way of understanding the difference between a document and a program, between source code and an application. Certainly the web (with HTML and JavaScript all mixed together) hasn't helped people understand the division intuitively. It's very hard for any of us to think like someone new to our entire world.

I'm rarely in a position to teach people how to program, but when I think about it, only two languages make sense to me: Ruby and Inform.

Ruby, because it is interactive yet fully object oriented. You can start with an interactive puts "Hello World" and quickly work up from there. You get to interact with the Ruby world as objects before you have to define your own objects. There's a nice clean slope from one-off quickies to eventual discipline as a true developer, without any huge leaps in the middle.

Inform, used to create interactive text adventures, is also intriguing. It's specifically designed for non-programmers, and has a laser-focused UI. It is interactive: you type a little bit about your world and hit Run to play it. Again, you experience an object oriented environment in an incredibly intuitive way long before you have to define to yourself, or to the language, what an object is.

Inform is truly some amazing software, given that advanced Inform games will make use of not just object oriented features, but also aspect oriented. Here's a little example I brewed up about a room built on top of a powerful magnet:

"Magnet Room" by Howard Lewis Ship

A thing is either magnetic or non-ferrous. Things are normally non-ferrous.

The Test Lab is a room. "A great circular space with lit by a vast array of ugly
flourescent lights.  In the center of the room is a white circular pedestal with a
great red switch on top. Your feet echo against the cold steel floor."

The red switch is a device in the test lab. It is scenery.

The double-sided coin is in the test lab. "Your lucky double sided half dollar rests
on the floor." The coin is magnetic.  Understand "dollar" as the coin.

A rabbits foot is in the test lab. "Your moth-eaten rabbits foot lies nearby."

Magnet Active is a scene.

Magnet Active begins when the red switch is switched on. 

When Magnet Active begins: say "A menacing hum begins to surge from beneath the
floor."

Magnet Active ends when the red switch is switched off.

When Magnet Active ends: say "The menacing hum fades off to nothing."

Instead of taking a thing that is magnetic during Magnet Active: say "[The noun]
is firmly fixed to the floor."

That's not a description of my program; that's the actual program. It's literate; meaning equally readable, more or less, by the compiler and the author. It's still a formal language with all that wonderful lexical and BNF stuff, it's just a bit richer than a typical programming language.

What I like about Inform is that you get a complete little game from this:

Magnet Room
An Interactive Fiction by Howard Lewis Ship
Release 1 / Serial number 070831 / Inform 7 build 4W37 (I6/v6.31 lib 6/11N) SD

Test Lab
A great circular space with lit by a vast array of ugly flourescent lights.  In the
center of the room is a white circular pedestal with a great red switch on top. Your
feet echo against the cold steel floor.

Your lucky double sided half dollar rests on the floor.

Your moth-eaten rabbits foot lies nearby.

>take dollar
Taken.

>drop it
Dropped.

>turn switch on
You switch the red switch on.

A menacing hum begins to surge from beneath the floor.

>take dollar
The double-sided coin is firmly fixed to the floor.

>take foot
Taken.

>turn switch off
You switch the red switch off.

The menacing hum fades off to nothing.

>take dollar
Taken.

>

The way rules work, especially in the context of scenes, is very much an aspect oriented approach. It is a pattern-based way to apply similar rules to a variety of objects. I've seen this kind of thing with aspect oriented programming in Java, but also with pattern based languages such as Haskell and Erlang.

The point is, using Ruby or Inform, you can learn the practices of a programmer ... dividing a complex problem into small manageable pieces, without being faced with a huge amount of a-priori knowledge and experience in order to get started. Both Ruby and Inform are self-contained environments designed for quick and easy adoption. That's something Java missed the boat on long, long ago!

Wednesday, August 29, 2007

Handling direct URLs in Tapestry 5

So, I'm starting work on the 1% web portion of my otherwise 99% Swing project. It's about sending invoices by email and allowing external users to view invoice PDFs, and accept or reject them.

I wanted to get a tracer bullet going; the process is kicked off when some COBOL code, via typical Rube Golderberg shenanigans, GETs a URL of the form /sendinvoice/invoice-number/email-address.

Well, in Tapestry terms, that could be a page with an activate event handler method:

package com.myclient.pages;

import org.apache.tapestry.util.TextStreamResponse;

public class SendInvoice 
{

    Object onActivate(String invoiceNumber, String emailAddress) 
    {

        // TODO: Generate and send the email
        
        return new TextStreamResponse("text/plain", 
          String.format("OK %s / %s", invoiceNumber, emailAddress));
    }
}
Returning a StreamResponse (that's the interface, TextStreamResponse is a simple implementation) allows your code to bypass the normal HTML response with one you control. This kind of thing was way too complicated to do in Tapestry 4 so I'm very happy with how minimal the Tapestry 5 approach is.

I fired this up and it worked the first time, echoing back to me the information I passed in the URL. If you don't provide at least two values in the URL, Tapestry will bypass this activate event handler method and will just render out the page; the page template simply announces to the user that they munged up the URL in some way.

This is something that would be pretty easy as a servlet, but is even easier in Tapestry. Just create the SendInvoice page and provide the onActivate() method. onActivate() gets called based on the number of path elements provided in the URL. When there are two (or more), this method gets invoked with the first two. Tapestry can automatically coerce from string to other types, but here both values happen to be strings anyway.

Obviously, much more to do (under deadline ... I don't really have time to blog this). I'm just happy to be doing a little Tapestry on my day job

Sunday, August 26, 2007

So long, commons-logging, hello SLF4J

I'm finally taking a bit to do something I've wanted to do for a long time: get away from commons-logging and switch over to SLF4J.

Commons-logging has a justified reputation for causing more problems than it solves. It's basic premise, that you should need to be able to switch between logging implementations, is very appealing to framework developers. Who am I to mandate that you use Log4J in your application? Sure, I mandate things like Javassist and a bunch of other dependencies, but for some reason, Log4J is too much. I'm not sure where this mindthink came from, but it is prevalent among framework folks.

Anyway, it's not a bad idea, though 99.99999% of applications use commons-logging as a wrapper around Log4J. I haven't yet met someone who uses JDK logging, or anything home brew. Why would you?

It's the implementation of commons-logging that the problem. First off, it called a "logger" a "log". I think that's just wrong, and (in fact) always preferred the term "category" (now deprecated by Log4J). To my mind, the category or logger is what you logged to, the log itself is the console, or a file, or something that receives the formatted output.

Also, commons-logging's ultra-late binding approach always causes class loader frustration and memory leaks. Every time they claim to get that fixed up, some new variation pops up.

The non-Apache follow on to commons-logging is SLF4J. It does things right: Logger, not Log. Better yet, Logger is an interface (it's a class in Log4J) which makes my EasyMock-addled brain quite happy. Finally, SLF4J takes a very simple, very sane approach: you combine the API JAR with one of several implementation JARs. That's it ... no class loader magic, no dynamic binding. This is an ideal solution for those same 99.99999% of applications out there.

This may cause some minor pain for Tapestry 5 early adopters, as they'll have to switch their (JCL) Log parameters to (SLF4J) Logger. Sorry, T5 is alpha ... but not for long, and this is a kind of last opportunity to make such a large change.

Update: Dion's been laughing from the sidelines but he's missing the point. He's put up a smokescreen that boils down to "just use System.out.println". But that's not what a logging framework is about ... it's about filtering, and organizing, a little bit about formatting, but mostly about being able to control what does log and what doesn't without hacking a lot of code (instead just tweaking a couple of control files).

Friday, August 24, 2007

Maven: Love to Hate or Hate to Love?

I've been struggling hard with Maven this week, and its really be bringing me down. I love the theory of Maven, but the execution is so awful I'm very close to eating crow and switching back to a Ant build, maybe using Ivy.

One of the things that concerns me with Maven is that for a tool whose goal is to be a "project comprehension tool" (giving quick access to Java source, Javadoc and other reports) it's almost impossible to find the source or get any insight into how Maven is built. Yet so much about Maven is impossible to find, with broken links, missing and out of date documentation, and other hazards running rampant.

I've been hunting around for 30+ minutes, trying to find the source to MavenCli. It's in the maven-core library. Good luck finding a link to that off the Maven web site. Through arcane means and Google searches I eventually stumbled across http://maven.apache.org/ref/ but 2.0.7 is not present there. So they rolled out a release without external documentation? I guess that's OK when you need a magical incantation to even find the documentation.

Well, I'm getting closer to my answers, but I'm probably still shiv'ed because Maven developer guidelines mandate zero documentation in the source. Or anywhere else. In fact, I believe the maven-fuckyou-plugin exists to strip documentation out of your source and seems to be enabled by default in all the Maven modules.

Well, eventually I answered my own question: -Dmaven.repo.local=directory allows me to control where the local repository exists. This is for my Bamboo server that needs to build multiple branches that share the same version numbers, so I don't want them to use the same repository.

Anyway, that's what it should be (from the tiny fraction of the code that's visible). But it blows up real good, apparently unwilling to download files into this repository, and spewing errors that make it appear as if artifacts were not present in the remote repository.

So, when Maven is useful it is very useful, but when it gets in your way, it totally blocks you.

Thursday, August 23, 2007

Quick and dirty Java application launcher

I've been busy working at a new company, for a new client, helping out on an existing product. The product is a Swing application and so I ran into my old nemesis: the ugly, buggy, hand maintained script to launch the application. You know the beast: start.bat and start.sh that you dread having to maintain.

We have a nifty, Maven based build, and a continuously evolving set of dependencies. Nobody wants to have to maintain that in two or three additional locations.

I've been seeing these kinds of scripts for years and hating them all along. Mostly, these scripts exist to find a bunch of JARs, put together a classpath (often by creating a monster CLASSPATH environment variable), then run Java to launch a particular class.

Previously, I've pushed to use Ant to do this. Ant is great a searching directories, building up classpaths, and invoking Java programs. It's pretty much inherently cross-platform. Who cares if it says "BUILD SUCCESSFUL" at the end?

Sun's tried to tackle this before, with the typical half-assed, lame results. Executable JAR files, Java Web Start, etc. Always stuff that looked good on some engineer's workstation, but never made sense in terms of a real development and deployment scenario. Ten years of Java later and it's still too much work to write a Java application to do a simple job. Meanwhile, Ruby and Groovy are so easy to run, it's painful (or laughable, or both).

JDK 1.6 has a new option to search a directory and pick up the JARs it finds. But we're targetting JDK 1.5 for both the client and the server. Thus my little launcher utility:

package org.example.launcher;

import java.io.File;
import java.io.FilenameFilter;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

/**
 * A wrapper used to assemble the classpath before launching the actual
 * application. The big trick is assembling the classpath before the launch.
 * Yep, this is functionality that should be built right into Java.
 * 
 * <p>
 * This is packaged into a JAR with no dependencies.
 * 
 */
public final class LauncherMain {

  private static final List<URL> _classpath = new ArrayList<URL>();

  /**
   * Usage:
   * 
   * <pre>
   * java -jar mblauncher.jar some.class.to.launch [--addclasspath dir] [--addjardir dir] [options]
   * </pre>
   * 
   * <p>
   * The --addclasspath parameter is used to add a directory to add to the
   * classpath. This is most commonly used with a directory containing
   * configuration files that override corresponding files stored inside other
   * JARs on the classpath.
   * 
   * <p>
   * The --addjardir parameter is used to define directories. JAR files
   * directly within such directories will be added to the search path. 
     * 
     * <p>Any
   * remaining options will be collected and passed to the main() method of
   * the launch class.
   */
  public static void main(String[] args) {

    List<String> launchOptions = new ArrayList<String>();

    if (args.length == 0)
      fail("No class to launch was specified.  This should be the first parameter.");

    String launchClass = args[0];

    int cursor = 1;

    while (cursor < args.length) {

      String arg = args[cursor];

      if (arg.equals("--addclasspath")) {
        if (cursor + 1 == args.length)
          fail("--addclasspath argument was not followed by the name of the directory to add to the classpath.");

        String dir = args[cursor + 1];

        add(dir);

        cursor += 2;
        continue;        
      }

      if (arg.equals("--addjardir")) {

        if (cursor + 1 == args.length) {
          fail("--addjardir argument was not followed by the name of a directory to search for JARs.");
        }

        String dir = args[cursor + 1];

        search(dir);

        cursor += 2;
        continue;
      }

      launchOptions.add(arg);

      cursor++;
    }

    String[] newArgs = launchOptions.toArray(new String[launchOptions
        .size()]);

    launch(launchClass, newArgs);
  }

  private static void launch(String launchClassName, String[] args) {

    URL[] classpathURLs = _classpath.toArray(new URL[_classpath.size()]);

    try {
      URLClassLoader newLoader = new URLClassLoader(classpathURLs, Thread
          .currentThread().getContextClassLoader());

      Thread.currentThread().setContextClassLoader(newLoader);

      Class launchClass = newLoader.loadClass(launchClassName);

      Method main = launchClass.getMethod("main",
          new Class[] { String[].class });

      main.invoke(null, new Object[] { args });
    } catch (ClassNotFoundException ex) {
      fail(String.format("Class '%s' not found.", launchClassName));
    } catch (NoSuchMethodException ex) {
      fail(String.format("Class '%s' does not contain a main() method.",
          launchClassName));
    } catch (Exception ex) {
      fail(String.format("Error invoking method main() of %s: %s",
          launchClassName, ex.toString()));
    }
  }

  private static void add(String directoryName) {
    File dir = toDir(directoryName);

    if (dir == null)
      return;

    addToClasspath(dir);
  }

  private static File toDir(String directoryName) {
    File dir = new File(directoryName);

    if (!dir.exists()) {
      System.err.printf("Warning: directory '%s' does not exist.\n",
          directoryName);
      return null;
    }

    if (!dir.isDirectory()) {
      System.err.printf("Warning: '%s' is a file, not a directory.\n",
          directoryName);
      return null;
    }

    return dir;
  }

  private static void search(String directoryName) {

    File dir = toDir(directoryName);

    if (dir == null)
      return;

    File[] jars = dir.listFiles(new FilenameFilter() {

      public boolean accept(File dir, String name) {
        return name.endsWith(".jar");
      }

    });

    for (File jar : jars) {
      addToClasspath(jar);
    }

  }

  private static void addToClasspath(File jar) {
    URL url = toURL(jar);

    if (url != null)
      _classpath.add(url);
  }

  private static URL toURL(File file) {
    try {
      return file.toURL();
    } catch (MalformedURLException ex) {
      System.err.printf("Error converting %s to a URL: %s\n", file, ex
          .getMessage());

      return null;
    }
  }

  private static void fail(String message) {
    System.err.println("Launcher failure: " + message);

    System.exit(-1);
  }

}
This gets packaged up as an executable JAR (i.e., with a MainClass manifest entry pointing to this class). Launching ends up looking like.
java -jar launcher.jar --addjardir lib org.example.MyMain --my-main-opt
Basically, the -jar launcher.jar and its options (--addjardir) replaces any other classpath manipulations. Once the classpath is setup, launcher emulates Java in terms of locating MyMain and executing its main() method, passing any remaining options.

Named mocks in EasyMock 2.3

This is a great new feature of EasyMock 2.3: you can name your mocks as you create them.

This is wonderful news, I can't wait to try it out. Why? Because once you get going with half a dozen different mocks that implement the same interface, it's a lot of work to figure out which one actually failed. This is going to make creating and maintaining some of my more complex tests much easier.

Friday, July 27, 2007

Tapestry 5 Preview at OSCON 2007

Yesterday was my session at OSCON. 45 minutes to covert Tapestry 5? Not a hope, especially with Rod Johnson running a little long.

I got the typical stunned reaction, followed by "is it done yet? Can I use it?" People crave T5 once they see it, and the presentation I do barely scratches the surface of what it can do today.

I've converted the presentation to PDF, which is great considering I had to flash through a lot of stuff about the grid very quickly.

Saturday, June 30, 2007

Upgrading Maven 2.0.5 to 2.0.7

I've been using Maven a bit for my "day job", converting a multi-module Ant build over to Maven. Since I just pushed out Tapestry 5.0.5 (awaiting a release vote), it seemed like a good time to look at upgrading to Maven 2.0.7.

The "day job" work has shown that having a traditionally stuctured aggregate project is viable even when using Eclipse. The Maven plugin for Eclipse is smart enough to read the parent POM as well as the child POM's in each module when building the overall classpath. I expect to restucture Tapestry 5 this way pretty soon, which will make it even easier to checkout and build the code than today. This structure, as a single mega-project, will make things easier when refactoring across module boundaries (a pain to do today, when using multiple Eclipse projects). However, the command line build will be very important, as it will catch some dependency issues that the Eclipse build will not.

Speaking of which ... Maven 2.0.7 (and 2.0.6 I believe) has tweaked dependencies. I had a couple of "runtime" scope dependencies inherited from tapestry-ioc to tapestry-core. These compiled fine in 2.0.5 but need to be changed in 2.0.7 to compile. In my case, since javassist was a "runtime" scope in tapestry-ioc, none of the javassist classes were visible when compiling tapestry-core.

A handy tool inside Maven for diagnosing this is the dependency plugin:

$ mvn dependency:resolve
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'dependency'.
[INFO] org.apache.maven.plugins: checking for updates from tapestry
[INFO] org.apache.maven.plugins: checking for updates from howardlewisship.com
[INFO] org.apache.maven.plugins: checking for updates from codehaus.snapshots
[INFO] org.codehaus.mojo: checking for updates from tapestry
[INFO] org.codehaus.mojo: checking for updates from howardlewisship.com
[INFO] org.codehaus.mojo: checking for updates from codehaus.snapshots
[INFO] artifact org.apache.maven.plugins:maven-dependency-plugin: checking for updates from tapestry
[INFO] artifact org.apache.maven.plugins:maven-dependency-plugin: checking for updates from howardlewisship.com
[INFO] artifact org.apache.maven.plugins:maven-dependency-plugin: checking for updates from codehaus.snapshots
[INFO] ----------------------------------------------------------------------------
[INFO] Building Tapestry Core Library
[INFO]    task-segment: [dependency:resolve]
[INFO] ----------------------------------------------------------------------------
[INFO] [dependency:resolve]
[INFO] 
[INFO] The following files have been resolved: 
[INFO]    commons-codec:commons-codec:jar:1.3 (scope = compile)
[INFO]    commons-logging:commons-logging:jar:1.0.4 (scope = compile)
[INFO]    javax.servlet:servlet-api:jar:2.4 (scope = provided)
[INFO]    jboss:javassist:jar:3.4.ga (scope = runtime)
[INFO]    junit:junit:jar:3.8.1 (scope = provided)
[INFO]    log4j:log4j:jar:1.2.9 (scope = test)
[INFO]    org.apache.tapestry:tapestry-ioc:jar:5.0.5 (scope = compile)
[INFO]    org.apache.tapestry:tapestry-test:jar:5.0.5 (scope = provided)
[INFO]    org.easymock:easymock:jar:2.2 (scope = provided)
[INFO]    org.openqa.selenium.client-drivers:selenium-java-client-driver:jar:0.8.1 (scope = provided)
[INFO]    org.openqa.selenium.server:selenium-server:jar:0.8.1 (scope = provided)
[INFO]    org.testng:testng:jar:jdk15:5.1 (scope = provided)
[INFO] 
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 seconds
[INFO] Finished at: Sat Jun 30 00:20:31 PDT 2007
[INFO] Final Memory: 4M/9M
[INFO] ------------------------------------------------------------------------
~/workspace/tapestry-core
$

Wednesday, June 06, 2007

Talking in Boulder and Denver

I'm doing back-to-back JUG talks next week:

Jun 13: Denver JUG

Jun 14: Boulder JUG

I'll be doing the same talk, a compressed introduction to Tapestry 5.

Wednesday, May 16, 2007

Free and Excellent Code Coverage for Eclipse

The EMMA plugin for Eclipse is my latest addition to the "can't live without it" category. It allows you to run applications and test suites from within Eclipse and gather code coverage ... better yet, that code coverage data is shown visibly in your code, much as it is in an HTML Cobertura report.

The plugin is slick, fast, easy and non-intrusive.

The one thing you do need to do is split your output directories, so that production code goes into bin, and test code goes into test-bin; this allows you to turn off coverage information for your test classes, and just gather

Of course, IDEA also has code coverage, based on EMMA, built right in.

Tuesday, May 15, 2007

Tapestry at JavaOne 2007

JavaOne was a lot of fun this year; I didn't arrive in time for the JavaFX keynote, but I understood it to be underwhelming. Yep ... that's what Java needs ... to take on Adobe and Microsoft in Adobe's home territory.

On the other hand, this was a very social JavaOne; lots of good conversations and meeting with people I only know online.

I also attended the Java Server Faces 2.0 Expert Group kickoff. I can't really see where that's going to go, alas. It's not like everyone bowed down and said "JSF 2.0 shall be Tapestry" (not even Jacob Hookom, who does see Tapestry as a good model for much of JSF 2.0). But I did get a chance to chat with Gavin King ... about motorcycles. He's riding a Ducati now, and I'm thinking about picking up a new bike once I buy a house. Gavin --- I used to ride a Yamaha FZR1000.

The best part of the JSF meeting was when Jonathan Swartz stopped by. We shook hands and talked about how slow adoption of JCP standards.

Elsewhere ... the fun part about the RubyEnv parody ad ...

... is that Tapestry is jar #3. Interesting, though I guess it's hard to know what "JSF in a Jar" looks like, or "Struts in a Jar" for that matter. My intention is to reverse the meaning, make Tapestry 5 something that'll make the Ruby guys envious. We'll see.