Small and Simple Web Applications - the Friki Way (Part 3)

by Frank Carver

Abstract

This article is the third of a series which laments the bloated and unmaintainable state of so many J2EE web applications and looks at ways to keep web applications small, simple and flexible. The series uses the author's Friki software as a case study, and discusses ways to design and build a powerful, flexible and usable web application in less space than a typical gif image.

This article continues the coding process begun last time, and finally starts working on real user requirements. On the way it helps take the effort out of rebuilding and re-running tests whenever anything changes.

Introduction

If you've read the first and second articles, you should be aware that the aim of this project is to develop a small, simple and understandable "Wiki" (editable web site) application. We've considered and decided to defer the relatively heavy decision of how to store and retrieve pages by introducing a Java interface, decided on and implemented a simple "template" system to display pages, and are building a supporting test suite as we go along.

Before we continue, I'd like to remind everyone of what our "customer" wants. For the purposes of this article, our customer says we will be "done" when we have a Java web application in which:

  1. each page may be viewed using it's own unique URL
  2. page content may contain links to other pages by name
  3. links to nonexistent pages will be marked with a "?"
  4. page content may be created and edited using just a browser
  5. at least 100 different pages can be stored

What do we write next?

By now, you should know that we start each new feature by writing a test for it, so the questions should perhaps rather be "what do we test next?". Whichever way you look at it, this is still a tough choice. If we want to make real progress toward our customer's goals, we need to start delivering software which meets those goals. So far, all we have is a repository interface and a template engine, neither of which is mentioned in the list.

There are lots of ways of deciding what to do next. Perhaps simply do them in the order they are written in. Maybe do the hardest first so we learn the most. Maybe ask the customer which is most important. I'm sure you can think of many more. In this case I'm going to recommend that we start by choosing the easiest to test. The idea behind starting with the easiest is that the customer goals are all tangled together. The more we can remove from the tangle, the simpler the rest will become. With any luck, by the time we get to the hard tasks, so much will already have been removed that they will be easier, too. The idea behind starting with the easiest to test is that until we have written a test, we can't start writing code, and I want to start writing real code as soon as possible.

Using this approach (often known informally as "pick the low-hanging fruit") can be suitable at the start of a new project or large single change, where there are several inter-related requirements which all need to be met for anything to work at all. Beware, though! This approach can become dangerous as soon as something has been delivered which the customers/users can try themselves. At that point it is usually better to pick whichever the customer wants the most, and see how that affects the remaining ones. It is also important to resist the temptation to keep adding more "vital" features which delay this first delivery. The sooner we can get real priority decisions from the customer, the better.

We have decided to go for the easiest to test. Which one is it?

Goals one and four look like we would need to set up a web server of some sort. To make it work would in turn mean building our software into something which can be deployed to the web server. The tests would maybe try to fetch pages from different URLs and see what they get. Sounds a lot of work before we see any results. Goal five looks like it would need us to build that repository we keep putting off. Goals two and three look as if they can be tested in a similar way to the way we tested the template engine, which seemed pretty easy. I think I'll choose to do goal two first, as completing it might help make goal three easier.

First task: "page content may contain links to other pages by name"

Let's start with a nice simple, "empty" test to make sure we have everything working. Even with this, we still need to think about how we would use the code we are going to write. One of the simpler ways is to think of it in terms of a formatting process. Some "raw" text from a stored Page will be "formatted" into "processed" text containing links where neccesary.

AllTests.java

package tests;

import junit.framework.*;

public class AllTests extends TestCase
{
    public static TestSuite suite()
    {
        TestSuite ret = new TestSuite();

        ret.addTest(new TestSuite(EmptyTest.class));
        ret.addTest(new TestSuite(TemplateTest.class));
        ret.addTest(new TestSuite(LinkTest.class));

        return ret;
    }
}

LinkTest.java

package tests;

import junit.framework.*;

import friki.PageFormatter;

public class LinkTest extends TestCase
{
    public void testEmpty()
    {
    	PageFormatter formatter = new PageFormatter();
    	assertEquals("", formatter.format(""));
    }
}

PageFormatter.java

package friki;

public class PageFormatter
{
    public String format(String input)
    {
        return input;
    }
}

Just as with the TemplateEngine, the first test and the first implementation can feel a bit like cheating. But remember, all of this is part of the design process -- we have already designed how to use the formatting code, now we have to make it work for more cases.

Next, though, a major design turning point. We know that "processed" page names will need to appear as HTML links, but how should we represent the names of pages in the "raw" text? This is an important decision, users of the system will have to type one of these names in to the browser every time they want to link to another page. The answer is not obvious, either. There are Wikis which use patterns of upper-case and lower-case letters, and automatically recognize things like FrankCarver and FrikiServlet. There are Wikis which use any group of characters wrapped in end characters like [[Frank Carver]] or {:Friki}. There are Wikis which use any alphabetic or alphanumeric word which starts with a special symbol, like @FrankCarver or ~Friki. There are even Wikis which check every word in the text, in case it might be the name of a page, and need no special delimiters at all.

This decision is so important, it needs the involvement of the customer. The job of the developers is to explain the options and answer questions to help the customer choose the right compromise between desired features and cost. In this case, our customer grumbles a bit, and settles for the word-starting-with-symbol approach, using '@' as the symbol. He says he may change his mind later, but we knew that already!

With this knowledge we can add some more tests to our LinkTest class:

    public void testNonLink()
    {
    	PageFormatter formatter = new PageFormatter();
    	assertEquals("hello", formatter.format("hello"));
    }

    public void testEmbeddedLinkSymbol()
    {
    	PageFormatter formatter = new PageFormatter();
    	assertEquals("friki@javaranch.com", formatter.format("friki@javaranch.com"));
    }

Both these tests pass with no extra code. Excellent. However, just as with the TemplateEngine test case, we now have the same setup code appearing in three places. It's time to remove the duplication and create a setUp fixture.

LinkTest.java

package tests;

import junit.framework.*;

import friki.PageFormatter;

public class LinkTest extends TestCase
{
    PageFormatter formatter;

    public void setUp()
    {
    	formatter = new PageFormatter();
    }

    public void testEmpty()
    {
    	assertEquals("", formatter.format(""));
    }

    public void testNonLink()
    {
    	assertEquals("hello", formatter.format("hello"));
    }

    public void testEmbeddedLinkSymbol()
    {
    	assertEquals("friki@javaranch.com", formatter.format("friki@javaranch.com"));
    }
}

Now we get to add a test which fails:

    public void testLink()
    {
    	assertEquals("<a href='view?hello'>hello</a>", formatter.format("@hello"));
    }

As usual, the next step is to write the simplest solution. Maybe something like:

PageFormatter.java

package friki;

public class PageFormatter
{
    public String format(String input)
    {
    	if (input.startsWith("@"))
    	{
            return "<a href='view?" + input.substring(1) + "'>" + input.substring(1) + "</a>";
    	}

        return input;
    }
}

Good, That works. It's a bit messy, and I can see some duplicated code in there, so lets refactor it to tidy it up. Don't forget to re-run all the tests after every change to make sure nothing has broken.

PageFormatter.java

package friki;

public class PageFormatter
{
    private String symbol = "@";

    private String makeLink(String name)
    {
        return "<a href='view?" + name + "'>" + name + "</a>";
    }

    public String format(String input)
    {
        if (input.startsWith(symbol))
        {
            return makeLink(input.substring(symbol.length()));
        }

        return input;
    }
}

You can probably guess where we go next. We've done just enough testing to test that we can generate a link, but it's pretty obvious that a Wiki page with just one word is not very useful, even if it is a link to another page. So we need to test that the PageFormatter can find and convert page references wherever they appear in a page. So, we add another test:

    public void testLinkInPage()
    {
    	assertEquals("You say <a href='view?hello'>hello</a>, I say goodbye.",
    		formatter.format("You say @hello, I say goodbye."));
    }

After the last test passed, we refactored the code to be simpler and neater. Now is a chance for a second sort of refactoring, the kind you can do before a change, to make the change itself simpler. I'm pretty sure we'll still need to be able to convert a word to a link if it starts with the special symbol, so let's extract that code as a method, which we can call from the "smarter" code we are about to write.

PageFormatter.java

package friki;

public class PageFormatter
{
    private String symbol = "@";

    private String makeLink(String name)
    {
        return "<a href='view?" + name + "'>" + name + "</a>";
    }

    public String formatWord(String word)
    {
        if (word.startsWith(symbol))
        {
            return makeLink(word.substring(symbol.length()));
        }

        return word;
    }

    public String format(String input)
    {
        return formatWord(input);
    }
}

Our new test still fails, of course, but now we have a clean place to put the new code. Of course there will be other changes to this class, but it's often a good idea to "clear the decks" before adding anything major.

PageFormatter.java

package friki;

import java.text.CharacterIterator;
import java.text.StringCharacterIterator;

public class PageFormatter
{
    private char symbol = '@';

    private String makeLink(String name)
    {
        return "<a href='view?" + name + "'>" + name + "</a>";
    }

    private boolean startsWith(String word, char symbol)
    {
    	return word.length() > 0 && word.charAt(0) == symbol;
    }

    public String formatWord(String word)
    {
        if (startsWith(word, symbol))
        {
            return makeLink(word.substring(1));
        }

        return word;
    }

    public String format(String input)
    {
        StringBuffer word = new StringBuffer();
        StringBuffer ret = new StringBuffer();

        CharacterIterator it = new StringCharacterIterator(input);
        for(char c = it.first(); c != CharacterIterator.DONE; c = it.next())
        {
            if (c == symbol || Character.isLetter(c))
            {
            	word.append(c);
            }
            else
            {
                ret.append(formatWord(word.toString()));
                word.setLength(0);
                ret.append(c);
            }
        }

        ret.append(formatWord(word.toString()));

        return ret.toString();
    }
}

It looks like we've finished one of the user's goals. Hooray!. There are, of course, a few things to note about this code:

