25 July 2007

More of the same

Another Example

Continuing from yesterday, here's another example of the quick-n-dirty development technique I was describing. Before I go too much further, I need to explain a couple of things:

Velocity

I mentioned Velocity yesterday. For those that don't know, Velocity is a text templating package. You create templates containing tags (marked with $ signs), and provide a Context, which is basically a Map. Velocity can then merge the template with the context to create output which is usually written directly to a Writer. As an example, I might have this template (let's call it MyTemplate.txt):

Dear $firstname,

Here is the list of books you have ordered:

#foreach( $book in $booklist )
$book
#end

Regards...


In my code, I can create the context and merge with the template:

// Create the Context
Map<String, Object> map = new HashMap<String, Object>();
map.put("firstname", "Fred");
List<String> books = new ArrayList<String>();
books.add
("Quidditch Through the Ages");
books.add
("Fantastic Beasts & Where to Find Them");
map.put("booklist", books);
VelocityContext ctx = new VelocityContext(map);

// Merge - assumes we already have a VelocityEngine
velocityEngine.mergeTemplate("MyTemplate.txt", ctx, writer);


The result would look like this:

Dear Fred,

Here is the list of books you have ordered:

Quidditch Through the Ages
Fantastic Beasts & Where to Find Them

Regards...


iText

iText is an open-source package that (among other things) makes it easy to create PDF documents. You use it by creating a Document object then calling API methods to add paragraphs, images, and tables, set fonts, colours, text alignments and so on.

Back to the plot...

Let's say you want to be able to use Velocity templates to generate PDF documents. As things stand there's an immediate problem: Velocity needs a Writer to deliver its output to, but iText doesn't include one - and even if it did, how would it know when to insert tables, text font/colour changes, page breaks and so on?

Here is one possible solution: create an ITextWriter class, subclassing Writer, which will bridge the gap. It makes the necessary calls to the iText API according to directives it finds embedded in its input data.

The way I did this was to add the input characters from the write() calls to a StringBuffer, then look at the buffer to see if I had a complete line of text. If the line starts with a '[' and ends with a ']', it's taken to be a directive line. All other lines are treated as text to be added to the document using the current style (font, colour, alignment etc.).

So let's say I have a Velocity template that looks like this:

Here is the report for
[bold]
[color red]
$date
[normal]
[newline]
at
[font verdana 16]
[bold]
[color blue]
$time
[normal]


Velocity processes this and directs the output into the ITextWriter:

Here is the report for
[bold]
[color red]
Wednesday, July 25 2007
[normal]
[newline]
at
[font verdana 16]
[bold]
[color blue]
12:47pm
[normal]


Now, here's where the technique I described yesterday comes into play. When a directive line is found the code breaks out the first keyword (color, for example) as the directive name and places the rest into a String[] array. Then it locates a method with the directive name that accepts a String[] argument and calls it, passing the array as the argument. The method makes the necessary calls to the iText API to take care of getting the data out to the PDF document in the specified format. The result:

Here is the report for Wednesday, July 25 2007
at 12:47pm


The great thing about this is that if I need some new formatting control, all I need to do is add a new method with a meaningful name - and the job's done:

void pagebreak(String[] args)
{
...
}


In fact, the first version of the writer only had perhaps half a dozen directive methods and I added new ones as I went along, as I found I needed them for the project I was working on. The current version has about twenty methods, and most of the new methods took only minutes to create and test.

Labels:

24 July 2007

A Technique for Faster Development

Take a look at this:

import static java.lang.Integer.parseInt;
import static java.lang.System.out;

import java.lang.reflect.Method;

public class DynamicMethods
{
public int operation(String op,
int a,
int b) throws Exception
{
Class<? extends DynamicMethods> c = this
.getClass();
Class<?>[] argtypes = {
int.class, int.class};
Method m = c
.getDeclaredMethod(op,
argtypes);
Object[] args = {a, b};
int result = (Integer) m
.invoke(this, args);
return result;
}

int add(int a, int b)
{
return a + b;
}

public static void main(String[] args)
throws Exception
{
DynamicMethods dm = new DynamicMethods();
int c = dm.operation(args[0],
parseInt(args[1]),
parseInt(args[2]));
out.println("Result = " + c);
}
}


Look first at the operation() method. It takes a String ("op") and locates a Method within this class that has that name and takes two ints as arguments. It then calls that method, passing in the arguments it was given.

Now, look at the main(). The first argument is used as the op value, while the other two are parsed as integer values and passed as the other two arguments in a call to operation().

So, if we run this little program with the arguments "add 4 8", operation() calls add(), passing 4 and 8 as arguments. add() returns the sum (12), which is printed out by main().

This may seem trivial but it is the core of a powerful technique that I've found can reduce development time significantly on certain types of projects. Here's the important part: let's say I want to add a "multiply" operation. All I need to do is add the new method...

int multiply(int a, int b)
{
return a * b;
}


...and I'm done. Now I can run the application again with "multiply 4 8" as arguments and it works right away. I don't need to add lookup table entries, or change some XML file to configure in the new function. I can add as many new operations as I like just as easily - all I have to do is make sure that the new methods have a signature that has this pattern:

int operationName(int a, int b)
{
...
}


Here's an example with a bit more meat. Recently I developed a small web application to run in Tomcat. It only needed about four different page layouts, so I didn't see the need to mess around with the complication of Struts, JSPs and Action Classes (in my opinion Struts introduces way more complication than is necessary to do what it does anyway, and this only gets worse as the projects get larger). Instead, the HTML is generated from Velocity templates.

Tomcat is configured to pass all URIs with pathnames ending in "*.do" to the application's one and only servlet. Here is the servlet's service() method (with try/catch blocks removed for clarity):

public void service(ServletRequest request,
ServletResponse response)
throws ServletException,
IOException
{
// Cast req/resp to more useful types
HttpServletRequest rq =
(HttpServletRequest) request;
HttpServletResponse rs =
(HttpServletResponse) response;

// Get the URI, strip off the leading '/'
// and the ending ".do"
String uri = rq.getServletPath();
String methodName = uri.substring(1,
uri.indexOf(".do"));

// Find the right method
Class<? extends MyServlet> c = this.getClass();
Class<?>[] argTypes = {
HttpServletRequest.class,
HttpServletResponse.class};
Method method = c.getDeclaredMethod(methodName,
argTypes);

// Call the method, get back a Map
Object[] args = {rq, rs};
Map<String, Object> m =
(Map<String, Object>) method.invoke(this, args);

// The map contains the required Velocity
// template name
String templateName =
(String) m.get("templateName");

// Use the map as a Velocity Context and merge -
// output streams to the client browser
VelocityContext ctx = new VelocityContext(m);
velocityEngine.mergeTemplate(templateName,
ctx,
response.getWriter());
}
}


Let's say I need to add a "/menu.do" operation. All I need do in the servlet is add one method:

Map<String, Object> menu(HttpServletRequest request,
HttpServletResponse response)
{
...
}


This method performs whatever business logic is required and returns a Map that can be used as a Velocity context. The map must also contain a key, "templateName" identifying the Velocity template that the context is to be merged with. service() takes care of the rest.

This isn't a 100% ideal solution because part of the presentation logic (building the map) is in the methods along with the business logic - but the actual rendering is controlled by the template, so the methods don't need to mess with that. But for small applications I've found this is an excellent approach.

Labels: