Thursday, November 15, 2007

XML, XML where for art thou XML?

Having a cool new ANT build system in place (it's still missing the javadoc task and the package source task, but I'll live for the time being) it's time to get some actual work done. The current first application tutorial goes through much of what we need to know but let's put the necessary source files in the correct spot for Buccoo.

I decided on the package path for Buccoo classes to be org.tobago.buccoo so, following the convention, in the src folder we create an org folder and then a tobago sub-folder and then a buccoo sub-folder. It seems a generally good idea to seperate Tapestry classes into data, pages, and services, so we create those sub folders as well. The web folder has the non-java source files in it as well so we should double check that there's a WEB-INF sub folder. Our layout should be something like what's below.
Ok, our first file to tackle should really be the web.xml file which we should create in the web/WEB-INF folder. I've included the web.xml below.


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Buccoo Tapestry 5 Application</display-name>
<context-param>
<!-- The only significant configuration for Tapestry 5, this informs Tapestry
of where to look for pages, components and mixins. -->
<param-name>tapestry.app-package</param-name>
<param-value>org.tobago.buccoo</param-value>
</context-param>
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry.TapestryFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>


This is almost exactly the same web.xml file that was described in the tutorial except I changed the display name (not so important) as well as the tapestry.app-package parameter to the package path for the Buccoo classes.

We have one more config file to tackle before we start coding up a storm. This would be the log4j.properties file that we're placing in the src folder. It's a text file that's a sibling to the org folder we have in src already. I've included the log4j.properties file below.


log4j.rootCategory=WARN, A1

# A1 is set to be a ConsoleAppender.
log4j.appender.A1=org.apache.log4j.ConsoleAppender

# A1 uses PatternLayout.
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=[%p] %c{1} %m%n

log4j.category.org.apache.tapestry.TapestryFilter=info
log4j.category.org.apache.tapestry=error
log4j.category.tapestry=error

log4j.category.org.tobago.buccoo=error

# Service category names are the name of the defining module class
# and then the service id.
log4j.category.org.tobago.buccoo.services.AppModule.TimingFilter=info

# Turning on debug mode for a page or component will show all of the code changes that occur when the
# class is loaded. Turning on debug mode for a page will enable detailed output about
# the contruction of the page, including the runtime code modifications that occur. Verbose
# mode is rarely used, as it output voluminous details about the rendering of the page.
# log4j.category.org.tobago.buccoo.pages.Start=debug


If you managed to have a gander at the log4j.properties file in the tutorial, this is exactly the same, except I changed all the namespaces to org.tobago.buccoo. The log4j lets us log messages with various levels of severity either to a file or the console. It's a lot easier than using a debugger in my opinion.

Time to start the java! Tapestry starts with a page named start and its associated class Start. From the tutorial, below is the code for the Start.java file which would be in the src/org/tobago/buccoo/pages folder.


package org.tobago.buccoo.pages;

import java.util.Date;

/**
* Start page of application.
*/
public class Start
{
public Date getCurrentTime()
{
return new Date();
}
}


The only thing different from the tutorial is the package location, for obvious reasons.

What we need now is the TML template file. I know Tapestry is designed to template out HTML but we'll diverge from the tutorial from here and create an XML template instead.

The TML template is a text file which we name Start.tml and place in the web folder. I've included the source below.


<response>
<status>1</status>
<results>
<message></message>
<responsetime>${currentTime}</responsetime>
</results>
</response>


I'm really not too sure what the AppModule.java file is or does, but it does seem rather important for extending Tapestry functionality, so I've included the source from the tutorial for reference. It should go in src/org/tobago/buccoo/services.


package org.tobago.buccoo.services;

import java.io.IOException;

import org.apache.tapestry.ioc.MappedConfiguration;
import org.apache.tapestry.ioc.OrderedConfiguration;
import org.apache.tapestry.ioc.ServiceBinder;
import org.apache.tapestry.ioc.annotations.InjectService;
import org.apache.tapestry.services.Request;
import org.apache.tapestry.services.RequestFilter;
import org.apache.tapestry.services.RequestHandler;
import org.apache.tapestry.services.Response;
import org.slf4j.Logger;

