Sven Homburg is hosting a Tapestry 5 components site on Google code. Interestingly, it requires Tapestry 5.0.6 which is only available today. They've been riding the bleeding edge, they have!
The inside scoop on what's happening with Tapestry ... from the creator of the Apache Tapestry framework. Plus all the normal, random thoughts on coding and technology.
Tapestry Training -- From The Source
Thursday, October 25, 2007
Big improvement to quickstart archetype
I've figured out the necessary magic incantation that allows you to use an archetype without having to specify the archetype version number. Now anybody with Maven 2.0.7 installed can be up and running a Tapestry application in just a few seconds.
To test it out, I just removed my ~/.m2/repository/org/apache/tapestry folder and used the archetype to create a new Tapestry web application. The listing below gets clipped ... the command is mvn archetype:create -DarchetypeGroupId=org.apache.tapestry -DarchetypeArtifactId=quickstart -DgroupId=org.example -DartifactId=myapp -DpackageName=org.example.myapp -Dversion=1.0.0-SNAPSHOT
$ mvn archetype:create -DarchetypeGroupId=org.apache.tapestry -DarchetypeArtifactId=quickstart -DgroupId=org.example -DartifactId=myapp -DpackageName=org.example.myapp -Dversion=1.0.0-SNAPSHOT
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'archetype'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO] task-segment: [archetype:create] (aggregator-style)
[INFO] ----------------------------------------------------------------------------
[INFO] Setting property: classpath.resource.loader.class => 'org.codehaus.plexus.velocity.ContextClassLoaderResourceLoader'.
[INFO] Setting property: velocimacro.messages.on => 'false'.
[INFO] Setting property: resource.loader => 'classpath'.
[INFO] Setting property: resource.manager.logwhenfound => 'false'.
[INFO] **************************************************************
[INFO] Starting Jakarta Velocity v1.4
[INFO] RuntimeInstance initializing.
[INFO] Default Properties File: org/apache/velocity/runtime/defaults/velocity.properties
[INFO] Default ResourceManager initializing. (class org.apache.velocity.runtime.resource.ResourceManagerImpl)
[INFO] Resource Loader Instantiated: org.codehaus.plexus.velocity.ContextClassLoaderResourceLoader
[INFO] ClasspathResourceLoader : initialization starting.
[INFO] ClasspathResourceLoader : initialization complete.
[INFO] ResourceCache : initialized. (class org.apache.velocity.runtime.resource.ResourceCacheImpl)
[INFO] Default ResourceManager initialization complete.
[INFO] Loaded System Directive: org.apache.velocity.runtime.directive.Literal
[INFO] Loaded System Directive: org.apache.velocity.runtime.directive.Macro
[INFO] Loaded System Directive: org.apache.velocity.runtime.directive.Parse
[INFO] Loaded System Directive: org.apache.velocity.runtime.directive.Include
[INFO] Loaded System Directive: org.apache.velocity.runtime.directive.Foreach
[INFO] Created: 20 parsers.
[INFO] Velocimacro : initialization starting.
[INFO] Velocimacro : adding VMs from VM library template : VM_global_library.vm
[ERROR] ResourceManager : unable to find resource 'VM_global_library.vm' in any resource loader.
[INFO] Velocimacro : error using VM library template VM_global_library.vm : org.apache.velocity.exception.ResourceNotFoundException: Unable to find resource 'VM_global_library.vm'
[INFO] Velocimacro : VM library template macro registration complete.
[INFO] Velocimacro : allowInline = true : VMs can be defined inline in templates
[INFO] Velocimacro : allowInlineToOverride = false : VMs defined inline may NOT replace previous VM definitions
[INFO] Velocimacro : allowInlineLocal = false : VMs defined inline will be global in scope if allowed.
[INFO] Velocimacro : initialization complete.
[INFO] Velocity successfully started.
[INFO] [archetype:create]
[INFO] artifact org.apache.tapestry:quickstart: checking for updates from central
Downloading: http://repo1.maven.org/maven2/org/apache/tapestry/quickstart/5.0.6/quickstart-5.0.6.jar
11K downloaded
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating Archetype: quickstart:RELEASE
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: org.example
[INFO] Parameter: packageName, Value: org.example.myapp
[INFO] Parameter: basedir, Value: /Users/Howard/work
[INFO] Parameter: package, Value: org.example.myapp
[INFO] Parameter: version, Value: 1.0.0-SNAPSHOT
[INFO] Parameter: artifactId, Value: myapp
[WARNING] org.apache.velocity.runtime.exception.ReferenceException: reference : template = archetype-resources/pom.xml [line 14,column 22] : ${tapestry-release-version} is not a valid reference.
[WARNING] org.apache.velocity.runtime.exception.ReferenceException: reference : template = archetype-resources/pom.xml [line 80,column 26] : ${tapestry-release-version} is not a valid reference.
[INFO] ********************* End of debug info from resources from generated POM ***********************
[WARNING] org.apache.velocity.runtime.exception.ReferenceException: reference : template = archetype-resources/src/main/webapp/Start.tml [line 11,column 34] : ${currentTime} is not a valid reference.
[INFO] Archetype created in dir: /Users/Howard/work/myapp
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2 seconds
[INFO] Finished at: Thu Oct 25 07:47:04 PDT 2007
[INFO] Final Memory: 5M/9M
[INFO] ------------------------------------------------------------------------
~/work
$ cd myapp
~/work/myapp
$ mvn jetty:run
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'jetty'.
[INFO] org.apache.maven.plugins: checking for updates from tapestry-snapshots
[INFO] org.codehaus.mojo: checking for updates from tapestry-snapshots
[INFO] artifact org.apache.maven.plugins:maven-compiler-plugin: checking for updates from tapestry-snapshots
[INFO] artifact org.mortbay.jetty:maven-jetty-plugin: checking for updates from tapestry-snapshots
[INFO] artifact org.apache.maven.plugins:maven-war-plugin: checking for updates from tapestry-snapshots
[INFO] ----------------------------------------------------------------------------
[INFO] Building myapp Tapestry 5 Application
[INFO] task-segment: [jetty:run]
[INFO] ----------------------------------------------------------------------------
[INFO] Preparing jetty:run
[INFO] artifact org.apache.maven.plugins:maven-resources-plugin: checking for updates from tapestry-snapshots
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
Downloading: http://tapestry.formos.com/maven-snapshot-repository//org/apache/tapestry/tapestry-core/5.0.6/tapestry-core-5.0.6.pom
Downloading: http://snapshots.repository.codehaus.org/org/apache/tapestry/tapestry-core/5.0.6/tapestry-core-5.0.6.pom
Downloading: http://maven.openqa.org//org/apache/tapestry/tapestry-core/5.0.6/tapestry-core-5.0.6.pom
Downloading: http://repo1.maven.org/maven2/org/apache/tapestry/tapestry-core/5.0.6/tapestry-core-5.0.6.pom
3K downloaded
Downloading: http://tapestry.formos.com/maven-snapshot-repository//org/apache/tapestry/tapestry-project/5.0.6/tapestry-project-5.0.6.pom
Downloading: http://snapshots.repository.codehaus.org/org/apache/tapestry/tapestry-project/5.0.6/tapestry-project-5.0.6.pom
Downloading: http://maven.openqa.org//org/apache/tapestry/tapestry-project/5.0.6/tapestry-project-5.0.6.pom
Downloading: http://repo1.maven.org/maven2/org/apache/tapestry/tapestry-project/5.0.6/tapestry-project-5.0.6.pom
12K downloaded
Downloading: http://tapestry.formos.com/maven-snapshot-repository//org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.pom
Downloading: http://snapshots.repository.codehaus.org/org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.pom
Downloading: http://maven.openqa.org//org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.pom
Downloading: http://repo1.maven.org/maven2/org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.pom
3K downloaded
Downloading: http://tapestry.formos.com/maven-snapshot-repository//org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.pom
Downloading: http://snapshots.repository.codehaus.org/org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.pom
Downloading: http://maven.openqa.org//org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.pom
Downloading: http://repo1.maven.org/maven2/org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.pom
1K downloaded
Downloading: http://tapestry.formos.com/maven-snapshot-repository//org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.jar
Downloading: http://snapshots.repository.codehaus.org/org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.jar
Downloading: http://maven.openqa.org//org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.jar
Downloading: http://tapestry.formos.com/maven-repository/org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.jar
Downloading: http://tapestry.formos.com/maven-snapshot-repository/org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.jar
Downloading: http://repo1.maven.org/maven2/org/apache/tapestry/tapestry-ioc/5.0.6/tapestry-ioc-5.0.6.jar
267K downloaded
Downloading: http://tapestry.formos.com/maven-snapshot-repository//org/apache/tapestry/tapestry-core/5.0.6/tapestry-core-5.0.6.jar
Downloading: http://snapshots.repository.codehaus.org/org/apache/tapestry/tapestry-core/5.0.6/tapestry-core-5.0.6.jar
Downloading: http://maven.openqa.org//org/apache/tapestry/tapestry-core/5.0.6/tapestry-core-5.0.6.jar
Downloading: http://repo1.maven.org/maven2/org/apache/tapestry/tapestry-core/5.0.6/tapestry-core-5.0.6.jar
952K downloaded
Downloading: http://tapestry.formos.com/maven-snapshot-repository//org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.jar
Downloading: http://snapshots.repository.codehaus.org/org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.jar
Downloading: http://maven.openqa.org//org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.jar
Downloading: http://tapestry.formos.com/maven-repository/org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.jar
Downloading: http://tapestry.formos.com/maven-snapshot-repository/org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.jar
Downloading: http://repo1.maven.org/maven2/org/apache/tapestry/tapestry-annotations/5.0.6/tapestry-annotations-5.0.6.jar
4K downloaded
[INFO] [compiler:compile]
[INFO] Compiling 2 source files to /Users/Howard/work/myapp/target/classes
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [jetty:run]
[INFO] Configuring Jetty for project: myapp Tapestry 5 Application
[INFO] Webapp source directory = /Users/Howard/work/myapp/src/main/webapp
[INFO] web.xml file = /Users/Howard/work/myapp/src/main/webapp/WEB-INF/web.xml
[INFO] Classes = /Users/Howard/work/myapp/target/classes
2007-10-25 07:47:44.872::INFO: Logging to STDERR via org.mortbay.log.StdErrLog
[INFO] Context path = /myapp
[INFO] Tmp directory = /Users/Howard/work/myapp/target/work
[INFO] Web defaults = jetty default
[INFO] Web overrides = none
[INFO] Webapp directory = /Users/Howard/work/myapp/src/main/webapp
[INFO] Starting jetty 6.1H.5-beta ...
2007-10-25 07:47:44.011::INFO: jetty-6.1H.5-beta
2007-10-25 07:47:44.189::INFO: No Transaction manager found - if your webapp requires one, please configure one.
[INFO] TapestryFilter Startup time: 189 ms to build IoC Registry, 587 ms overall.
2007-10-25 07:47:45.282::INFO: Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server
127.0.0.1 - - [Thu, 25 Oct 2007 14:48:04 GMT] "GET / HTTP/1.1" 404 765 "-" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.8) Gecko/20071008 Firefox/2.0.0.8"
127.0.0.1 - - [Thu, 25 Oct 2007 14:48:07 GMT] "GET /myapp HTTP/1.1" 302 0 "http://localhost:8080/" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.8) Gecko/20071008 Firefox/2.0.0.8"
[INFO] TimingFilter Request time: 459 ms
127.0.0.1 - - [Thu, 25 Oct 2007 14:48:07 GMT] "GET /myapp/ HTTP/1.1" 200 519 "http://localhost:8080/" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.8) Gecko/20071008 Firefox/2.0.0.8"
[INFO] TimingFilter Request time: 3 ms
127.0.0.1 - - [Thu, 25 Oct 2007 14:48:08 GMT] "GET /myapp/assets/tapestry/default.css HTTP/1.1" 200 4916 "http://localhost:8080/myapp/" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.8) Gecko/20071008 Firefox/2.0.0.8"
[INFO] TimingFilter Request time: 2 ms
127.0.0.1 - - [Thu, 25 Oct 2007 14:48:12 GMT] "GET /myapp/start HTTP/1.1" 200 519 "http://localhost:8080/myapp/" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.8) Gecko/20071008 Firefox/2.0.0.8"
[INFO] TimingFilter Request time: 4 ms
127.0.0.1 - - [Thu, 25 Oct 2007 14:48:12 GMT] "GET /myapp/assets/tapestry/default.css HTTP/1.1" 304 0 "http://localhost:8080/myapp/start" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.8) Gecko/20071008 Firefox/2.0.0.8"
That's it! Jetty is running, and the application is available at http://localhost:8080/myapp. And of course, I have a Ruby script to make setup even easier.
Tapestry 5.0.6 is available
Yet another preview release, Tapestry 5.0.6 adds a raft of new features , including:
A new DateField component (using a client-side JavaScript calendar).
The Grid component can now be utilized inside a Form.
The BeanEditForm component has been refactored, allowing you to create complex forms with multiple BeanEditors.
There is now a BeanDisplay component, a counterpart to BeanEditor that displays the properties of a bean.
For early adopters upgrading from release 5.0.5, you should be
aware that Tapestry template files now have a
.tml
extension, and are stored in the context root, not under
WEB-INF
. In addition, the @Inject annotation in tapestry-core has
been removed, and the @Inject annotation from tapestry-ioc is
now doing double-duty.
In addition, Tapestry 5 now uses SLF4j as its pluggable logging library, replacing commons-logging. This may require that you upgrade to Log4j 1.2.14.
Sunday, October 14, 2007
Tapestry 5.0.6 soon
I'm very much hoping to have Tapestry 5.0.6 out soon. I've been squeezing in some more time to work on Tapestry 5 and just added a BeanDisplay component (to complement the BeanEditor component).
The Ajax story for Tapestry 5 is still up in the air ... the specter of writing an abstraction layer has come back. And I still want better, smarter handling of Hibernate ... it seems to me that the BeanEditor should be capable of handling to-one relationships in a smart way. And Spring WebFlow integration would be a snap if I just had a couple of days to work on it.
I'm personally anxious to get Tapestry 5 solid and shipped ... but not before it's ready.
Monday, October 01, 2007
TheDailyTube: Powered by Tapestry 5
A posting on the Tapestry mailing list led me to TheDailyTube, a video sharing site powered by ... Tapestry 5. It may be "alpha", but people are putting their faith in Tapestry 5.
The site looks sweet and is plenty responsive. Can't really tell too much else about the site though ... they've turned off the default exception page. Still, this is the kind of high volume, outward facing site that I'm proud to have helped facilitate. Viva Tapestry 5!
Friday, September 28, 2007
One of the greatest Worse-Than-Failures, ever ...
Serializalicious, now on WTF is just one example Steve, Ben and I discovered in this application. The article is somewhat misleading, it makes it look like Steve put that code in there. Nope, Steve, Ben and I were all victims of the missing Architect. Funny, scary stuff.
Wednesday, September 26, 2007
Let there be Nightly Docs!
With just a little bit of tweaking, we now have full, nightly documentation for Tapestry 5. This is great news, as the public web site can start staying static between releases (thus all the docs there will reflect just the latest stable release), and the snapshots and documentation on tapestry.formos.com will also be in sync, reflecting what's currently going on in SVN trunk.
That should ease a lot of confusion, when the docs say something exists but it doesn't in the stable (non-snapshot) versions.
Tuesday, September 25, 2007
Dr. Seuss visits the Tapestry Mailing List
Just noticed this posting by Bill Holloway:
Will you work with JSP? They mess up my MVC. I will not work with JSP. We'll put them in an IDE. I don't want them in an IDE. They screw up the MVC. I will not work with JSP. We'll put them down inside some struts. You'll never see them, In the guts. You cannot hide them in the guts! They're in your face when they're in struts! I don't want them on my IDE. They screw up our MVC! I will not work with JSP. We'll put them in some server faces. Many use them, many places. That way the MVC's ok. Will you work with them today? The XML of server faces Bogs down the coders in those places! You cannot hide them in the guts. I will not work with them in struts. Get them off my CRT! They're $@%#*&ing up my MVC! Give me back my Tapestry.
Tapestry 5 Nightly Builds
I've finally gotten around to setting up nightly builds for Tapestry 5. It just another build on the Bamboo server. You can get access to it by adding:
<repositories>
<repository>
<id>tapestry-snapshot</id>
<url>http://tapestry.formos.com/maven-snapshot-repository</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
to your POM.
Eventually, we'll be publishing documentation every night too, but that's waiting on a Maven bug.
Saturday, September 15, 2007
Mac OS X bundles vs. Subversion
If you work on Mac OS X, you may have noticed how cool Macs deal with complex documents, things like Keynote presentations or applications themselves. They're stored as directories. The Finder hides this, making them look and act like individual files. This works nicely, often the contents of a bundle are simple text and XML files ... whereas the equivalent under Windows is either a very proprietary (and potentially fragile) binary format, or multiple files and folders that YOU have to treat as a unit.
Alas, this all breaks down when using Subversion. You can't just check in MyPresentation.key into SVN ... it will create those pesky .svn directories inside the bundle, and those will be destroyed every time you save your presentation.
My solution to this is to convert the bundles into an archive, and check in the archive. The bundle folders are marked as svn:ignore. I guess this reveals that I mostly use SVN as a safe, structured backup.
In any case, manually creating those archives can be a pain. So ... out comes my solution to many problems: Ruby.
The goal here is to find bundles that need to be archived; do it efficiently (only update the archive if necessary) and do it recursively, seeking out bundles in sub-directories.
#!/usr/bin/ruby
# Used to prepare a directory for commit to Subversion. This is necessary for certain file types on Mac OS X because what appear to be files in the Finder
# are actually directories (Mac uses the term "bundle" for this concept). It is useless to put the .svn folder inside such a directory, because it will
# tend to be deleted whenever the "file" is saved.
#
# Instead, we want to compress the directory to a single archive file; the bundle will be marked as svn:ignore.
#
# We use tar with Bzip2 compression, which is resource intensive to create, but
# compresses much better than GZip or PKZip.
#
# The trick is that we only want to create the acrhive version when necessary; when
# the archive does not exist, or when any file
# in the bundle is newer than the archive.
require 'optparse'
# Set via command line options
$extensions = %w{pages key oo3 graffle}
$recursive = true
$dry_run = false
# Queue of folders to search (for bundles)
$queue = []
def matching_extension(name)
dotx = name.rindex('.')
return false unless dotx
ext = name[dotx + 1 .. -1]
return $extensions.include?(ext)
end
# Iterate over the directory, identify bundles that may need to be compressed and (if recursive) subdirectories
# to search.
#
# path: string path for a directory
def search_directory(dirpath)
Dir.foreach(dirpath) do |name|
# Skip hidden files and directories
next if name[0..0] == "."
path = File.join(dirpath, name)
next unless File.directory?(path)
if matching_extension(name)
update_archive path
next
end
if $recursive
$queue << path
end
end
end
def needs_update(bundle_path, archive_path)
return true unless File.exists?(archive_path)
archive_mtime = File.mtime(archive_path)
# The archive exists ... can we find a file inside the bundle thats newer?
# This won't catch deletions, but that's ok. Bundles tend to get completly
# overwritten when any tiny thing changes.
dirqueue = [bundle_path]
until dirqueue.empty?
dirpath = dirqueue.pop
Dir.foreach(dirpath) do |name|
path = File.join(dirpath, name)
if File.directory?(path)
dirqueue << path unless [".", ".."].include?(name)
next
end
# Is this file newer?
if File.mtime(path) > archive_mtime
return true
end
end
end
return false
end
def update_archive(path)
archive = path + ".tar.bz2"
return unless needs_update(path, archive)
if $dry_run
puts "Would create #{archive}"
return
end
puts "Creating #{archive}"
dir = File.dirname(path)
bundle = File.basename(path)
# Could probably fork and do it in a subshell
system "tar --create --file=#{archive} --bzip2 --directory=#{dir} #{bundle}"
end
$opts = OptionParser.new do |opts|
opts.banner = "Usage: prepsvn [options]"
opts.on("-d", "--dir DIR", "Add directory to search (if no directory specify, current directory is searched)") do |value|
$queue << value
end
opts.on("-e", "--extension EXTENSION", "Add another extension to match when searching for bundles to archive") do |value|
$extensions << value
end
opts.on("-N", "--non-recursive", "Do not search non-bundle sub directories for files to archive") do
$recursive = false
end
opts.on("-D", "--dry-run", "Identify what archives would be created") do
$dry_run = true
end
opts.on("-h", "--help", "Help for this command") do
puts opts
exit
end
end
def fail(message)
puts "Error: #{message}"
puts $opts
end
begin
$opts.parse!
rescue OptionParser::InvalidOption
fail $!
end
# If no --dir specified, use the current directory.
if $queue.empty?
$queue << Dir.getwd
end
until $queue.empty?
search_directory $queue.pop
end
I do love Ruby syntax, it is so minimal, and lets me follow my personal mantra less is more.
I'm sure there's some edge cases that aren't handle well, such as spaces in path names and maybe issues related to permissions. But it works for me.
You do need to have tar installed, in order to build the archives. I can't remember if that's built in to Mac OS X (probably) or whether I obtained it using Fink.
In any case, you need to remember to execute prepsvn in your workspace, to spot file bundles that need archiving, before you check in. It would be awesome if Subversion supported some client-side check-in hooks to do this automatically.
Saturday, September 08, 2007
Itch scratching: Even better feedback for all thumbs typists
I was reading through Matt's rundown of Struts 2 (does it suck?) and he strayed into one of my most passionate areas: feedback.
He gave an example of an incorrect property name, and how that would be reported to the user. He showed examples from all the major frameworks, and the Tapestry 4 version, even without its proper CSS styles, won hands-down.
However, as I was reading and responding, it struck me that while other framework can barely tell you what you've done wrong, Tapestry 5 should be telling you how to fix it. In this case, advising you about the possible properties you could have specified, which I've added as TAPESTRY-1737 and fixed.
Here's the result. Not bad for ten minutes work. And remember: property names in Tapestry 5 are already case insensitive, which wipes out probably 50% of your typical typing errors anyway.
Thursday, September 06, 2007
Biled!
Well, Hani has finally Biled the Bearded One. But what a cheap and meaningless shot ... he's upset about a switch from commons-logging to SLF4J? What is it with Hani and commons-logging? Did commons-logging steal his prom date? Did it kick sand in his face at the beach? Maybe it touched him inappropriately as a child. In any case, he's strangely obsessive about anything that touches on the subject.
On a serious note ... people have come out of the woodwork to defend the use of JDK logging, especially inside Websphere (God help them), which does justify a pluggable approach. Again, Log4J does not quite fit all, and because it is based on classes, not interfaces, it gets in the way of mock testing.
Friday, August 31, 2007
The Blindness of James Gosling: Java as A First Language
I think Java is an excellent all-around-language. It is truly ubiqitous, well specified, highly performant and well accepted by the industry.
But there's a big difference between those credentials, and the credentials of the first language a developer learns. Regardless, James Gosling feels obligated to recommend Java as the first language anyone learns.
Java as a first language? Please! Yes, Java is simpler than C++, C, Lisp, assembler and all the languages that seasoned veterans, such as James Gosling, are familiar with. But the fact that Java shields you from pointers, malloc() and zero-terminated strings does not make it a good first language!
Java is extremely monolithic: in order to understand how to run a simple Hello World program, you'll be exposed to:
- Classes
- Java packages
- Static vs. instance methods
- Source files vs. compiled classes
- Editing vs. Execution
- Using a compiler or an IDE
- Method return types and method parameter types
- The magic (for newbies) that is "System.out.println()"
... in fact, this list is far from exhaustive. A lot of that has fallen off our collective radar ... we're blind to it from seeing it so much over the last ten years or more.
What's important to understand is that people new to programming don't really have a way of understanding the difference between a document and a program, between source code and an application. Certainly the web (with HTML and JavaScript all mixed together) hasn't helped people understand the division intuitively. It's very hard for any of us to think like someone new to our entire world.
I'm rarely in a position to teach people how to program, but when I think about it, only two languages make sense to me: Ruby and Inform.
Ruby, because it is interactive yet fully object oriented. You can start with an interactive puts "Hello World" and quickly work up from there. You get to interact with the Ruby world as objects before you have to define your own objects. There's a nice clean slope from one-off quickies to eventual discipline as a true developer, without any huge leaps in the middle.
Inform, used to create interactive text adventures, is also intriguing. It's specifically designed for non-programmers, and has a laser-focused UI. It is interactive: you type a little bit about your world and hit Run to play it. Again, you experience an object oriented environment in an incredibly intuitive way long before you have to define to yourself, or to the language, what an object is.
Inform is truly some amazing software, given that advanced Inform games will make use of not just object oriented features, but also aspect oriented. Here's a little example I brewed up about a room built on top of a powerful magnet:
"Magnet Room" by Howard Lewis Ship A thing is either magnetic or non-ferrous. Things are normally non-ferrous. The Test Lab is a room. "A great circular space with lit by a vast array of ugly flourescent lights. In the center of the room is a white circular pedestal with a great red switch on top. Your feet echo against the cold steel floor." The red switch is a device in the test lab. It is scenery. The double-sided coin is in the test lab. "Your lucky double sided half dollar rests on the floor." The coin is magnetic. Understand "dollar" as the coin. A rabbits foot is in the test lab. "Your moth-eaten rabbits foot lies nearby." Magnet Active is a scene. Magnet Active begins when the red switch is switched on. When Magnet Active begins: say "A menacing hum begins to surge from beneath the floor." Magnet Active ends when the red switch is switched off. When Magnet Active ends: say "The menacing hum fades off to nothing." Instead of taking a thing that is magnetic during Magnet Active: say "[The noun] is firmly fixed to the floor."
That's not a description of my program; that's the actual program. It's literate; meaning equally readable, more or less, by the compiler and the author. It's still a formal language with all that wonderful lexical and BNF stuff, it's just a bit richer than a typical programming language.
What I like about Inform is that you get a complete little game from this:
Magnet Room An Interactive Fiction by Howard Lewis Ship Release 1 / Serial number 070831 / Inform 7 build 4W37 (I6/v6.31 lib 6/11N) SD Test Lab A great circular space with lit by a vast array of ugly flourescent lights. In the center of the room is a white circular pedestal with a great red switch on top. Your feet echo against the cold steel floor. Your lucky double sided half dollar rests on the floor. Your moth-eaten rabbits foot lies nearby. >take dollar Taken. >drop it Dropped. >turn switch on You switch the red switch on. A menacing hum begins to surge from beneath the floor. >take dollar The double-sided coin is firmly fixed to the floor. >take foot Taken. >turn switch off You switch the red switch off. The menacing hum fades off to nothing. >take dollar Taken. >
The way rules work, especially in the context of scenes, is very much an aspect oriented approach. It is a pattern-based way to apply similar rules to a variety of objects. I've seen this kind of thing with aspect oriented programming in Java, but also with pattern based languages such as Haskell and Erlang.
The point is, using Ruby or Inform, you can learn the practices of a programmer ... dividing a complex problem into small manageable pieces, without being faced with a huge amount of a-priori knowledge and experience in order to get started. Both Ruby and Inform are self-contained environments designed for quick and easy adoption. That's something Java missed the boat on long, long ago!
Wednesday, August 29, 2007
Handling direct URLs in Tapestry 5
So, I'm starting work on the 1% web portion of my otherwise 99% Swing project. It's about sending invoices by email and allowing external users to view invoice PDFs, and accept or reject them.
I wanted to get a tracer bullet going; the process is kicked off when some COBOL code, via typical Rube Golderberg shenanigans, GETs a URL of the form /sendinvoice/invoice-number/email-address.
Well, in Tapestry terms, that could be a page with an activate event handler method:
package com.myclient.pages;
import org.apache.tapestry.util.TextStreamResponse;
public class SendInvoice
{
Object onActivate(String invoiceNumber, String emailAddress)
{
// TODO: Generate and send the email
return new TextStreamResponse("text/plain",
String.format("OK %s / %s", invoiceNumber, emailAddress));
}
}
Returning a StreamResponse (that's the interface, TextStreamResponse is a simple implementation) allows your code to bypass the normal HTML response with one you control. This kind of thing was way too complicated to do in Tapestry 4 so I'm very happy with how minimal the Tapestry 5 approach is.
I fired this up and it worked the first time, echoing back to me the information I passed in the URL. If you don't provide at least two values in the URL, Tapestry will bypass this activate event handler method and will just render out the page; the page template simply announces to the user that they munged up the URL in some way.
This is something that would be pretty easy as a servlet, but is even easier in Tapestry. Just create the SendInvoice page and provide the onActivate() method. onActivate() gets called based on the number of path elements provided in the URL. When there are two (or more), this method gets invoked with the first two. Tapestry can automatically coerce from string to other types, but here both values happen to be strings anyway.
Obviously, much more to do (under deadline ... I don't really have time to blog this). I'm just happy to be doing a little Tapestry on my day job
Sunday, August 26, 2007
So long, commons-logging, hello SLF4J
I'm finally taking a bit to do something I've wanted to do for a long time: get away from commons-logging and switch over to SLF4J.
Commons-logging has a justified reputation for causing more problems than it solves. It's basic premise, that you should need to be able to switch between logging implementations, is very appealing to framework developers. Who am I to mandate that you use Log4J in your application? Sure, I mandate things like Javassist and a bunch of other dependencies, but for some reason, Log4J is too much. I'm not sure where this mindthink came from, but it is prevalent among framework folks.
Anyway, it's not a bad idea, though 99.99999% of applications use commons-logging as a wrapper around Log4J. I haven't yet met someone who uses JDK logging, or anything home brew. Why would you?
It's the implementation of commons-logging that the problem. First off, it called a "logger" a "log". I think that's just wrong, and (in fact) always preferred the term "category" (now deprecated by Log4J). To my mind, the category or logger is what you logged to, the log itself is the console, or a file, or something that receives the formatted output.
Also, commons-logging's ultra-late binding approach always causes class loader frustration and memory leaks. Every time they claim to get that fixed up, some new variation pops up.
The non-Apache follow on to commons-logging is SLF4J. It does things right: Logger, not Log. Better yet, Logger is an interface (it's a class in Log4J) which makes my EasyMock-addled brain quite happy. Finally, SLF4J takes a very simple, very sane approach: you combine the API JAR with one of several implementation JARs. That's it ... no class loader magic, no dynamic binding. This is an ideal solution for those same 99.99999% of applications out there.
This may cause some minor pain for Tapestry 5 early adopters, as they'll have to switch their (JCL) Log parameters to (SLF4J) Logger. Sorry, T5 is alpha ... but not for long, and this is a kind of last opportunity to make such a large change.
Update: Dion's been laughing from the sidelines but he's missing the point. He's put up a smokescreen that boils down to "just use System.out.println". But that's not what a logging framework is about ... it's about filtering, and organizing, a little bit about formatting, but mostly about being able to control what does log and what doesn't without hacking a lot of code (instead just tweaking a couple of control files).
Friday, August 24, 2007
Maven: Love to Hate or Hate to Love?
I've been struggling hard with Maven this week, and its really be bringing me down. I love the theory of Maven, but the execution is so awful I'm very close to eating crow and switching back to a Ant build, maybe using Ivy.
One of the things that concerns me with Maven is that for a tool whose goal is to be a "project comprehension tool" (giving quick access to Java source, Javadoc and other reports) it's almost impossible to find the source or get any insight into how Maven is built. Yet so much about Maven is impossible to find, with broken links, missing and out of date documentation, and other hazards running rampant.
I've been hunting around for 30+ minutes, trying to find the source to MavenCli. It's in the maven-core library. Good luck finding a link to that off the Maven web site. Through arcane means and Google searches I eventually stumbled across http://maven.apache.org/ref/ but 2.0.7 is not present there. So they rolled out a release without external documentation? I guess that's OK when you need a magical incantation to even find the documentation.
Well, I'm getting closer to my answers, but I'm probably still shiv'ed because Maven developer guidelines mandate zero documentation in the source. Or anywhere else. In fact, I believe the maven-fuckyou-plugin exists to strip documentation out of your source and seems to be enabled by default in all the Maven modules.
Well, eventually I answered my own question: -Dmaven.repo.local=directory allows me to control where the local repository exists. This is for my Bamboo server that needs to build multiple branches that share the same version numbers, so I don't want them to use the same repository.
Anyway, that's what it should be (from the tiny fraction of the code that's visible). But it blows up real good, apparently unwilling to download files into this repository, and spewing errors that make it appear as if artifacts were not present in the remote repository.
So, when Maven is useful it is very useful, but when it gets in your way, it totally blocks you.
Thursday, August 23, 2007
Quick and dirty Java application launcher
I've been busy working at a new company, for a new client, helping out on an existing product. The product is a Swing application and so I ran into my old nemesis: the ugly, buggy, hand maintained script to launch the application. You know the beast: start.bat and start.sh that you dread having to maintain.
We have a nifty, Maven based build, and a continuously evolving set of dependencies. Nobody wants to have to maintain that in two or three additional locations.
I've been seeing these kinds of scripts for years and hating them all along. Mostly, these scripts exist to find a bunch of JARs, put together a classpath (often by creating a monster CLASSPATH environment variable), then run Java to launch a particular class.
Previously, I've pushed to use Ant to do this. Ant is great a searching directories, building up classpaths, and invoking Java programs. It's pretty much inherently cross-platform. Who cares if it says "BUILD SUCCESSFUL" at the end?
Sun's tried to tackle this before, with the typical half-assed, lame results. Executable JAR files, Java Web Start, etc. Always stuff that looked good on some engineer's workstation, but never made sense in terms of a real development and deployment scenario. Ten years of Java later and it's still too much work to write a Java application to do a simple job. Meanwhile, Ruby and Groovy are so easy to run, it's painful (or laughable, or both).
JDK 1.6 has a new option to search a directory and pick up the JARs it finds. But we're targetting JDK 1.5 for both the client and the server. Thus my little launcher utility:
package org.example.launcher;
import java.io.File;
import java.io.FilenameFilter;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
/**
* A wrapper used to assemble the classpath before launching the actual
* application. The big trick is assembling the classpath before the launch.
* Yep, this is functionality that should be built right into Java.
*
* <p>
* This is packaged into a JAR with no dependencies.
*
*/
public final class LauncherMain {
private static final List<URL> _classpath = new ArrayList<URL>();
/**
* Usage:
*
* <pre>
* java -jar mblauncher.jar some.class.to.launch [--addclasspath dir] [--addjardir dir] [options]
* </pre>
*
* <p>
* The --addclasspath parameter is used to add a directory to add to the
* classpath. This is most commonly used with a directory containing
* configuration files that override corresponding files stored inside other
* JARs on the classpath.
*
* <p>
* The --addjardir parameter is used to define directories. JAR files
* directly within such directories will be added to the search path.
*
* <p>Any
* remaining options will be collected and passed to the main() method of
* the launch class.
*/
public static void main(String[] args) {
List<String> launchOptions = new ArrayList<String>();
if (args.length == 0)
fail("No class to launch was specified. This should be the first parameter.");
String launchClass = args[0];
int cursor = 1;
while (cursor < args.length) {
String arg = args[cursor];
if (arg.equals("--addclasspath")) {
if (cursor + 1 == args.length)
fail("--addclasspath argument was not followed by the name of the directory to add to the classpath.");
String dir = args[cursor + 1];
add(dir);
cursor += 2;
continue;
}
if (arg.equals("--addjardir")) {
if (cursor + 1 == args.length) {
fail("--addjardir argument was not followed by the name of a directory to search for JARs.");
}
String dir = args[cursor + 1];
search(dir);
cursor += 2;
continue;
}
launchOptions.add(arg);
cursor++;
}
String[] newArgs = launchOptions.toArray(new String[launchOptions
.size()]);
launch(launchClass, newArgs);
}
private static void launch(String launchClassName, String[] args) {
URL[] classpathURLs = _classpath.toArray(new URL[_classpath.size()]);
try {
URLClassLoader newLoader = new URLClassLoader(classpathURLs, Thread
.currentThread().getContextClassLoader());
Thread.currentThread().setContextClassLoader(newLoader);
Class launchClass = newLoader.loadClass(launchClassName);
Method main = launchClass.getMethod("main",
new Class[] { String[].class });
main.invoke(null, new Object[] { args });
} catch (ClassNotFoundException ex) {
fail(String.format("Class '%s' not found.", launchClassName));
} catch (NoSuchMethodException ex) {
fail(String.format("Class '%s' does not contain a main() method.",
launchClassName));
} catch (Exception ex) {
fail(String.format("Error invoking method main() of %s: %s",
launchClassName, ex.toString()));
}
}
private static void add(String directoryName) {
File dir = toDir(directoryName);
if (dir == null)
return;
addToClasspath(dir);
}
private static File toDir(String directoryName) {
File dir = new File(directoryName);
if (!dir.exists()) {
System.err.printf("Warning: directory '%s' does not exist.\n",
directoryName);
return null;
}
if (!dir.isDirectory()) {
System.err.printf("Warning: '%s' is a file, not a directory.\n",
directoryName);
return null;
}
return dir;
}
private static void search(String directoryName) {
File dir = toDir(directoryName);
if (dir == null)
return;
File[] jars = dir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
});
for (File jar : jars) {
addToClasspath(jar);
}
}
private static void addToClasspath(File jar) {
URL url = toURL(jar);
if (url != null)
_classpath.add(url);
}
private static URL toURL(File file) {
try {
return file.toURL();
} catch (MalformedURLException ex) {
System.err.printf("Error converting %s to a URL: %s\n", file, ex
.getMessage());
return null;
}
}
private static void fail(String message) {
System.err.println("Launcher failure: " + message);
System.exit(-1);
}
}
This gets packaged up as an executable JAR (i.e., with a MainClass manifest entry pointing to this class).
Launching ends up looking like.
java -jar launcher.jar --addjardir lib org.example.MyMain --my-main-optBasically, 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 $