First, although that "formatWord" method is still there, it has changed a little. The variable "symbol" is now a char rather than a String to make things easier in the "format" method. This restricts future expansion a tiny bit, but that doesn't matter. If anyone really ever wants to use a multi-character word prefix, there isn't much to change. The thing that irritates me most is that I can't use the built-in and nicely descriptive String.startsWith method any more, as there is no version for asking if a String starts with a char. Sigh.

Second, the structure of the "format" method looks strangely similar to the TemplateEngine.expand method we developed last time. Bear this in mind, and if we see an opportunity, we should consider refactoring the whole system to remove this duplication.

Making Rebuilding Simpler

Are you getting fed up with all that typing "javac ..." and "java ..." yet? I know I am. Even if you use an IDE or a command-line history mechanism which hides it, there's still a lot of repetition. We owe it to ourselves and to our customers to stamp this out. The solution I suggest for this is to use a "build tool".

A build tool is a program which reads a pre-defined set of instructions and relationships, and uses them to build software for you. A build tool is a kind of specialized scripting language, indeed you can get a long way using just the scripting abilities built in to your system (DOS .bat files, Unix shell-scripts etc.). In this case I recommend the Ant build tool from the Apache project. If you are not already familiar with Ant, you may want to look at Thomas Paul's March 2002 newsletter article, and install Ant as directed.

Ant is flexible and powerful, and we will be using some of its more interesting features in later articles. For now, though, we just want to get it to compile and test our code. Before we leap into scripting, we need to understand what we want Ant to do, and we also need to be able to test (even if only by inspection) that it has done it right.

The first step is to set up a sensible directory structure for our project, and copy our source files into it. The structure I recommend follows the common Ant pattern of separate directories for separate operations. In particular, note that source code and classes for the delivered application are kept completely separate from the test code. We will likely have more test code than application code by the time we have finished, and we want to keep the delivered application as lean as possible. So we keep test code separate to avoid the risk of delivering any of it.

                   friki
                     |
         +-----------+----------+
         |                      |
        src                   build
         |                      |
   +-----+------+           +---+---+
   |            |           |       |
delivery      tests      delivery tests
   |            |           |       |
 +-+---+      +-+--+        |       |
 |     |      |    |        |       |
java files  java files   classes classes
  1. Copy PageFormatter.java and TemplateEngine.java into friki/src/delivery/java/friki
  2. Copy AllTests.java, EmptyTest.java, LinkTest.java and TemplateTest.java into friki/src/tests/java/tests
  3. Create a new file friki/build.xml and put the following into it:

build.xml

<project name="friki" default="build" basedir=".">

 <path id="classpath">
   <pathelement location="build/delivery/classes"/>
   <pathelement path="${java.class.path}"/>
 </path>

 <path id="testclasspath">
   <pathelement location="build/delivery/classes"/>
   <pathelement location="build/tests/classes"/>
   <pathelement path="${java.class.path}"/>
 </path>

 <target name="clean">
   <mkdir dir="build"/><delete dir="build"/>
 </target>

 <target name="compile-delivery">
   <mkdir dir="build/delivery/classes/"/>

   <javac srcdir="src/delivery" destdir="build/delivery/classes" debug="on">
     <classpath refid="classpath"/>
   </javac>
 </target>

 <target name="compile-local-test">
   <mkdir dir="build/tests/classes"/>
   <javac srcdir="src/tests/java" destdir="build/tests/classes" debug="on">
     <classpath refid="testclasspath"/>
   </javac>
 </target>

 <target name="compile" depends="compile-delivery,compile-local-test"/>

 <target name="test">
   <junit fork="yes" haltonfailure="yes" printsummary="yes" dir="build/tests">
     <jvmarg value="-Djava.compiler=NONE"/>
     <test name="tests.AllTests"/>
     <classpath refid="testclasspath"/>
     <formatter type="plain"/>
   </junit>
 </target>

 <target name="build" depends="compile,test"/>
</project>

This may look complicated, but mostly it's just a way of configuring Ant to understand the directory structure we are using. Each target is equivalent to a public method or function definition, and the final "build" target brings them all together

Now, open a command-line, change to the top friki directory, and type ant. As specified on the first line of the build file, if no target name is specified it runs the default "build" target. You should now see a series of messages finishing with something like:

test:
    [junit] Running tests.AllTests
    [junit] Tests run: 9, Failures: 0, Errors: 0, Time elapsed: 0.156 sec

build:

BUILD SUCCESSFUL

If you get any complaints from Ant, double-check the directory structure and the build.xml file. If you see any failures or errors reported by junit, look in the file friki/TEST-tests.AllTests.txt for more details. If it all works, celebrate!

How are We Doing?

We still haven't made a Wiki yet! But we have met one of the five user goals. We have continued expanding our test suite to cover everything we have done so far, and have created a directory structure and build script to enable the whole project to be built and tested using a single, simple command. We are poised on the edge of producing a real web application.

Next session we'll add more customer features to our Wiki and extend our build script to automatically create a deployable "web application archive" (war) file. Why not try and guess which of the remaining customer goals will be next on the list...