/**
* This module is automatically included as part of the Tapestry IoC Registry, it's a good place to
* configure and extend Tapestry, or to place your own service definitions.
*/
public class AppModule
{
public static void bind(ServiceBinder binder)
{
// binder.bind(MyServiceInterface.class, MyServiceImpl.class);

// Make bind() calls on the binder object to define most IoC services.
// Use service builder methods (example below) when the implementation
// is provided inline, or requires more initialization than simply
// invoking the constructor.
}


public static void contributeApplicationDefaults(
MappedConfiguration<String, String> configuration)
{
// Contributions to ApplicationDefaults will override any contributions to
// FactoryDefaults (with the same key). Here we're restricting the supported
// locales to just "en" (English). As you add localised message catalogs and other assets,
// you can extend this list of locales (it's a comma seperated series of locale names;
// the first locale name is the default when there's no reasonable match).

configuration.add("tapestry.supported-locales", "en");
}


/**
* This is a service definition, the service will be named "TimingFilter". The interface,
* RequestFilter, is used within the RequestHandler service pipeline, which is built from the
* RequestHandler service configuration. Tapestry IoC is responsible for passing in an
* appropriate Log instance. Requests for static resources are handled at a higher level, so
* this filter will only be invoked for Tapestry related requests.
*
* <p>
* Service builder methods are useful when the implementation is inline as an inner class
* (as here) or require some other kind of special initialization. In most cases,
* use the static bind() method instead.
*
* <p>
* If this method was named "build", then the service id would be taken from the
* service interface and would be "RequestFilter". Since Tapestry already defines
* a service named "RequestFilter" we use an explicit service id that we can reference
* inside the contribution method.
*/
public RequestFilter buildTimingFilter(final Logger log)
{
return new RequestFilter()
{
public boolean service(Request request, Response response, RequestHandler handler)
throws IOException
{
long startTime = System.currentTimeMillis();

try
{
// The reponsibility of a filter is to invoke the corresponding method
// in the handler. When you chain multiple filters together, each filter
// received a handler that is a bridge to the next filter.

return handler.service(request, response);
}
finally
{
long elapsed = System.currentTimeMillis() - startTime;

log.info(String.format("Request time: %d ms", elapsed));
}
}
};
}

/**
* This is a contribution to the RequestHandler service configuration. This is how we extend
* Tapestry using the timing filter. A common use for this kind of filter is transaction
* management or security.
*/
public void contributeRequestHandler(OrderedConfiguration<RequestFilter> configuration,
@InjectService("TimingFilter")
RequestFilter filter)
{
// Each contribution to an ordered configuration has a name, When necessary, you may
// set constraints to precisely control the invocation order of the contributed filter
// within the pipeline.

configuration.add("Timing", filter);
}
}


The only thing I changed was the package location.

After building and deploying, it seems that although the content is generating as it should, the content type is not being sent as text/xml. This is simple enough to solve it seems, by adding a Meta annotation to the Start class. Below is the updated source of the Start.java class.


package org.tobago.buccoo.pages;

import java.util.Date;
import org.apache.tapestry.annotations.Meta;

/**
* Start page of application.
*/
@Meta("tapestry.response-content-type=text/xml")
public class Start
{
public Date getCurrentTime()
{
return new Date();
}
}


I imported the org.apache.tapestry.annotations.Meta class and set the content-type using the Tapestry property.

I found this by searching the Tapestry user list (lots of helpful people there) and ended up finding a message replied to by someone named Fernando. If you're reading this, thanks Fernando!

One more build and deploy and our XML is served with the correct content type of text/xml. I played around a bit and Tapestry seems to strip out the XML header/encoding <?xml version="1.0" encoding="UTF-8" ?>. I'm sure there must be a way to include it if I really wanted to, but for now we have some basic XML output from a Tapestry 5 template. You can confirm after you type "ant deploy" at a command prompt while in the C:\work\buccoo directory and then visit http://localhost:8080/Buccoo/Start.

I've been reading through the Tapestry 5 wiki and I think the next thing I'd tackle is some simple Tapestry security.

No comments: