» tagged pages
» logout
HiveMind
Return to HiveMind

Tapestry and HiveMind

(or Cancel)

(Editing anonymously: to be credited for your changes, login or register a new account)

other page actions:

Tags Applied to this Topic

1 person has tagged this page:

HiveMind Wiki Pages

Ongoing thoughts about all things Jakarta Tapestry and Jakarta HiveMind, by Howard Lewis Ship, the creator of both frameworks.

Sunday, September 21, 2008

Tapestry 5.0.15 released

Tapestry version 5.0.15 has been released. The good news is that this is the final beta. The bad news is that it was supposed to be the release candidate. After the release was created, and during the voting period, a couple of annoyances (mostly for non-English applications) were identified. Rather than postpone the release further, it was decided to continue with the release and have one more release as the release candidate (and likely final release).

Anyway, outside of these minor issues, this release is ready for prime time, with some significant improvements to client-side behavior and Ajax support, more localizations, improvements to the IoC container, and the solution to a significant deadlocking problem.

Meanwhile, I'm in London now after a very successful talk on Tapestry at JavaZone in Oslo. My goal for those sessions is shock and awe with a dash of humor, and I'm told I delivered, to nearly 100 attendees. Suzy was in the audience, as was Eitan Suez, James Ward and Scott Davis.

This week is Tapestry 5 training in London, and next week Haarlem (in Amsterdam). I get back to the US in October, and that's when we'll get the final Tapestry 5.0 release out the door ... and I'm already working on 5.1 in a branch.

Thursday, September 18, 2008

AppFuse for Tapestry 5

Serge Eby has created a new version of AppFuse for Tapestry 5.

Sunday, September 14, 2008

Vote for Tapestry 5.0.15 (Release Candidate) is under way

The vote to release Tapestry version 5.0.15 is under way. This is the release candidate. Once it's available, we'll give it a few weeks of exposure (while I'm in Europe) and then, barring any unforeseen critical bugs, we will vote it up as the 5.0 GA release.

I already have extensive plans for improvements to Tapestry in 5.1. I want one of the compelling reasons to use Tapestry to be performance, so I'm looking at automatically GZIPing content, compressing JavaScript (and perhaps combining JavaScript files together), making increased use of far-future expired headers and so forth. In other words, just make the framework do the right thing.

I think there's also room to optimize the server-side further. I have some ideas for limiting the number of render commands needed to render a page, and limit the amount of work wasted on event notifications that have no listener.

Saturday, September 13, 2008

Boost your Productivity with Tapestry

Just found a short article by Moataz Anany, detailing his search for a good Java web framework ... and how he's taken to Tapestry. He identifies some of the strengths and a few of the weaknesses of Tapestry.

Saturday, September 13, 2008

A Succinct Definition of Science Fiction

Just a bit bored, redoing the Tapestry 5.0.15 build. Surfing the web during the build, and found Robert Heinlein's all purpose FAQ.

Science Fiction: Stories that would cease to exist if elements involving science or technology were omitted.

This puts Star Trek and Star Wars where they both belong: in fantasy. You could "swap out" phasers for guns, light sabers for magic swords, aliens for demons, planets for foreign cities or countries, and space ships for traditional transportation and not really change the Star Wars story, or most Star Trek episodes, at all. I tend to call this "Sci Fantasy"; most older Space Opera falls into this space as well.

Cryptonomicon and the Baroque Cycle are very firmly in SF however, since both stories are driven by science and technology and even the philosophy of science and technology (and the personalities behind those who invent science and technology).

My thought here is that the movies and television, which nominally have a large bandwidth of information, are actually too narrow to portray science fiction properly. Novels and even short stories, because they can more naturally go inside people's heads and reveal their thoughts, are much more adept at capturing what makes Science Fiction about science and not fantasy. But, of course, it's always on a spectrum ... virtually any story that places actual humans on distant planets beyond the solar system is implausible to a point that borders on fantasy, even if faster-than-light travel is avoided.

Thursday, September 11, 2008

Tapestry 5 IoC: Introducing Service Configurations

In the previous article, I discussed the basics of Tapestry 5 IoC. I focused on the terseness of Tapestry's container, even though everything occurs in Java code. I alluded to special features of Tapestry 5 IoC, service configurations. Let's start investigating those.

In traditional dependency injection, the relationship between a service and its dependencies is many-to-one: many services may inject a specific dependency. Whether that dependency is selected just by service type, or by service id, or by some other mechanism, it's still one single object.

Service configurations are somewhat inverted: they are a relationship from one service to many objects. The objects, or contributions, may be simple objects or may themselves be services.

Let's use a specific example from Tapestry to put this into perspective. Previously I showed the service builder method for the TranslatorSource service:

public static TranslatorSource buildTranslatorSource(ComponentInstantiatorSource componentInstantiatorSource, 
  ServiceResources resources)
{
    TranslatorSourceImpl service = resources.autobuild(TranslatorSourceImpl.class);
componentInstantiatorSource.addInvalidationListener(service);
return service; }

Let's dive a little deeper and look at what this service does. It's a source for Translator objects, which are an integral part of Tapestry's HTML form support. Translators are responsible for converting between server-side values (such as numbers, dates, and so forth) and client-side strings. They also play a role in client-side validation of user input.

Tapestry matches up properties that are edited by TextFields with corresponding Translator instances. This all happens inside the TextField component and is largely invisible to programmers. In any case, the TranslatorSource service is central:

public interface TranslatorSource
{
    /**
     * Returns the translator with the given logical name.
     *
     * @param name name of translator (as configured)
     * @return the shared translator instance
     * @throws RuntimeException if no translator is configured for the provided name
     */
    Translator get(String name);
/** * Finds a {@link Translator} that is appropriate to the given type, which is usually obtained via {@link * org.apache.tapestry5.Binding#getBindingType()}. Performs an inheritanced-based search for the best match. * * @param valueType the type of value for which a default translator is needed * @return the matching translator, or null if no match can be found */ Translator findByType(Class valueType);
/** * Finds a {@link Translator} that is appropriate to the given type, which is usually obtained via {@link * org.apache.tapestry5.Binding#getBindingType()}. Performs an inheritanced-based search for the best match. * * @param valueType the type of value for which a default translator is needed * @return the matching translator * @throws IllegalArgumentException if no known validator matches the provided type */ Translator getByType(Class valueType); }

Here's where it gets interesting

So, what Translators are built into Tapestry? You might think you could tell by looking at the implementation of the service:

public class TranslatorSourceImpl implements TranslatorSource, InvalidationListener
{
    private final Map<String, Translator> translators = CollectionFactory.newCaseInsensitiveMap();
private final StrategyRegistry<Translator> registry;
public TranslatorSourceImpl(Collection<Translator> configuration) { Map<Class, Translator> typeToTranslator = CollectionFactory.newMap();
for (Translator t : configuration) { translators.put(t.getName(), t); typeToTranslator.put(t.getType(), t); }
registry = StrategyRegistry.newInstance(Translator.class, typeToTranslator, true); }
public Translator get(String name) {
Translator result = translators.get(name);
if (result == null) throw new RuntimeException(ServicesMessages.unknownTranslatorType(name, InternalUtils .sortedKeys(translators)));
return result; }
public Translator getByType(Class valueType) { Translator result = registry.get(valueType);
if (result == null) { List<String> names = CollectionFactory.newList();
for (Class type : registry.getTypes()) { names.add(type.getName()); }
throw new IllegalArgumentException(ServicesMessages.noTranslatorForType(valueType, names)); }
return result; }
public Translator findByType(Class valueType) { return registry.get(valueType); }
public void objectWasInvalidated() { registry.clearCache(); } }

But you don't see any pre-defined Translator instances here ... just Collection<Translator> configuration passed to the constructor. Each Translator provides its name, and those all go into the translators map ... but the question is, where do they come from?

Jumping back to TapestryModule, we see a likely method:

public static void contributeTranslatorSource(Configuration<Translator> configuration)
{
    configuration.add(new StringTranslator());
    configuration.add(new ByteTranslator());
    configuration.add(new IntegerTranslator());
    configuration.add(new LongTranslator());
    configuration.add(new FloatTranslator());
    configuration.add(new DoubleTranslator());
    configuration.add(new ShortTranslator());
}

It's looking pretty likely that Tapestry supports string, byte, integer, long, float, double and short out of the box. The naming of this method is another example of convention over configuration. The prefix this time is contribute and the rest of the method name matches the service id, TranslatorSource.

The Configuration object has a single method, add():

public interface Configuration<T>
{
    /**
     * Adds an object to the service's contribution.
     *
     * @param object to add to the service's configuration
     */
    void add(T object);
}

So, Tapestry has invoked the contributeTranslatorSource() method, collected up the objects, the Translators, added to the configuration object, and converted the configuration object to a Collection, which is ultimately passed to the TranslatorSourceImpl constructor.

Seems awfully complicated, doesn't it? Well, it is nice (from a testing perspective) that TranslatorSourceImpl isn't tied to any particular implementations of Translator. But that's not the real benefit.

The real benefit, and this is the basis of the entire concept, is that you are not locked into just these Translators. You can define your own, and mix them in with the ones supplied by Tapestry. And you don't have to hack TapestryModule or TranslatorSourceImpl to do it.

Say your application defines a Currency class, to track currency amounts of orders and payments. That might be handy to use instead of double, for accuracy reasons. You might also want to parse and format currency values differently than naked doubles ... for example, to require exactly two digits of precision, or to ignore a leading dollar sign.

To mix in your own Translators, all you need to do is define your own module:

public class AppModule
{
  public static void contributeTranslatorSource(Configuration<Translator> configuration)
  {
    configuration.add(new CurrencyTranslator());
  }
}

This translator, CurrencyTranslator, will be indistinguishable from the default set of Translators provided by Tapestry; TranslatorSourceImpl will have no way of telling which Translators came from where. Your contributions are on an even footing with those provided by Tapestry itself.

You might ask in what order are these contribute methods are invoked? The answer is: Who knows? That's why the configuration is passed to the service implementation as (unordered) Collection, not (ordered) List. As we'll see in the next article, Tapestry has alternatives for when you care about ordering, or when you want your configuration in the form of a Map.

When are these methods called? They are called when the TranslatorSource service is realized, which happens when a method of the service is first invoked. Tapestry IoC is by default very lazy, it doesn't instantiate services until necessary. The service's proxy is responsible for this realization process, and its done in a thread-safe manner.

Dealing with this kind of loose binding in a structured manner is very helpful: it means that the TranslatorSource service is simplified: it doesn't need a method to add a new Translator, there's fewer issues related to multiple threads, and the available set of Translators never changes, which makes the behavior of the service much more predictable.

Conclusion

We've only just pierced the surface of Tapestry configurations, but we're beginning to see what I mean when talk about Tapestry's extensibility. Much of the key behavior of Tapestry is specified in terms of this kind of configuration, or one of its close relatives. And, as the example showed, building a service that uses a configuration is just a matter of defining a parameter of type Collection in the service's constructor.

In future articles, I'll discuss other variations of service configurations, and show how to go meta with Tapestry by leveraging configurations in combination with service-building services!

Monday, September 08, 2008

Closures coming for Objective-C

Apparent, closures are coming for C and Objective-C. Objective-C was my first OO programming language, and the transition to Java was a painful one. For a long time, I couldn't go back because Objective-C was not garbage collected (this has changed). Then I couldn't go back because it didn't have closures (or even closures' red headed step child: the inner class).

