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!

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.

Saturday, May 05, 2007

Tapestry at JavaOne

Not only am I doing my Tapestry 5 "BOF" session (Tapestry 5: Java Programming Language Power, Scripting Ease), but there's also TS-7354 (Fast Feedback Loop: Unit Testing Strategies for Tapestry) and BOF-9834 ( Grails, Sails, and Trails: Rails Through a Coffee Filter).

I'll be arriving Tuesday afternoon and staying through Saturday. JavaOne is always hectic, especially right around my session. However, I'd be glad to meet with people informally; drop me an e-mail at hlship AT gmail DOT com and we'll see.

Meanwhile, did anyone else suffer through Sun's Schedule Builder? Just a horrendous application, made all the worse that it is crying out for the Ajax treatment. Show me the sessions side-by-side, dynamically filter the page by day and time slot and session track, and let me click or drag the sessions I like. For god's sake ... don't make me schedule one sesson at a time and then scroll back to the top of the page!

Thursday, May 03, 2007

Launching IntelliJ for the first time...

First the damn cool crowd of NFJS speakers got me to buy a Mac, now they've suckered me into switching from Eclipse to IntelliJ. God help me, I don't even know where to start with this thing.

Tuesday, April 24, 2007

Duh! Annotation values as constants, not literals

One funny thing I discovered recently is that annotation values don't have to be literal values. You can reference constants. Thus I went from:

@Scope("perthread")
public class .... 

To:

@Scope(IOCConstants.PERTHREAD_SCOPE)
public class ...

I think this is a Good Thing, even though it's a bit more verbose (a static import helps there) because I know at compile time that there isn't a typo in my constant's variable name ... whereas @Scope("prthread") would not be caught until runtime.

Pleasing the Crowds, Improving IoC, Extending the Community

I've been quite busy of late: a nearby Tapestry 4 project to pay the bills, a bunch of chances to talk to crowds about Tapestry 5 (in Philadelphia, Minneapolis and at home in Portland, Oregon).

I've significantly changed my Tapestry 4 presentation; it now highlights the BeanForm component, then the Table component and lastly Trails. I'm showing the high end of what's possible in Tapestry, rather than showing people the gory little details up front. That usually gets their attention.

I then show what's going on in Tapestry 5 and that really get's peoples jaws dropping. People crave this. They want to use it today. That is the desired effect. Case-insensitive, pretty URLs. Live class reloading. No XML. Incredible performance. Best of breed exception reporting. And we've barely gotten started yet.

I've been busy: Guice has set a high bar for IoC containers, but it still doesn't have certain features that Tapestry is dependent upon. That hasn't slowed me from taking the best ideas from it. Tapestry 5 now has a similar approach to autobuilding, and in most cases, a Tapestry service no longer needs a service builder method. Instead, a module can define a static bind() method and use the ServiceBinder to tell Tapestry about service interfaces and service implementations: Here's an example from the Tapestry module itself:

    public static void bind(ServiceBinder binder)
    {
        binder.bind(ClasspathAssetAliasManager.class, ClasspathAssetAliasManagerImpl.class);
        binder.bind(PersistentLocale.class, PersistentLocaleImpl.class);
        binder.bind(ApplicationStateManager.class, ApplicationStateManagerImpl.class);
        binder.bind(
                ApplicationStatePersistenceStrategySource.class,
                ApplicationStatePersistenceStrategySourceImpl.class);
        binder.bind(BindingSource.class, BindingSourceImpl.class);
        binder.bind(TranslatorSource.class, TranslatorSourceImpl.class);
        binder.bind(PersistentFieldManager.class, PersistentFieldManagerImpl.class);
        binder.bind(FieldValidatorSource.class, FieldValidatorSourceImpl.class);
        binder.bind(ApplicationGlobals.class, ApplicationGlobalsImpl.class);
        binder.bind(AssetSource.class, AssetSourceImpl.class);
        binder.bind(Cookies.class, CookiesImpl.class);
        binder.bind(Environment.class, EnvironmentImpl.class);
        binder.bind(FieldValidatorDefaultSource.class, FieldValidatorDefaultSourceImpl.class);
        binder.bind(RequestGlobals.class, RequestGlobalsImpl.class);
        binder.bind(ResourceDigestGenerator.class, ResourceDigestGeneratorImpl.class);
        binder.bind(ValidationConstraintGenerator.class, ValidationConstraintGeneratorImpl.class);
        binder.bind(EnvironmentalShadowBuilder.class, EnvironmentalShadowBuilderImpl.class);
        binder.bind(ComponentSource.class, ComponentSourceImpl.class);
        binder.bind(BeanModelSource.class, BeanModelSourceImpl.class);
    }

The ServiceBinder interface is fluent, we could follow on with .withScope(), .withId() or .eagerLoad() if we wanted. In most cases, defaults from there are sensible or come from annotations on the implementation class (and therefore, rarely need to be overridden).

The primary mechanism for injection is via constructor parameters. In general, annotations are not necessary on those parameters any more, and Tapestry will find the correct object or service automatically (primarily by matching on parameter type).

Service builder methods are still useful for when a service involves more than just instantiating a class, such as registering it for some kind of notification from another service:

    public ComponentClassResolver buildComponentClassResolver(ServiceResources resources)
    {
        ComponentClassResolverImpl service = resources.autobuild(ComponentClassResolverImpl.class);

        // Allow the resolver to clean its cache when the source is invalidated

        _componentInstantiatorSource.addInvalidationListener(service);

        return service;
    }

The autobuild() method will construct an instance, performing necessary injections. We can then perform any additional realization before returning it.

The end result has been to simplify and minimize the amount of code in the module builder classes.

Tapestry IoC now supports services that do not have a service interface; the actual type is used as the service interface and the service is not proxied: it is created on first reference and can't have interceptors. Normal services are proxied on first reference, and only realized (converted into a full service with a core service implementation and interceptors) on first use (the first method call).

In a step away from the code, we are running a vote to add Dan Gredler as a Tapestry committer. I expect that to run successfully, and I also expect to add a few more people to the roles soon.

I'm getting very excited. Things are coming together nicely (but never fast enough).

Saturday, April 07, 2007

Ouch! VMWare Fusion is not compatible with VMWare Player

Turns out that VMWare Fusion, the VMWare for Mac OS X, creates an image that isn't compatible with the VMWare player on Windows. This is why I'm happy I do test runs, but it does mean I have to spend another few hours recreating my workshop environment on Windows, using VMWare Workstation. So much for catching a movie tonight, or leaving the house tomorrow.

Hopefully this is something that'll be corrected in a later release.

Tapestry 5 at JavaOne and OSCON

My session at JavaOne has been scheduled:

BOF-6563: Tapestry 5: Java Programming Language Power, Scripting Ease
Esplanade 307-310
May 9 20-07, 8:55pm

In addition to JavaOne and NFJS, I've also been accepted to speak again at OSCON this year. I'm looking forward to spreading the Tapestry 5 message and meeting people using Tapestry, as always!

Wednesday, April 04, 2007

More Servlet Spec Wish-List Ideas

I'm thinking back to an earlier post about Servlet Spec 2.5 and what's missing.

Here's a few more ideas, based on implementing Tapestry 5 and other things besides. My previous ideas still stand.

web.xml introspection: It would be great to know what's going on inside web.xml. An API to query the web.xml meta-data and learn the names and mappings of the other servlets and filters would be great. Tapestry 5 acts as a filter and it would be handy if it could know what other servlets were in the WAR to help it decide whether to process a URL as inside Tapestry, or let another servlet handle it.

Event registration: Many of the listeners possible with the servlet API only apply to objects created by the servlet container's brain-dead IoC container. If my code needs to recerive notifications as a say, HttpSessionAttributeListener, I currently have to put together a Rube Goldberg machine in order to receive those notifications. There should be an API, perhaps attached to the ServletContext object, for registering for such notifications.

Quality of Service: Currently we have single server or clustered. Single server is not scalable beyond a certain point. Clustered causes a lot of problems managing data and introduces lots of overhead in terms of copying session attributes around the cluster.

I would like at least one additional level of service: non-replicated cluster. It assumes sticky sessions such that all requests for a given session are handled by a single box within the cluster. If that box fails, then future requests will be sent to a different box within the cluster, and the application will receive a notification that data has been lost.

Really, most non-financial applications can survive a rare loss of data midway through a session, and having this intermediate service level would allow the vast majority of applications to scale much, much higher than is possible today. I've previously implemented something like this by storing an object in the HttpSession that contained a Map in a transient variable.

Concurrency documentation: Many objects in the Servlet API are shared between threads (ServletContext, HttpSession) and it would be good form, and very useful, to know the proper semantics there. Futher, there needs to be better documentation for application and framework developers about the implications thereof.

In light of what I've learned about the Java Memory Model from Brian Goetz (and his book, Java Concurrency in Practice) any non-synchronized access to a shared object, say a mutable object stored into the HttpSession, is suspect: a potential source of scary, non-reproduceable, intermittent concurrency bugs.

Should all methods of such objects be synchronized? Can the servlet container acquire a lock when the HttpSession is accessed and hold it for the application for the remainder of the request (that would be handy!). Could there be an API for allowing the framework/application to manage access to HttpSession variables?

It's clear that the HttpSession API was designed to store small, immutable objects such as Strings and Integers. It is also clear that most applications don't use it this way (though Tapestry is a little better because it mostly stores individual page fields in the session, which are often small immutables, whereas most frameworks and applications store more complex state holding mutable objects).

... well, I've run out of steam, but I'll be racking my brains for more ideas like these.

Tuesday, April 03, 2007

Tapestry 5 IoC from a Guice Perspective

So Guice has a few good ideas, and with Tapestry 5 still in a fluid state, I've absorbed a few of them into Tapestry 5 IoC. At the core, is the key ObjectProvider interface:

public interface ObjectProvider
{
    <T> T provide(Class<T> objectType, AnnotationProvider annotationProvider, ServiceLocator locator);
}

Implementations of this interface plug into a chain-of-command within Tapestry and, via the AnnotationProvider, can query for the presence of annotations to help decide exactly what is to be injected. A lot of Guice's examples, things like the @Blue service, are easily replicated in Tapestry 5 IoC, there's just not a lot of need for it. I supposed it would look something like:

public class BlueObjectProvider {
  private final Map<Class,Object> _blueObjects;

  public BlueObjectProvider(...) { ... }

    <T> T provide(Class<T> objectType, AnnotationProvider annotationProvider, ServiceLocator locator) {
      if (annotationProvider.getAnnotation(Blue.class) == null) return null;

      return objectType.cast(_blueObjects.get(objectType));
  }
}

Implicit here is a service configuration of implementations, keyed on object type, used when the @Blue annotation is present. Here's what the module would look like:

public class BlueModule {

  public ObjectProvider buildBlueProvider(Map<Class, Object> configuration)
  {
    return new BlueObjectProvider(configuration);
  }

  public void contributeMasterObjectProvider(
    @InjectService("BlueProvider") ObjectProvider blueProvider,
    OrderedConfiguration<ObjectProvider> configuration) {
    
    configuration.add("Blue", blueProvider);
  }

  public void contributeBlueProvider(MappedConfiguration<Class, Object> configuration,
    @InjectService("MyBlueService") Service myBlueService) {
    configuration.add(Service.class, myBlueService);
  }
}

public class OtherModule {

  public OtherService build(@Inject @Blue Service service) {
    return new OtherServiceImpl(service);
  }
}

On the one hand, this is more verbose, since its somewhat more procedurally based than Guice's approach, which is pattern based. On the other hand, it demonstrates a couple of key features of Tapestry 5 IoC:

  • Service construction occurs inside service builder methods; there's no layer of abstraction around service instantiation, configuration and initialization ... you just do it in Java code. There simply isn't a better language for descibing instantiating Java objects and invoking Java methods than Java itself.
  • Dependency injection is a concern of the module not the service implementation. The special annotations go in the module class, and the service implementation class is blissfully unaware of where those dependencies come from or are identified and obtained.
  • Dependencies are passed to the service builder methods, which do whatever is appropriate with those dependencies; in many cases, the dependencies are not passed to the instantiated class, but are used for other concerns, such as registering the new service for event notifications from some other service.
  • The contributeBlueProvider() method can appear in many different modules, and the results are cumulative. This allows one module to "lay the groundwork" and for other modules to "fill in the details". And, again, if this style of injection ("flavored" with the annotations) really became popular, it would be easy to integrate it into the central Tapestry 5 IoC module so it could be leveraged everywhere else.

In fact, in the OtherModule class, you can see how the injection lays out once the groundwork is prepared: Just an @Inject qualified with @Blue and you're done. Again, I can't emphasize enough how well moving construction concerns into the service builder methods works; it gives you complete control, it's unit testable in a couple of different ways, it keeps the service implementations themselves clean (of dependencies on the IoC container -- even in the form of annotations). It also keeps explicit what order operations occur in. HiveMind chased this with ever more complex XML markup to describe how to instantiate, configure, and initialize services.

That last feature, the interaction of multiple modules discovered at runtime, is the key distinguishing feature, and the one that's hardest to grasp. For me, it is the point where an IoC container transitions from an application container to a framework container. In an application container, you explicitly know what modules are present and how they will interact. It's your cake, starting from raw flour, sugar and eggs. Nothings in the mix that you didn't add yourself.

With HiveMind and Tapestry 5 IoC, the goal is to build off of an existing framework, and its interwoven set of services. The framework, or layers of framework, provide the cake, and you are trying to add just the icing. Configurations are the way to inject that icing without disturbing the whole cake, or even knowing the cake's recipe.

So Guice is a good kick in the pants, and Tapestry 5 IoC has evolved; the old @Inject annotation, which used to include a string attribute to name the thing to inject, has been simplified. The @Inject annotation is still there, but the string part is gone. Tapestry primarily works from the object type to match to the lone service implementing that interface. When that is insufficient, there's the MasterObjectProvider and Alias service configurations, which provide plenty of room to disambiguate (possibly by adding additional annotations to the field being injected).

The big advantage is type safety; refactor the name of a service interface and you'll see little or no disruption of the service network, because dependencies are almost always expressed in terms of just the (refactored) service interface, rather than any additional layer of "logical name".

I think there's a growing issue with Google-worship. It was interesting and disturbing to see so many people announce "Spring is dead! Long live Guice!" on various blogs (and I'll even admit to a little envy ... how come Tapestry 5 IoC doesn't generate this kind of interest?). Guice itself is pretty useless by itself, just as Spring's IoC container is, by itself, useless. The value of Spring is in what's built on top of the container: Wrappers for JDBC, Hibernate, JPA, JMS and every other acronym you can name. Transaction support and interceptors. And, more recently, the really solid AspectJ layer. If Guice wants to be a Spring-killer, or even a real player in the IoC space, it needs to embrace and extend Spring: make it easier to use those Spring integrations than it would be using Spring natively.

Thursday, March 15, 2007

Ruby script for creating new Tapestry 5 projects

One thing with building lots of demos for upcoming presentations, labs, & etc. is that I'm having to use mvn archetype:create a lot and that command line is just hideous.

So I took a deep breath, stepped back, and wrote a Ruby script, newproj to simplify the process:

#!/usr/bin/ruby

require 'getoptlong'

opts = GetoptLong.new(
  [ "--group", "-g", GetoptLong::REQUIRED_ARGUMENT ],
  [ "--artifact", "-a", GetoptLong::REQUIRED_ARGUMENT ],
  [ "--package", "-p", GetoptLong::OPTIONAL_ARGUMENT ],
  [ "--version", "-v", GetoptLong::OPTIONAL_ARGUMENT ]
)

group = nil
artifact = nil
package = nil
version = "1.0.0-SNAPSHOT"
error = false

begin
  opts.each do | opt, arg |
    case opt
      when "--group" 
        group = arg
      when "--artifact" 
        artifact = arg
      when "--package" 
        package = arg
      when "--version" 
        version = arg
    end
end
rescue GetoptLong::Error
  error = true
end

if error || ARGV.length != 0 || group == nil || artifact == nil
  puts "newproj: --group groupId --artifact arifactId [--package package] [--version version]"
  exit 0
end

package = package || "#{group}.#{artifact}"

command = "mvn archetype:create -DarchetypeGroupId=org.apache.tapestry -DarchetypeArtifactId=quickstart -DarchetypeVersion=5.0.3"
command << " -DgroupId=#{group} -DartifactId=#{artifact} -DpackageName=#{package} -Dversion=#{version}"

puts command

Kernel::exec(command)

I'll eventually add to this; it needs the option to control the mvn -o (offline) flag, and further in the future, the ability to choose the correct archetype (once we add more than just quickstart). But this sure beats copying and pasting out of the documentation, like I've been doing.

Monday, March 05, 2007

Q: What's your toughest coding challenge? A:...

I've seen this question before on resumes and other people's blogs. I've never had a specific example that worked well. Previously, I've had vague stories of struggling with, say, Javassist ... but the best story here would involve my own code.

A few weeks ago, I noticed that some of my tests, the integration tests, didn't run very well on the Tapestry Bamboo server (our continuous integration server). Occasionally, even when developing on my Mac, I'd see these anomalous errors.

When things work in unit tests and fail in integration tests, one of the first places to look is for concurrency issues. Tapestry has a lot of code related to concurrency: synchronizing, caching, clearing of caches, on-demand instantiation, the works.

I started seeing things that made me question Java reality. For instance, this method is only invoked from a dynamic proxy, from a synchronized method, and the proxy only invokes the method once, yet I was seeing multiple calls:

public class OneShotServiceCreator implements ObjectCreator
{
    private final ServiceDef _serviceDef;

    private final ObjectCreator _delegate;

    private boolean _locked;

    public OneShotServiceCreator(ServiceDef serviceDef, ObjectCreator delegate)
    {
        _serviceDef = serviceDef;
        _delegate = delegate;
    }

    /**
     * We could make this method synchronized, but in the context of creating a service for a proxy,
     * it will already be synchronized (inside the proxy).
     */
    public Object createObject()
    {
        if (_locked)
            throw new IllegalStateException(IOCMessages.recursiveServiceBuild(_serviceDef));

        _locked = true;

        return _delegate.createObject();
    }

}

The code that calls this looks something like:

private synchronized SomeService _delegate()
{
  if (_delegate == null) {
    _delegate = (SomeService) _creator.createObject();
    _creator = null;
  }

  return _delegate;
}

You can see how this would tend to drive you a bit crazy. Eventually, I realized what was going on ... sometimes a runtime exception (an OutOfMemoryError) would be thrown, so the proxy would never complete _delegate(), and would (later, possibly in a different thread), re-invoke this the createObject() method ... and fail, because the lock was set. Solution:

     */
    public Object createObject()
    {
        if (_locked)
            throw new IllegalStateException(IOCMessages.recursiveServiceBuild(_serviceDef));

        // Set the lock, to ensure that recursive service construction fails.

        _locked = true;

        try
        {
            return _delegate.createObject();
        }
        catch (RuntimeException ex)
        {
            _log.error(IOCMessages.serviceConstructionFailed(_serviceDef, ex), ex);

            // Release the lock on failure; the service is now in an unknown state, but we may
            // be able to continue from here.

            _locked = false;

            throw ex;
        }

    }

I also started seeing bizarre error messages, like: Unable to resolve page 'MyPage' to a component class name. Available page names: Start, MyPage, YourPage.. What the hell was going on there ... was something modifying the underlying CaseInsensitiveMap? But the access methods are synchronized.

Once you start seeing bizarre concurrent behavior, and after listening to a few Brian Goetz talks about concurrency (not to mention his great book) ... well, you can start getting paranoid. Maybe its a bug in the JVM (trust me, it's never going to be a bug in the JVM). Maybe I'm not understanding access to effectively immutable objects outside of synchronized blocks (that's a bit more reasonable).

Then I saw something even more bizarre:

    public  T getService(String serviceId, Class serviceInterface, Module module)
    {
        notBlank(serviceId, "serviceId");
        notNull(serviceInterface, "serviceInterface");
        // module may be null.

        ServiceDef def = _moduleDef.getServiceDef(serviceId);

        if (def == null) throw new IllegalArgumentException(IOCMessages.missingService(serviceId));

        if (notVisible(def, module))
            throw new RuntimeException(IOCMessages.serviceIsPrivate(serviceId));

        Object service = findOrCreate(def);

        try
        {
            return serviceInterface.cast(service);
        }
        catch (ClassCastException ex)
        {
            // This may be overkill: I don't know how this could happen
            // given that the return type of the method determines
            // the service interface.

            throw new RuntimeException(IOCMessages.serviceWrongInterface(serviceId, def
                    .getServiceInterface(), serviceInterface));
        }
    }

See the comment about how that code is not reachable? It was reached! It took a while, but I found out that sometimes I was getting back the wrong ServiceDef object out of the ModuleDef. Rarely, but sometimes.

Then it struck me ... all of this strange behavior was traced to my CaseInsensitiveMap implementation. Sure it has a 100% code coverage test suite ... but when I double checked the code I found some sloppiness: it uses a couple of instance variables as a scratch pad while searching. This means that, under the right timing, even reads of the effectively immutable Map by different threads would interfere with each other, with Thread A getting the result from Thread B's query. Here's a diff of the solution, which basically moved those two scratch variables into their own inner class.

Things are working perfectly (for now), which is a great relief. This bug hunt was a distraction from other things, but better to tackle it now than later. This is also a good example of the need for a continuous integration server ... the fact that the server is on a different OS and JDK ensured that the tests would fail pretty consistently on the CI server even though they mostly worked on my Mac. Brian states this as well is his book ... test on all sorts of hardware with all sorts of memory configurations, because you don't know which one aligns with your bugs and brings them out into the open.

Saturday, March 03, 2007

T5 Spring Integration -- Hibernate next?

I've put together a first pass at Tapestry 5/Spring integration as another new module: tapestry-spring-integration.

It's small and to the point, leveraging the normal Spring configuration for web applications, just making beans available for injection into Tapestry components and services. Also, it makes accessing the Spring beans (from the Tapestry side) case insensitive.

Next up will be some form of Tapestry 5 / Hibernate integration ... however, due to the conflicting licenses, I may take a pass at tapestry-ejb3.

That licensing is driving me crazy; I've checked repeatedly with the Lords of Apache Licensing, and they maintain that the ASL is not compatible with the LGPL, that by linking to LGPL code (importing LGPL classes, in Java terms) the LGPL "infects" the ASL code, adding an unwanted restriction not present in ASL.

From the sidelines, it's funny and disturbing: The ASL folks talk about "fauxpen source licenses" as if openness was purely black and white, and the least restriction was a total betrayal. Meanwhile, the FSF camp keeps saying the licenses are compatible. Go figure.

I've had discussions with people who really got heated over ASL vs. LGPL. Andrew Oliver, for one, really tried to sell me on the idea that the ASL was a boon for corporations over individuals. From my position, the theoretical taking of "Tapestry" over by, say, IBM and rebranding it as "IBM Web Presentation Objects" (or something) would be laughable ... and even if it did happen, I think it would still be good for Tapestry, which is good for me, and good for the Tapestry community. True open source forks are really rare and look more like a straw man argument than a real consideration.

Wednesday, February 28, 2007

Screencast #5: Grid Component

The latest Tapestry 5 Screencast is now ready; this one clocks in at almost 11 minutes, and shows me forgetting parameter names and such as I set up a grid with my good ole' fallback: iTunes music data. I actually made a couple of mistakes on purpose to show off the exception reporting (that's only gotten better since 5.0.2, the base line for this screencast).

I think this shows off how quickly and easily you can pull data out of a service and up on the screen. To borrow a phrase: "I don't have time for drag and drop!"

One minor mistake was that I didn't map the context correctly (I had cut-n-pasted an existing launch configuration); thus all the URLs are prefixed with "hilo/" rather than, say, "musiclib/". Oh well. Oops, and I forgot to hide the dock!

Tuesday, February 27, 2007

Monday, February 26, 2007

Tapestry 5.0.2 released

A new preview release, Tapestry 5.0.2 is now available. It fixes a number of bugs and adds a number of features (including line precise exception reporting, and sortable Grid columns). Along the way, I found out some important deployment notes about Tomcat (and JBoss).

The new release is available via the central Maven repository, or via direct download.

Sunday, February 25, 2007

Tapestry 5 and Groovy

I was curious just how well T5 and Groovy would work together, so I took an existing application, enabled Groovy for it (using the Eclipse plugin) and added a Groove page to my existing application:

package org.example.hilo.pages;

class Holder
{
  String firstName;
  String lastName;
  int age;
}

class Groove {

 def message()
 {
   "Get your Groove on, Baby!"
 }
 
 def onAction()
 {
  println "onAction invoked"
 }
 
 def items()
 {
   [ new Holder(firstName: "Howard", lastName:"Lewis Ship", age:40), new Holder(firstName: "Scott", lastName: "Simon", age:42) ]
 }
}

Guess what? It just worked, no problems. I generated a Grid with the two Holder items, my onAction() method is invocable, and I can retrieve the message. It is just bytecode under the covers.

Alas, without some kind of annotation support, there's no way to mark a field as persistent, so there's tremendous limits on what can be accomplished right now. I've heard rumors that some kind of JDK annotation support is forthcoming; failing that, I've thought about an additional file, read at runtime, that would provide class, method and field annotations. But I'd rather someone else does it.

In addition, Tapestry is reading the compiled .class file; I haven't even thought about trying to get things to work from uncompiled .groovy files and I can imagine there may be some class loader headaches there.

Still, for five minutes effort, it was nice. And I really think I could come to really like the streamlined, sensible syntax ... and for god's sake, first class closures!

Saturday, February 24, 2007

Updated Tapestry Tutorial

I've updated the Tapestry 5 Tutorial, adding chapter 3, which is about the basics (including ActionLink). I may need to trim down some of the theoreticals, it's a bit verbose.

Tapestry 5.0.2 is ready to be announced on monday; one of the last things in was a renewed Exception Report page, including the return of line precise exception reporting. I've gotten better at design layout & CSS (though still pretty lame) but the results are looking good (you can seen a screenshot of the exception report page in the tutorial).

Thursday, February 22, 2007

T5 coming together rapidly

A long day today, fixing bugs, adding missing features, and otherwise getting things ready for another preview release, 5.0.2. I just re-implemented the support for line precise exception reporting (the part that shows the content of the file in error) and, with a bunch of new CSS tricks up my sleeve, it looks better than ever. At least, on Firefox it does. Somehow I'm sure the ExceptionReport page will pop up when I do the next screencast.

I also added a lot of documentation on page navigation in Tapestry 5, finally documenting exactly what event handler method return values are meaningful, and what behavior they trigger. Of course, that's extensible via service configuration contributions.

I've got many more things on my plate: there seems to be an issue with localization, at least according to one Chinese (or at least, Asian) user. I need to get cracking on nicely integrated JavaScript. I need to support dates with a date translator and some date validators. Lots of stuff related to Ajax, especially the intersection of Ajax and form support. Well, just plain lots and lots more. Joy.

Wednesday, February 21, 2007

See you at JavaOne!

My Birds of a Feather session, "Tapestry 5: Java Programming Language Power, Scripting Ease" is in for JavaOne. No details on when and where (I predict someplace dank and dark, and late at night when all the good parties are going) but still, it's good to get the nod this year, given how competitive it is.

I promised Ajax features and live demos; I haven't written the Ajax stuff yet, but the live demos will be cake with Tapestry 5.

Hope to see everyone there!

Tuesday, February 20, 2007

Fighting with Tomcat

It all started innocently enough, TAPESTRY-1287: Tapestry does not deploy properly under JBoss 4.0. Just a simple security or configuration problem, no biggie I thought.

Turns out, its not JBoss at all, it's Tomcat 5.5.20.

Tapestry 5 relies on a feature of class loaders: If you invoke ClassLoader.getResources() with a folder path, it returns the URLs of the folders. Sometimes this URL is for a file on the filesystem, and if you open the connection you get a series of lines identifying the files and sub-folders. Other times, the URL is for a JAR and you get a JARURLConnection and ultimately a JarFile instance.

Tapestry uses this information to scan the classpath, including the WAR, for all page and component classes. This is the key part of the case insensitivity feature.

This works great under Jetty and I didn't give it a second thought until this bug popped up.

Alas, Tomcat works very differently. It partially explodes the WAR on deployment, but doesn't extract the classes to WEB-INF/classes. It also doesn't do what Jetty and the JVM's ClassLoaders do, in terms of responding sensibly to folder paths (as discussed above).

That leaves Tapestry 5 out in the cold, because it can't locate any of the web application's pages, either via the supported approach, ServletContext.getResourcePaths(), or via the ClassLoader approach (which is not documented but completely reasonable).

Tapestry isn't the first one to hit the kind of problem; a recent JBoss bug, JBAS-2676 shows that the Facelets crew hit a similar problem, and I've added Bug 41664 to ASF's Bugzilla to see if I can get a response.

So, I'm starting to dig around, to see if there's any magic I can use to kludge together a workaround. I've been hunting around in the debugger, trying to find some object that has a reference to the original WAR, which I could open up and read, just to scan WEB-INF/classes.

If that doesn't come together soon, I'll need a different approach, such as a Maven task to locate the classes and add some kind of index file to the WAR that can be used at runtime. I've really been trying to avoid doing anything that requires anything special during build and deploy, I want everything to just work ... and Tomcat is letting me down!

Monday, February 19, 2007

Blog comments disabled temporarily

I've had to cancel blog comments for a little bit, until the current wave of comment spammers "play through".

Sunday, February 18, 2007

T5: Component Reference Documentation

Had a frustrating weekend struggling with Maven (those people are sadists) and with Javadoc (just plain idiots), but the end result is a Maven report plugin to generate component reference documentation.

Here's a sample, for tapestry-core.

Now, this is just the reference documentation, equivalent to JavaDoc: a starting point for real documentation. But it is generated right off the source files (via a JavaDoc doclet) so it is always automatically up to date.

Thursday, February 15, 2007

T5 Screencast #4: BeanEditForm

Just finished a new screencast on BeanEditForm: Tapestry 5 Screencast #4.

In twelve minutes (including some lovely fumbling around the 8 minute mark), I only scratch the surface of what BeanEditForm can do, and it's still not quite as powerful as Daniel Gredler's BeanForm component for Tapestry 4 ... but it is easy and fast and full of fun tricks.

Introducing this component first, and only incidentally talking about Label and TextField, is according to plan. I want people to get hooked on Tapestry 5's power and ease of use immediately.

Next up: the Grid component.

Wednesday, February 14, 2007

Direct Component Rendering in T5

One question that pops up in the T4 mailing lists quite often is "How do I render a PDF?" (or a chart, or an Excel spreadsheet, etc.).

In T4, this takes a bit of care and planning: a new engine service to represent the resource to be generated, perhaps a new Asset implementation that references the service. The necessary HiveMind configuration to add the engine service to the mix, and perhaps give it a friendly URL mapping. Perhaps a touch of web.xml for that last bit as well.

In Tapestry 5, there's an interface, StreamResponse, that may be used as the return value from an component event handler method. StreamResponse encapsulates a MIME content type, and an InputStream that will be read and pumped down to the client.

And that's really all it takes. Here's an example from the test suite:

public class Start
{
    Object onActionFromTextStreamResponse()
    {
        String text = "<html><body>Success!</body></html>";

        return new TextStreamResponse("text/html", text);
    }
}

This method is invoked when the corresponding component's link is clicked; from the template:

<a t:type="ActionLink" t:id="textStreamResponse">Text Stream Response</a>

This is just an event handler method; normally these return a page name or a page instance, and a client redirect is sent to get that page to render. By returning a StreamResponse, it is the stream response that is returned to the client (immediately, in the same request; there's no redirect).

That's the point of T5: Simple things should be easy.

Saturday, February 10, 2007

More Tapestry 5: Grid Component, Case Insensitivity

I've been busy building a Grid component to replace T4's contrib:Table component. It's coming out great, I can't wait to put up a screen cast of it soon (once I get sorting working). It has a clean UI (more default CSS), uses a Digg-style pager, at the bottom, and it's scary fast.

Better yet, its pretty automatic. If you pass the Grid a list of components, it builds its model from the first component (it expects them to be uniform). The model even understands the order of properties, based on the order of the getter methods within the bean class. It uses similar concepts to the BeanEditor to create default labels and so forth. Basically, it's zero configuration ... push a list of objects at it, and it will build the entire UI, column labels, column order ... the whole enchilada.

Meanwhile, I've been working under the covers to further extend the bounds of case insensitivity. It's now a hard-wired concept inside Tapestry IOC, which provides a CaseInsensitiveMap (a full java.util.Map where the keys are case insensitive) to properly support it. I'll be able to strip out a whole lot of string.toLowerCase() calls inside tapestry-core (I'm just taking a little break now). Back to the point ... now when you are injecting, contributing, or doing anything else with Tapestry IOC the case of the ids just don't matter anymore. One less thing to worry about while developing (no more "is that 'Checkbox' or 'CheckBox'?").

I showed off some Tapestry 5 to my friends at Formos and they loved every bit of it. Here's a company that develops exclusively in Tapestry, and has concurrent projects in Tapestry 3 and Tapestry 4 and they loved every bit of it. Many of my "innovations" were similar to things they extended Tapestry to do for their applications, but they appreciated the idea that it is baked into Tapestry itself. And there's a few other ideas I've had that haven't occured to anyone else yet.

It was very gratifying to see how excited they got by Tapestry IOC. "It's HiveMind -- without the XML, come check it out!". So it's nice to get a little confirmation that things are on the right track.

Monday, January 29, 2007

New Screencast: Tapestry Maven Archetype

I've added a new screencast, this one showing off the use of the tapestry-simple Maven archetype to create a new project. It runs about five minutes and shows off how simple it is to get up and running.

If you haven't built Tapestry 5 locally, you need to remember to add -DremoteRepositories=http://people.apache.org/repo/m2-snapshot-repository/ to the command line.

I'm thinking about starting a vote to do a preview release of Tapestry 5, just to get the necessary JARs and archetypes into the general Maven repository.

Sunday, January 28, 2007

Tapestry 5: Caseless URLs

Big improvements to Tapestry 5 URLs today. Whereas Tapestry 4 had "friendly URLs" (friendly to what? Mostly to search engines), Tapestry 5 has "pretty URLs" that look much more like their hand-tooled brethrern.

The news today is that the pretty URLs are now case-insensitive. Well, that's a slight exaggeration; the portion of the URL that specifies the name of the page and the component id within the page is now case insensitive. Thus http://localhost:8080/login.form.action is exactly the same, to Tapestry, as http://localhost:8080/LOGIN.FORM.ACTION1. The first form, all lower case, is what's generated by Tapestry by default.

Under Tapestry 5, there are really two main types of URLs: component event URLs and page render URLs. Page render URLs are now just the name of the page, i.e., http://localhost:8080/login ... there's no longer an extension tacked on. Submitting forms, or clicking component action links, does some processing and sends back a page render URL, so what you see in the browser's address bar is always a bookmarkable, short, relatively opaque URL.

In some cases, a page will have a "passified context" which is just a very generic way of saying, "the primary key for the entity this page displays". Thus you might have a ViewPerson page that uses some kind of numeric id as its context. The URL ends up being http://localhost:8080/viewperson/99. In other words, the URLs Tapestry is generating end up looking pretty much exactly like what you'd create in a hand-tooled application. And all of this with zero configuration. It just works.

1As I typed this, I realized that the last part of the URL, ".action", which is the component event name, is not currently case insensitive, something I'll address tormorrow.

Thursday, January 25, 2007

Tapestry 5 Maven Archetype

I've just put together a rather decent Maven Archetype for Tapestry 5. It's called "tapestry-simple" and its documented here.

The archetype allows you to create a Tapestry 5 application in, literally, seconds. It generates the pom.xml (for Maven), .classpath/.project (for Eclipse), as well as web.xml, log4j.properties, the complete directory structure, even a sample Start.html and Start.java.

It also configures the Jetty plugin so that you can immediately run the application using "mvn jetty:run".

This took only a little doing. The Maven documentation was awful as usual (yes, kettle, stove, etc.), but peeking at a couple of the sample archetypes, and trying some experiments, allowed me to put together something relatively complete and polished in a couple of hours. And those hours will save thousands of developers an hour or two each.

To make this work, you need to edit your settings.xml to add in the Apache snapshot repository at http://people.apache.org/repo/m2-snapshot-repository/ ... but perhaps better (and almost as easy) is to check out the latest Tapestry 5 source from SVN and build locally. You'll want the tapestry-project, tapestry-ioc, tapestry-core and tapestry-simple projects. Do a "mvn clean install" and all the latest and greatest will be in your local Maven repository!

Monday, January 08, 2007

1.0.0 releases for tapestry-prop, -flash, -spring

I've gotten no negative feedback on the main three libraries at Tapestry @ JavaForge so it's time for a 1.0.0 release for all three:

  • tapestry-spring - Tapestry/Spring integration
  • tapestry-prop - Highly efficient property access, used instead of OGNL in many palces
  • tapestry-flash - Temporary storage of data until the next request (often used for redirect-after-post messages)

The files are all available from my Maven 2 repository: http://howardlewisship.com/repository/. If you aren't a Maven user, the binary and source distribution are available at http://howardlewisship.com/downloads/tapestry-javaforge/.

Note that all of these are compiled against Tapestry 4.0.2, the latest stable release of Tapestry. Once Tapestry 4.1 has a stable release, I'll bump the version numbers and update the dependency.

Friday, January 05, 2007

Proposal for new property operator

There's been a few blog postings about adding property support to the Java language properly. Here's a synopsis. The goal is to make it easier to access object properties, using property names, rather than method names.

I'm amazed at some people's ability to Not Get It.

The proposal is too add a special operator, ->, for this purpose.

This means that you have to be aware that something is a property before you know how to access it.

Properties in other languages are powerful because they are accessed just like fields. You see this in Ruby especially. You can change your mind and switch from simple public instance variables to getter/setter methods at any time, without breaking any existing code. Ruby will even generate the getter and setter for you. Braindead simple.

So, Mr. Danny Coward, here's a proposal for you. I would like the property access operator to be ..

Let me make that more clear: .

You know, the exact operator used in other languages, the exact operator that anyone would expect.

Let the compiler do the work. The -> operator is an approach to make the compiler do less work and the developer do more. That is ass-backwards. I'm slaying dragons over here in Tapestry 5 land, to make the environment bend over backwards to adapt to the user, easily and seamlessly. It would be nice if the overlords in charge of the Java language would take that as their lesson. Complexity is not solved by adding more complexity. Complexity is solved by removing complexity ... even if it requires a bit more work behind the scenes to support that simplicity.

Of course, this may all be a distraction from their main attemmpt to screw the pooch: super modules. More on that soon.

Wednesday, January 03, 2007

Latest T5 Snapshots: Invisible Instrumentation

I just uploaded the latest T5 snapshots. I did some work on the template parser; you now have an alternate template syntax, more like Tapestry 4:

<span t:id="foo" t:type="Bar" param="literal-string"/>

In Tapestry 4, it would be the same, except jwcid="foo@Bar".

I'm struggling with what's the correct binding prefix? With <t:comp> the default binding prefix is "prop:", meaning a property name or property path unless an explicit prefix is given.

With invisibly instrumented elements, the expectation is that more of the parameters will be fixed string literals, so the default binding prefix is "literal:". Thus you might have to do the following:

<input type="text" t:id="userName" t:type="TextField" value="prop:userName" validate="validate:required"/>

However, there's some implemented and upcoming improvements that will shorten and simplify this snippet.

Notice that the t:id and value attributes are pretty much the same? That's a Dont Repeat Yourself violation; Tapestry 5 already handles this particular case: if the value parameter is omitted, it is assumed to match a property of the container with same name as the component's id. This is already implemented. So the example is reduced to:

<input type="text" t:id="userName" t:type="TextField" validate="validate:required"/>

The remaining ideas are not yet implemented.

We've put <input type="text"> into the template for preview purposes. Tapestry should be able to deduce that we want a TextField component. That reduces the example to:

<input type="text" t:id="userName" validate="validate:required"/>

The final change refers to the validate parameter. It's type is FieldValidator, not string ... that's why we're invoking the "validate:" binding factory to convert the string ("required") into a FieldVaidator instance. I'm not sure what this would look like (either special code on the TextField component, or some additional logic to invoke the ValidateBindingFactory), but it seems to me we should be able to remove the "validate:" portion:

<input type="text" t:id="userName" validate="required"/>

This is part of the goal of Tapestry, that whether you're writing templates or Java code, you should only write the bare minimum that's necessary, and Tapestry should be able to fill in the gaps. That's partly convention over configuration, but the overall concept goes deeper than that.

This last bit, omitting the need for explicit binding prefixes, was a battle I lost in the Tapestry 4 time frame, but the approach I was taking then was more involved; a default binding prefix for each parameter. I just want Tapestry to realize that I supplied a simple string, but the component needs a FieldValidator, and that those come from athe ValidatorBindingFactory service. Tapestry should be able to close the loop on that without generating any surprises.

Tuesday, January 02, 2007

HiveDoc for Maven 2

Marcus Schulte has put together a Maven 2 plugin for HiveDoc. It generates very good hypertext documentation, with a better layout than than the built-in documentation and well integrated into the overall Maven site. An example of the generated documentation comes from the honeycomb project (which is one of the Hibernate integration modules available for HiveMind and Tapestry). Happy New Year, Marcus ... and thanks!

Thursday, December 21, 2006

Waiting for VMWare Mac

I'm looking forward to doing more Tapestry 4 training fairly soon, and some Tapestry 5 training in the medium future. One important part of my arsenal is missing: VMWare for Mac. Four months ago they said "open beta real soon now" (i.e., by the end of the year). I hope I don't have to lug along my old Dell Laptop, just to run my VMWare Ubuntu image!

Tuesday, December 05, 2006

Tapestry 101

Warner Onstine's book on Tapestry 4: Tapestry 101 is now available!

Tapestry 101 is a great introduction to Tapestry using real-world examples. It goes beyond just using Tapestry components and dives into integrating Tapestry with Spring and with Hibernate. It's organized around a real application implemented in the small and should go a great way towards filling in the gaps in people's Tapestry knowlege.

It's available right now as a downloadable PDF.

Friday, December 01, 2006

The Difference between Blogs and Support Lines

The difference between Blogs and Support Lines:

Tapestry questions posted as blog comments don't get answered.

If you have a question about Tapestry, the tapestry user mailing list (users@tapestry.apache.org) is the correct starting point. Questions asked there get answered pretty quickly, by the Tapestry community (including the committers).

Failing that, you can contact me or any of the other Tapestry committers. Just remember that for us, Tapestry is a business and answering questions can be a distraction, a drain on resources that needs compensation.

From my perspective, every minute of my time should be spent on Tapestry 5. Any distraction from that, any time I could be working on T5 but end up working on something else, is a net negative for the Tapestry community as a whole.

So if you're one of the folks whose been posting various questions onto the blog as comments (I see all the comments because I moderate them) ... well, now you know why I don't usually answer. This simply isn't the right venue or the right situation.

Tapestry 5 Progress: Localization, Assets

Been rapidly reassembling Tapestry functionality inside the new Tapestry 5 code base.

A first pass at localization is now in place. It's missing a couple of key features and a bunch of bells and whistles, but the basics are in place.

I've also started implementing assets. Currently, only context assets are supported, but I'll be working on classpath assets next.

Both of these things were necessary detours before I could start work on form input validation in earnest.

The code is coming together rapidly and very nicely, very cleanly.

I've been stretching to meet "the principle of least surprise" ... just looking for ways that the framework can cleanly and easily do things automatically. For example, the @Inject annotation is very flexible; it takes into account the type of field when determining how to interpret its value. @InjectAsset would have been easier, but that's one more thing for people to remember.

Also, I'm working on an "automatic" Tapestry stylesheet. I want Tapestry apps to have a good, clean look automatically, by inheriting a base stylesheet from the application itself.

Interestingly, because of the way Tapestry 5 renders (to a simplified DOM), it won't be necessary to have Shell or Body components, as with Tapestry 4. There's a post-processing stage that will be able to navigate the DOM and make selected changes, somewhat like a limited version of SiteMesh. This stage will be able to do things like insert a stylesheet link into the <head>.

In fact, I think there will be significantly fewer components in Tapestry 5, especially because of mixins (which will allow existing components to be used in new ways, rather than forcing the creation of entirely new components).

Onward and upwards. Less is More

Monday, November 27, 2006

Firefox 2.0 stability on Mac OS X

I've found that Firefox 2.0 is simply not stable on Mac OS X (10.4.8 Intel). It was locking up on my multiple times a day. I downgraded to 1.5 and have had virtually no problems since (in the last couple of weeks).

It seemed like it had trouble with heavy JavaScript pages such as Google Mail. Again, 1.5 is fine.

It really is like being between a rock and a hard place; Safari is faster and more stable but doesn't do everything I want it to do (such as the WebDeveloper or FireBug plugins to Firefox). FireFox is less "OS-X-y" (James Duncan Davidson won't run it because it feels like a Windows program to him ... and it sure could use a few more glowing curvy glass buttons!). Some common JavaScript, such as animation effects from Dojo, run incorrectly in Safari but correctly in FireFox. So, what's next ... Opera?

Nope, probably stay with Firefox (1.5, until they admit there are Mac OS X problems and fix them). It's much more likely that my clients will be running FireFox than Safari or Opera (my clients, and my client's client's are almost universally running Windows). I think a shift has occured, where people understand that Firefox is the standard, and that IE may take some extra effort "to get working correctly". I'm not saying people don't expect things to run flawlessly in IE ... just that they understand that it's not automatic, that IE makes things harder.

Sunday, November 26, 2006

The Danger of Beta: Tapestry and HiveMind Blog has Moved

Some day there will be a medical diagnosis for my condition, which may be characterized as an overeagerness to adopt unstable beta software.

In this case, it was a switch to Blogger Beta, which promised all kinds of improvements to the user experience of reading this blog. Alas, the real improvements are of questionable value, such as publishing an Atom feed where the RSS feed used to be (and not writing the correct <link> tags).

Anyway, it became pretty evident that the new, post Google Blogger really wants to host the blog on blogspot.com. Thus the new URL:

http://tapestryjava.blogspot.com

Wednesday, November 22, 2006

Updates to Tapestry @ JavaForge

Well, I've had a busy morning. I've done a ton of work on the projects at Tapestry @ JavaForge.

I've changed the version number of everything to 1.0.0-SNAPSHOT.

Documentation has been updated. Site navigation updated. Look and feel is now aligned with everything else Tapestry.

This was spurred by a comment in the user mailing list about how tapestry-spring doesn't work with prototype beans. It does, but I haven't updated the documentation.

Of course, Maven 2 is letting me down here. It doesn't support site publishing via FTP (just secure FTP and various SSH related options). This adds a manual step to deploying the documentation that I'll roll into a Ruby script next time I publish.

Anyway, tapestry-testng, tapestry-flash and tapestry-spring are release candidates. tapestry-prop may have a few features (already in Tapestry 5) added to it before its final.

Thursday, November 09, 2006

Switched!

I've joined the legions of other No Fluff Just Stuff speakers, excluding (of course), Ted Neward, who are running on Mac. A MacBook Pro (the latest version) with 2 gig of ram, and a 30" cinema display.

Of course, the user experience is fantastic, especially with the 30" display. I don't even own a TV that large. It's so big that I sometimes have to pull windows with small text down to eye level for easy reading. Scary big. Ridiculously big. Love it.

In one respect, it's like coming back home; my first real windowing environment (the first I developed applications for) was NeXTSTEP back in the mid 90's. I developed a big database driven GUI app using AppKit and Enterprise Objects Framework. I remember an Apple rep coming by to show us some weird thing called WebObjects, but that's a different story.

