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

Frank Carver, July 2003

Abstract

This article is the fourth 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 carries on working through the user requirements. On the way we'll start to extend our build system to make a deployable web application.

Introduction

If you've read the first, second and third 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 and an automated build script as we go along.

First, let's recap what our "customer" wants, and what we have got so far:

  1. each page may be viewed using it's own unique URL
  2. page content may contain links to other pages by name DONE
  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?

In the previous article I introduced the idea of "picking off" the easiest thing to test. Carrying on with this we now need to make a guess at which remaining story is the one to go for. Stories one and four still look like they need a server, and story five needs a repository. Looks like story three is the next easiest. Interestingly, it looks much easier now that we have got story two working. So let's get started.

Second task: links to nonexistent pages will be marked with a '?'

As usual, we start by adding a new test. This time, though, there's no need to start a whole new test class. We can just add to the existing "LinkTest" and see how far we get. We want page references to missing pages to be identified by a '?' which (when clicked) brings up an "edit" page to create the new page.

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"));
    }

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

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

    public void testMissingLink()
    {
    	assertEquals("goodbye<a href='edit?goodbye'>?</a>", formatter.format("@goodbye"));
    }
}

Compile and run this by typing "ant". Does it work? Of course not. We have no code for it.

What we get back is the HTML as if it is a known page. What we need to do, is to add some code which can tell the difference between known and unknown pages. Before we hit the keyboard, let's think a little. Even though this is still a very small project, do we have anything which we can re-use?

In the very first installment of this series, we created an interface:

PageRepository.java

package friki;

import java.util.Iterator;

public interface PageRepository
{
    public Page get(String name);
    public void put(String name, Page page);
    public boolean contains(String name);
    public Iterator matchingPageNames(String pattern);
}

That "contains" method looks like just what we need. Let's create the smallest "cheating" class we can, and see if it helps.

InMemoryPageRepository.java

package friki;

import java.util.Iterator;

public class InMemoryPageRepository implements PageRepository
{
    public boolean contains(String name)
    {
        return !"goodbye".equals(name);
    }

    public Page get(String name) { return null; }
    public void put(String name, Page page) {}
    public Iterator matchingPageNames(String pattern) { return null; }
}

It won't compile, we need a Page class. Before we leap to create one, let's stop and think for a bit again.

Why do we need a Page class at this point? It's not for the method we want to use, but for some of the others - ones that we've made dummy implementations for. I can't shake the feeling that there's something wrong with making an empty class just to satisfy an empty method. Is there a better way?

Luckily, there is. Remember when I said in part 2 "This interface may change or even disappear before any code is released"? Well here's just such a change. Extract the "contains" method to its own interface:

Container.java

package friki;

public interface Container
{
    public boolean contains(String name);
}

PageRepository.java

package friki;

import java.util.Iterator;

public interface PageRepository extends Container
{
    public Page get(String name);
    public void put(String name, Page page);
    public Iterator matchingPageNames(String pattern);
}

That's much better. Now we can get rid of those empty methods from our "in-memory repository", because all it needs to be is an "in-memory container":

InMemoryContainer.java

package friki;

public class InMemoryContainer implements Container
{
    public boolean contains(String name)
    {
        return !"goodbye".equals(name);
    }
}

It now all compiles, and it's neater. That's more progress. But it still doesn't make that failing test pass. We now need to use our new container in the application code. If you look back up to the test code you'll see that the formatter is responsible for generating the links, and so that's the object which needs to know about presence or absence of pages. For the moment, let's just pass our container into the formatter constructor:

LinkTest.java

package tests;

import junit.framework.*;

import friki.PageFormatter;
import friki.Container;

public class LinkTest extends TestCase
{
    PageFormatter formatter;

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

    ...
}

Looks good. But we need to update the PageFormatter to use the container:

PageFormatter.java

package friki;

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

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

    public PageFormatter(Container container)
    {
        this.container = container;
    }

    private String makeLink(String name)
    {
        if (container.contains(name))
        {
            return "<a href='view?" + name + "'>" + name + "</a>";
        }
        else
        {
            return name + "<a href='edit?" + name + "'>?</a>";
        }
    }

    ...
}

Run the tests:

    [junit] Tests run: 10, Failures: 0, Errors: 0, Time elapsed: 0.078 sec

Excellent. We're still "cheating", though. Let's add another test to point this out:

    public void testMissingLink2()
    {
    	assertEquals("whatever<a href='edit?whatever'>?</a>", formatter.format("@whatever"));
    }

