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, 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.