It's intimidating ... there's so much new stuff to learn. Hanging around with Stu, Justin, Neal and Dave (among many others) who are so big on mastering your environment and working smart ... well, between Automator, Quicksilver, and everything else ... well, there's a lot to learn, a lot of catching up to do. And of course, Ruby is pre-installed as well.

Dashboard widgets look cool; as I'm trying to flex some JavaScript muscle, that's a definite way to build something useful as well.

So far, the only thing I don't like is the Mighty Mouse; I already ditched mine on Ebay and am using the same Logitech LX7 I use on my Aurora (windows desktop). I also had to pick up a keyboard, since the monitor is so huge I can't get the laptop (and it's keyboard) anywhere near it.

Also, OS X doesn't give you the choice about what to do with your laptop monitor when plugged into an external display. I'd just as soon shut it off but no ... it has to be a desktop extension. Once I get a little more settled in, I'll need to pick up one of those "Hacks" books.

So, exciting and scary. I have so many things to learn.

Thursday, November 02, 2006

Improve Tapestry performance with tapestry-prop

I've just deployed a new version of the tapestry-prop library for Tapestry 4.0. tapestry-prop adds a new binding prefix, "prop:", as an alternative to "ognl:". The actual moving of property values is accomplished using runtime generated bytecode, which performs better than OGNL's use of reflection.

The major new feature is support for property paths. You may now uses a series of property names, i.e., user.name. This vastly extends the usefulness of the "prop:" binding, since is can be used in about 90% of the places you'd normally use "ognl:".

The lack of reflection means that "prop:" peforms almost exactly the same as pure Java code. Based on some cursory performance testing, we're looking at a 27x improvement. In a typical Tapestry page, I suspect this may add up to a millisecond or so per request.

The version number has been changed to 1.0.0-snapshot. tapestry-prop should work with JDK 1.3 (Tapestry's minimum requirement).

If you were paying attention during the screencast, you'll note that an even more powerful version of this code is the default binding prefix in Tapestry 5.

Wednesday, November 01, 2006

Tapestry -- Duke's Choice Photos


Duke's Choice Lineup, originally uploaded by Tapestry Dude.

Finally got around to uploading some shots taken at JavaOne 2006. Here's a slideshow from the Duke's Choice award ceremony.