It fails, of course. It's time to make that container class do some work. There's no need to do more work than necessary, though, especially when there are system classes to help:

InMemoryContainer.java

package friki;

public class InMemoryContainer extends java.util.HashMap
	implements Container
{
    public boolean contains(String name)
    {
        return super.containsKey(name);
    }
}

LinkTest.java

package tests;

import junit.framework.*;

import friki.PageFormatter;
import friki.Container;

public class LinkTest extends TestCase
{
    PageFormatter formatter;

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

    ...
}

Run the tests:

    [junit] Tests run: 11, Failures: 1, Errors: 0, Time elapsed: 0.125 sec

The tests still don't work. Can you tell why? The trick is to look in the test log, and see what failed:

Testcase: testLinkInPage took 0.016 sec
	FAILED
expected:<...<a href='view?hello'>hello...>
but was: <...hello<a href='edit?hello'>?...>
...

Aha! Our new test now works, but in making it work we have broken one of the existing tests. Here we begin to see the huge value of keeping our test cases for old features, and re-running them every time. If we look at the tests, we see that the old test just assumes that the page "hello" is present. But our new InMemoryContainer starts off empty, so there is no page "hello". The code is all doing what it should; we just need to tell the new container that we have a page "hello".

LinkTest.java

package tests;

import junit.framework.*;

import friki.PageFormatter;
import friki.Container;

public class LinkTest extends TestCase
{
    PageFormatter formatter;

    public void setUp()
    {
    	InMemoryContainer container = new InMemoryContainer();
    	container.put("hello", "some stuff");
    	formatter = new PageFormatter(container);
    }

    ...
}

Run the tests:

    [junit] Tests run: 11, Failures: 0, Errors: 0, Time elapsed: 0.078 sec

Cool.That's another feature done. Feel free to add more tests if you are not sure of anything. The gurus say "keep adding tests until fear turns to boredom"

Let's make war!

If we ever want to have an application we can just "drop in" to a servlet container, we need to make a deployable "war" file. I won't go in to a lot of detail on the structure and contents of a war file right now; I suggest you look around the internet a bit in preparation for the next installment. This time, however, all we are concerned about is what to do with our freshly baked, tested, class files.

Before I describe how to do this, I'll take a few moments to describe how not to do it.

Do you use an IDE (integrated development environment) which has buttons or menus for this sort of thing? It's a great temptation to use IDE features, but in this case I feel strongly that it would be a mistake. We want the complete build-test-package-deploy cycle to be seamless, simple and quick. We already use Ant for compilation and testing, and I suggest that Ant is good for this too. When we have eventually finished, I'm expecting to be able to just type "ant" to compile and completely test the code, build a web application and deploy it to a running server. Stopping half-way through to click some buttons or menu choices seems a clumsy way to do it.

If you have been paying attention, you should have guessed that I would recommend Ant for this. What you may not have been able to predict is that even though Ant includes a built-in "war" task, I don't recommend that you use it!

The Ant "war" task allows you to combine physically separated resources into a single war file. It does this by requiring you to specify where to get each of the main categories of information in the war file. So you tell it separately where your classes are, where your "web.xml" file is and so on. While this is a useful facility if you want to build a war file from files scattered across a file system, our ant build is not like that. We have control of where our files go. The final nail is that some versions of the "war" task have problems, such as creating the important "WEB-INF" directory as the non-standard "web-inf".

For most day-to-day purposes I recommend using the much simpler "jar" task instead. Use the ant "copy" task to make sure all our resources are laid out under some directory as they would be in a war file, then scoop the whole lot up using "jar". Add the following to "build.xml":

build.xml

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

 ...

 <target name="build-war">
  <mkdir dir='build/war/WEB-INF/classes'/>
  <copy todir='build/war/WEB-INF/classes'>
   <fileset dir='build/delivery/classes'/>
  </copy>

  <jar jarfile='frikidemo.war'>
   <fileset dir="build/war"/>
  </jar>
 </target>

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

Now we can run this and look at the generated "frikidemo.war". It should be about 4K bytes. If you wish, you can use something like WinZip to see what's inside, and check that all our class files are in there.

How are We Doing?

We still haven't made a Wiki yet! But we have completed another of our user goals, and have built on our ant-scripting efforts to automatically produce our first web application "war" file. We've also seen the benefits of growing our regression test suite as we go along.

Next session we will attack some more customer goals, and we really will produce an application which can be deployed to a web server and actually do something.