A significant amount of what Tapestry does is meta programming: code
that modifies other code. Generally, we're talking about adding
behavior to component classes, which are transformed as they are loaded
into memory. The meta-programming is the code that sees all those
annotations on methods and fields, and rebuilds the classes so that
everything works at runtime.
Unlike AspectJ, Tapestry does all of its meta-programming at runtime.
This fits in better with live class reloading, and also allows for
loaded libraries to extend the meta-programming that's built-in to the
framework.
All the facilities Tapestry has evolved to handle meta-programming make
it easy to add new features. For example, I was doing some work with
the Heartbeat enviromental object. Heartbeat allows you to schedule
part of your behavior for "later". First off, why would you need this?
A simple example is the relationship between a Label component and a
form control component such as TextField. In your template, you may use
the two together:
<t:label for="email"/> <t:textfield t:id="email"/>

The for parameter there is not a simple string, it is a component id.
You can see that in the source for the Label component:
@Parameter(name = "for", required = true, allowNull = false,
defaultPrefix = BindingConstants.COMPONENT) private Field field;

Why does for="email" match agains the email component, and not some
property of the page named email? That's what the defaultPrefix
annotation attribute does: it says "pretend there's a component: prefix
on the binding unless the programmer supplies an explicit prefix."
So you'd think that would wrap it up, we just need to do the following
in the Label code:
writer.element("label", "for", field.getClientId());

Right? Just ask the field for its client-side id and now all is happy.
Alas, that won't work. The Label component renders before the
TextField, and the clientId property is not set until the TextField
renders. What we need to do is wait until they've both rendered, and
then fill in the for attribute after the fact.
That's where Heartbeat comes in. A Heartbeat represents a container
such as a Loop or a Form. A Heartbeat starts, and accumulates deferred
commands. When the Heartbeat ends, the deferred commands are executed.
Also, Heartbeats can nest.
Using the Heartbeat, we can wait until the end of the current heartbeat
after both the Label and the TextField have rendered and then get an
accurate view of the field's client-side id. Since Tapestry renders a
DOM (not a simple text stream) we can modify the Label's DOM Element
after the fact.
Without the meta-programming, it looks like this:
@Environmental private Heartbeat heartbeat; private Element
labelElement; boolean beginRender(MarkupWriter writer) { final Field
field = this.field; decorator.beforeLabel(field); labelElement =
writer.element("label"); resources.renderInformalParameters(writer);
Runnable command = new Runnable() { public void run() { String fieldId
= field.getClientId(); labelElement.forceAttributes("for",
fieldId, "id", fieldId + "-label"); decorator.insideLabel(field,
labelElement); } }; heartbeat.defer(command); return !ignoreBody; }

See, we've gotten the active Heartbeat instance for this request and we
provide a command, as a Runnable. We capture the label's Element in an
instance variable, and force the values of the for (and id) attributes.
Notice all the steps: inject the Heartbeat environmental, create the
Runnable, and pass it to defer().
So where does the meta-programming come in? Well, since Java doesn't
have closures, it has a pattern of using component methods for the same
function. Following that line of reasoning, we can replace the Runnable
instance with a method call that has special semantics, triggered by an
annotation:
private Element labelElement; boolean beginRender(MarkupWriter writer)
{ final Field field = this.field; decorator.beforeLabel(field);
labelElement = writer.element("label");
resources.renderInformalParameters(writer); updateAttributes();
return !ignoreBody; } @HeartbeatDeferred private void
updateAttributes() { String fieldId = field.getClientId();
labelElement.forceAttributes("for", fieldId, "id", fieldId + "-label");
decorator.insideLabel(field, labelElement); }

See what's gone on here? We invoke updateAttributes, but because of
this new annotation, @HeartbeatDeferred, the code doesn't execute
immediately, it waits for the end of the current heartbeat.
What's more surprising is how little code is necessary to accomplish
this. First, the new annotation:
@Target(ElementType.METHOD) @Retention(RUNTIME) @Documented @UseWith( {
COMPONENT, MIXIN, PAGE }) public @interface HeartbeatDeferred { }

The @UseWith annotation is for documentation purposes only, to make it
clear that this annotation is for use with components, pages and
mixins ... but can't be expected to work elsewhere, such as in services
layer objects.
Next we need the actual meta-programming code. Component
meta-programming is accomplished by classes that implement the
ComponentClassTransformationWorker interface.
public class HeartbeatDeferredWorker implements
ComponentClassTransformWorker { private final Heartbeat heartbeat;
private final ComponentMethodAdvice deferredAdvice = new
ComponentMethodAdvice() { public void advise(final
ComponentMethodInvocation invocation) { heartbeat.defer(new Runnable()
{ public void run() { invocation.proceed(); } }); } }; public
HeartbeatDeferredWorker(Heartbeat heartbeat) { this.heartbeat =
heartbeat; } public void transform(ClassTransformation transformation,
MutableComponentModel model) { for (TransformMethod method :
transformation.matchMethodsWithAnnotation(HeartbeatDeferred.class)) {
deferMethodInvocations(method); } } void
deferMethodInvocations(TransformMethod method) { validateVoid(method);
validateNoCheckedExceptions(method);
method.addAdvice(deferredAdvice); } private void
validateNoCheckedExceptions(TransformMethod method) { if
(method.getSignature().getExceptionTypes().length > 0) throw new
RuntimeException( String .format( "Method %s is not compatible with
the @HeartbeatDeferred annotation, as it throws checked exceptions.",
method.getMethodIdentifier())); } private void
validateVoid(TransformMethod method) { if
(!method.getSignature().getReturnType().equals("void")) throw new
RuntimeException(String.format( "Method %s is not compatible with
the @HeartbeatDeferred annotation, as it is not a void method.",
method.getMethodIdentifier())); } }

It all comes down to method advice. We can provide method advice that
executes around the call to the annotated method.
When advice is triggered, it does not call invocation.proceed()
immediately, to continue on to the original method. Instead, it builds
a Runnable command that it defers into the Heartbeat. When the command
is executed, the invocation finally does proceed and the annotated
method finally gets invoked.
That just leaves a bit of configuration code to wire this up. Tapestry
uses a chain-of-command to identify all the different workers (theres
more than a dozen built in) that get their chance to transform
component classes. Since HeartbeatDeferredWorker is part of Tapestry,
we need to extend contributeComponentClassTransformWorker() in
TapestryModule:
public static void contributeComponentClassTransformWorker(
OrderedConfiguration<ComponentClassTransformWorker> configuration { ...
configuration.addInstance("HeartbeatDeferred",
HeartbeatDeferredWorker.class, "after:RenderPhase"); }

Meta-programming gives you the ability to change the semantics of Java
programs and eliminate boiler-plate code while you're at it. Because
Tapestry is a managed environment (it loads, transforms and
instantiates the component classes) it is a great platform for
meta-programming. Whether your concerns are security, caching,
monitoring, parallelization or something else entirely, Tapestry gives
you the facilities to you need to move Java from what it is to what you
would like it to be.

--
Posted By Howard to Tapestry Central at 4/06/2010 10:00:00 AM

Reply via email to