But Objective-C with garbage collection and true closures? That's getting somewhere, fast.

And no I'm not rewriting Tapestry in Objective-C. You know that question was coming!

Monday, September 08, 2008

IntelliJ: Maven improvements in 7.0.4

I haven't updated my project dependencies in a bit since upgrading from IntelliJ 7.0.3 to 7.0.4. I was pleasantly surprised after updating my POM dependencies that the Maven support has improved. In the past, the Maven support had a tendency to select less than ideal values for each module's source paths ... and it would overwrite my customizations every time I did a sync. I had come to dread changing dependencies because of this.

In 7.0.4 I had to refresh my Maven projects list; this took a few minutes. But lo and behold; it did a perfect job, not altering my personal changes. Bravo!

Sunday, September 07, 2008

Java's jconsole to the rescue

If you find yourself doing something tricky with threads, such as rooting out some insidious thread deadlocks, jconsole is invaluable. Having it analyze the threads to get the deadlocks is awesome, and the stack trace even includes identifiers about what object monitors have been locked. Way better than sorting through all that, by hand, from a text stack dump.

Sunday, September 07, 2008

Selenium tests just started hanging? Don't Panic!

So, here I am, in the middle of some intense debugging related to the dreaded combination of threads, class loaders and deadlocks and suddenly ... my integration tests no longer run!

Ultimately, my changes were very modest (a little bit of extra synchronization against the context class loader), so what gives?

Well, if your tests are like mine, and run against Firefox, your issue might be that Selenium is unable to start up Firefox if an upgrade has occurred, especially if the Firefox upgrade is not compatible with your plugins.

The solution? Start Firefox manually, to get through the dialogs it presents. Then rerun your tests.

Sunday, September 07, 2008

And the release candidate sprint is on!

Just rounding out the last few important bug fixes (and enhancements) that are needed for the Tapestry 5 release candidate. Can I get it together before I leave for my Europe trip on Sept. 15th? If not, the RC may wait until October.

Importantly, I think I've nailed TAPESTRY-2561, the thread deadlock bug.

I'm already looking forward to 5.1: I have lots of fully backwards compatible enhancements to make.

Sunday, September 07, 2008

Selenium tests just starting hanging? Don't Panic!

So, here I am, in the middle of some intense debugging related to the dreaded combination of threads, class loaders and deadlocks and suddenly ... my integration tests no longer run!

Ultimately, my changes were very modest (a little bit of extra synchronization against the context class loader), so what gives?

Well, if your tests are like mine, and run against Firefox, your issue might be that Selenium is unable to start up Firefox if an upgrade has occurred, especially if the Firefox upgrade is not compatible with your plugins.

The solution? Start Firefox manually, to get through the dialogs it presents. Then rerun your tests.

Sunday, September 07, 2008

jsconsole to the rescue

If you find yourself doing something tricky with threads, such as rooting out some insidious thread deadlocks, jconsole is invaluable. Having it analyze the threads to get the deadlocks is awesome, and the stack trace even includes identifiers about what object monitors have been locked. Way better than sorting through all that, by hand, from a text stack dump.

Friday, August 29, 2008

Id conflict inside Web Browser

Just spent many minutes on a wild goose chase and the underlying cause was that I had a <div> and a <textarea> that shared the same id. $(id) returned the <div>, most likely because it occured first in the document. I then got an error because <div> doesn't response to activate() ... that's a Prototype method attached to form elements.

I would be tempted to make Tapestry enforce this (that the id attribute for any node was unique), but that won't proof it against partial renders under Ajax, so I don't know that there's any point.

What's really going on is that CSS rules based on element id rather than element class are evil, and should only be used as a last resort. The free CSS template I got off the web uses too many explicit ids where it could be using CSS classes. You get what you pay for.

Thursday, August 28, 2008

Joshua Considers Tapestry 5

Josua Partogi has put together a nice quick intro to Tapestry 5. My only complaints are that the formatting makes the blog posting a bit hard to read, and that he included a lot of extra stuff in AppModule (put there by the Tapestry quickstart archetype) that's not required, such as the request timing filter. The archetype adds that as an example of what you can do. Finally, he uses the term "template" where all other documentation calls that type of component a "layout" (or, in Tapestry 4 terms, a "border").

Thursday, August 28, 2008

Stupid DateFormat

Working on some bugs related to the DateField component. Did you know that (for US English, anyway) DateFormat.getDateInstance(DateFormat.SHORT) provides back a DateFormat equivalent to "M/d/yy"?

That's a problem, because it has a tendency to format Dates to be two digit years. When it parses those same strings, it ends up considering the date to be in the first century AD.

Thus in this code:

  Date date1 = new Date(...);
  Date date2 = format.parse(format.format(date1));

date2 may or may not be the same as date1. Certainly any time information will be stripped out (this is expected), but often date2 will be in the wrong century.

I'm working right now to get the correct localized DateFormat, but ensure that the year portion is four digits, not two.

Thursday, August 28, 2008

Tapestry @ JavaZone 2008

I'll be presenting on Tapestry 5 at JavaZone. I'll be in Lab 2, on Sept 18th, at 2:15. With less than an hour to talk about Tapestry 5 and compare it briefly to Wicket and Rails ... well, I'll be talking fast!

I also just noticed that there's a session on comparing IoC containers that includes Tapestry 5 IoC. That's Lab 6, on the 17th, at 5pm.

Thursday, August 28, 2008

Ajax and Selenium: waitForCondition()

Selenium is a very useful tool but it can be very, very obtuse.

One challenge is dealing with Ajax; you might click on a button, but without a full page refresh, it's hard to know when to look for expected changes via Ajax and DHTML.

In the past, my test suites had short sleeps, a few hundred milliseconds. This makes them fail sporadically ... every once and a while on my MacBook Pro I'm doing so much other stuff while the tests run that the timing goes screwy.

You're then left with a difficult choice: sleep too short and the tests may fail. Sleep too long and your tests will always be slow.

Fortunately, there's a third option: Selenium's waitForCondition call. Of course, their documentation is worthless.

What it is supposed to do is evaluate a JavaScript snippet repeatedly, until the snippet returns true. However, it's tricky to get right. Like much in JavaScript, it's about context.

In my case, I wanted to wait for a client-side popup <div> to appear:

        type("amount", "abc");
        type("quantity", "abc");
click(SUBMIT);
waitForCondition("document.getElementById('amount:errorpopup')", "5000");
assertText("//div[@id='amount:errorpopup']/span", "You must provide a numeric value for Amount."); assertText("//div[@id='quantity:errorpopup']/span", "Provide quantity as a number.");

JavaScript treats null as false, and getElementById() returns null if an element with the id does not exist.

I'm making the assumption that once one of two <div> elements appears, they both will. I then use some XPath to get the text inside the <span> inside each <div>, to make sure the correct message was displayed to the user.

But this code doesn't work.

The problem is that document isn't what you'd expect; I'm guessing that it's some other frame inside the browser (Selenium's UI and code executes in one frame, which runs the actual application inside the second frame).

The solution took some research and the sacrifice of a few small furry animals to obtain:

        waitForCondition("selenium.browserbot.getCurrentWindow().document.getElementById('amount:errorpopup')", "5000");

That works, and it works much faster than adding a Thread.sleep() in the middle of my code.

Thursday, August 28, 2008

Tapestry 5 IoC: Binding and Building Services

Tapestry 5 includes its own internal Inversion of Control container. This is often a point of contention ... why not just use Spring or (in more recent conversations) Guice?

That's a complex question; simply put, Tapestry has requirements as a framework that the other containers don't offer solutions to.

This posting is a simple introduction to the basics of Tapestry 5 IoC. In later postings, we'll get into more detail about the advanced features of Tapestry's IoC container, the ones that really distance it from Spring and Guice.

Tapestry uses the term "service" for the primary objects that it manages for you. Spring uses the term "bean". A service is normally an interface and a class that implements the interface. In the most typical case, only a single service implements the interface, but T5 IoC is fully capable of handling the case where one service interface has a number of distinct services; even the case where a single class is instantiated with a different configuration.

Every service has a unique id string. In most cases, this is just the simple name of the service interface. When the same interface is used by multiple services, you will have to identify the service id explicitly.

To keep things real, I'll use actual, though abbreviated, examples from Tapestry's code base.

T5 IoC uses module classes to identify what services are available. A module class is a POJO class with a special method on it, a method named bind(). A Tapestry application will consist of a number of modules: some modules provided by Tapestry itself, some by third party libraries or extensions, and some by the application itself. Tapestry mixes and matches all of this information, all of the services defined by each of the modules, into a single registry of services.

That may sound more complex than it really is. The reality is that in the bind() method, we simply match service interfaces to corresponding implementations:

public final class TapestryModule
{
    public static void bind(ServiceBinder binder)
    {
        binder.bind(ClasspathAssetAliasManager.class, ClasspathAssetAliasManagerImpl.class);
        binder.bind(PersistentLocale.class, PersistentLocaleImpl.class);
        binder.bind(ApplicationStateManager.class, ApplicationStateManagerImpl.class);
        // ... and so on
    }
}

The ServiceBinder is uses generics to ensure that the class you specify implements the service interface. The API is a fluent interface: you can chain a few extra method calls onto bind() to override defaults, for example:

      binder.bind(ObjectProvider.class, AssetObjectProvider.class).withId("AssetObjectProvider");

TapestryModule actually defines quite a few additional services.

Let's look at an example:

public interface PersistentLocale
{
    void set(Locale locale);
Locale get();
boolean isSet(); }

I've stripped out the comments to save space ... but this service manages the user's locale; it's a key part of Tapestry 5's localization support. The implementation we'll see shortly works using HTTP Cookies, but that isn't important to the code that uses PersistentLocale.

public class PersistentLocaleImpl implements PersistentLocale
{
    private static final String LOCALE_COOKIE_NAME = "org.apache.tapestry5.locale";
private final Cookies cookieSource;
public PersistentLocaleImpl(Cookies cookieSource) { this.cookieSource = cookieSource; }
public void set(Locale locale) { cookieSource.writeCookieValue(LOCALE_COOKIE_NAME, locale.toString()); }
public Locale get() { String localeCookieValue = getCookieValue();
return localeCookieValue != null ? LocaleUtils.toLocale(localeCookieValue) : null; }
private String getCookieValue() { return cookieSource.readCookieValue(LOCALE_COOKIE_NAME); }
public boolean isSet() { return getCookieValue() != null; } }

T5 IoC does all injection through the constructor. This is to encourage you to write your dependencies into final fields, which is thread safe. Typically, your services will be immutable objects: all fields final.

PersistentLocaleImpl has a dependency on another service, Cookies. And what is Cookies? It's another service interface. Notice that we don't have to do any extra configuration here ... since there's one, and only one, service that implements the Cookies interface, that's all the information Tapestry needs to wire things together.

Other service implementations inside Tapestry have as few as zero dependencies, and as many as eight. There's no theoretical limit, it's just that having more than a few dependencies is a design smell ... that you can break things into smaller pieces.

One of the hallmarks of coding using an IoC container is this level of terseness, also knows as passing the buck. Given that PersistentLocaleImpl is concerned with HTTP cookies, you'd think that it would, somehow, get ahold of the HttpServletRequest object and start invoking getCookies() and addCookie() on it ... but instead, all the details of interfacing with the Servlet API and the rather awkward API for HTTP cookies is swept into a corner, inside the Cookies service implementation.

That's great ... it makes the implementation of PersistentLocaleImpl (as well as any other code that happens to care about HTTP cookies) that much simpler and easier to test.

Service Lifecycle

Tapestry services have a specific lifecycle:

defined
Identified via the ServiceBinder, but not yet referenced
virtual
A proxy exists that has been injected as a dependency of some other service, but no methods of the proxy have been invoked
realized
The service has been instantiated with dependencies

The beauty of this is that your code is completely unaware of this; all the work inside Tapestry ... creating proxies, realizing service implementations, occurs in a lazy but thread-safe manner. It's as if all the services are instantiated at startup without taking the time to actually do that work.

Again, the appeal of an IoC container is that you get to break your application into tiny, easily tested bits, and the IoC container is responsible for connecting everything back together at runtime. It really leads to a new way of coding, and thinking about coding.

Service Builder Methods

Sometimes just instantiating a class is not enough; there may be additional configuration needed as part of instantiating the class. Tapestry 5 IoC's predecessor, HiveMind, accomplished such goals with complex service-building services. It ended up being a lot of XML.

T5 IoC accomplishes the same, and more, using service builder methods; module methods that construct a service. A typical case is when a service implementation needs to listen to events from some other service:

public static TranslatorSource buildTranslatorSource(ComponentInstantiatorSource componentInstantiatorSource, 
  ServiceResources resources)
{
    TranslatorSourceImpl service = resources.autobuild(TranslatorSourceImpl.class);
componentInstantiatorSource.addInvalidationListener(service);
return service; }

Module methods prefixed with "build" are service builder methods. The service interface is defined from the return value (TranslatorSource). The service id is explicitly "TranslatorSource" (that is, everything after "build" in the method name).

Here, Tapestry has injected into the service builder method. ComponentInstantiatorSource is a service that fires events. ServiceResources is something else: it is a bundle of resources related to the service being constructed ... including the ability to instantiate an object including dependencies. What's great here is that buildTranslatorSource() doesn't need to know what the dependencies of TranslatorSourceImpl are, it can instantiate the class with dependencies using the autobuild() method. The service builder then adds the new service as a listener of the ComponentInstantiatorSource, before returning it.

This is a great separation of concerns: we have a construction concern (being an event listener) that's distinct from the operational concerns of TranslatorSource. And they are kept separate.

Conclusion

Tapestry IoC has simple and concise API for defining services and, in most cases, handles dependencies automatically. The end result is that it becomes child's play to divide-and-conquer: convert old, monolithic, hard to maintain code into small, easily tested, easily understood services.

In future postings, I'll go into more detail about the more advanced features of Tapestry: service scopes, service configurations and service decorations.

Thursday, August 28, 2008

Tapestry Europe Tour

I'm going to be in Oslo, Norway for JavaZone from Sep. 16 to Sep. 19th. I'm presenting on Thursday afternoon.

Next up, a week of Tapestry training in London.

I then fly to Amsterdam on Sep. 27th, for three more days of Tapestry training and then some vacation.

Next, down to Paris just for vacation before returning home on October 10th.

Should be a lot of fun, and a chance to meet with new and future Tapestry users. I can't wait!

Page 1 | Next >>
Username:
Password:
(or Cancel)