Red Squirrel Reflections
Dave Hoover explores the psychology of software development

Dave Hoover
dave.hoover@gmail.com

Categories
All [Atom]
Craftsmanship [Atom]
Dynamic [Atom]
Intersection [Atom]
Learning [Atom]
Links [Atom]
Polyglot [Atom]
Projects [Atom]
XP [Atom]
Old Blog

Obtivian Blogs

Andy Maleh
Colin Harris
Fred Polgardy
Jim Breen
Kevin Taylor
Todd Webb
Turner King
Tyler Jennings

Archives

March 2009 (1)
January 2009 (1)
December 2008 (1)
October 2008 (3)
September 2008 (1)
June 2008 (4)
April 2008 (3)
March 2008 (1)
February 2008 (1)
August 2007 (1)
July 2007 (1)
June 2007 (1)
May 2007 (4)
April 2007 (3)
March 2007 (5)
February 2007 (6)
January 2007 (6)
December 2006 (10)
November 2006 (5)
October 2006 (8)
September 2006 (8)
August 2006 (5)
July 2006 (12)
June 2006 (7)
May 2006 (5)
April 2006 (5)
March 2006 (4)
February 2006 (2)
January 2006 (5)
December 2005 (5)
November 2005 (3)
October 2005 (3)
September 2005 (6)
August 2005 (4)
July 2005 (7)
June 2005 (14)
May 2005 (6)
April 2005 (8)
March 2005 (9)
February 2005 (11)
January 2005 (16)
Old Archives

 

Mon, 07 Mar 2005

Quote Manager Source

Posted the source at http://redsquirrel.com/dave/work/QuoteManager/QuoteManager.zip

[/projects/quotes] permanent link

Mon, 07 Feb 2005

Quotes Manager Project -- Day 13

An ongoing pet project blog...

As I explore my solution, I see that subsequent calls to add a new quote actually append the entire document to the previous document. That's obviously not what I want.

Ah, solved the problem I found yesterday. The hasText method in PersonPanel was messed up. Just an annoying firstName.getText() != "" vs. !"".equals(firstName.getText()) issue. I didn't think I would need to write a test for something that trivial. Stupid mistake.

Now for the second problem... Before the XML output is generated from the Document, I need to somehow empty the file. Not sure how to do that off the top of my head with only an OutputStream. Exploring...grrr, can't do it. I really don't want XmlQuoteListener to have to use files becauses that makes the testing harder.

I'm feeling stuck, so I'll write a failing test...

    public void testShouldWriteDocumentToFileWhenUpdatedMultipleTimes() throws Exception {
        Document document = parseStub();

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        QuoteListener listener = new XmlQuoteListener(document, output);

        assertEquals("", output.toString());

        listener.update();
        listener.update();

        String expectedOutput = documentToText(document);
        assertEquals(expectedOutput, output.toString());
    }
OK, this test fails and identifies the problem. If I can get it to pass, I'll be in good shape. Maybe I'll have more clarity on the way home...we're pulling into Chicago...

How about a factory to provide OutputStreams? That way we could get a new one with every call to update...

    public void testShouldWriteDocumentToFileWhenUpdatedMultipleTimes() throws Exception {
        Document document = parseStub();

        ByteArrayOutputStream output = new ByteArrayOutputStream();

        Mock factory = mock(OutputFactory.class);
        OutputStream ignored = new ByteArrayOutputStream();
        factory.expects(atLeastOnce()).method("newOutput")
               .will(onConsecutiveCalls(returnValue(ignored), returnValue(output)));

        QuoteListener listener = new XmlQuoteListener(document, (OutputFactory) factory.proxy());

        assertEquals("", output.toString());

        listener.update();
        listener.update();

        String expectedOutput = documentToText(document);
        assertEquals(expectedOutput, output.toString());
    }
This won't compile and I'll take a couple biggish steps to make it pass...
public class XmlQuoteListener implements QuoteListener {
    private final Document document;
    private final OutputFactory factory;

    public XmlQuoteListener(Document document, OutputFactory factory) {
        this.document = document;
        this.factory = factory;
    }

    public void update() {
        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            Source source = new DOMSource(document);
            Result result = new StreamResult(factory.newOutput());
            transformer.transform(source, result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
OK, I've pushed the responsibility down. I have yet another anonoymous class (this time an OutputFactory) in my main method...
    public static void main(String[] args) throws Exception {
        JFrame frame = new JFrame(TITLE);

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();

        final String xmlFile = "play.xml";
        Document document = builder.parse(xmlFile);

        QuoteListener listener = new XmlQuoteListener(document, new OutputFactory() {
            public OutputStream newOutput() throws IOException {
                return new FileOutputStream(xmlFile);
            }
        });

        QuoteManager manager = new XmlQuoteManager(document);
        manager.addListener(listener);

        QuoteManagerPanel quoterManager = new QuoteManagerPanel(manager);
        frame.getContentPane().add(quoterManager);

        frame.pack();
        frame.setVisible(true);
    }
I'll run the app and to see if that fixes my problem. I have an uneasy feeling about creating a new OutputStream every time... But hey, it worked like a charm! Time for a check in.

So let's look back at the initial goals and see where we are...

  • I want to change the quotes page to be sorted by quoter author. DONE. There should be a list of all the quoters at the top. NOT DONE. Their names should link down to their quotes on the page below. NOT DONE.
  • I want to use a simple Swing application to manage the quotes. FIRST RELEASE: ADD QUOTE
  • The Swing app will store the quotes in XML. DONE. The app will have the ability to transform this XML into HTML via XSLT. NOT DONE.
Cool, this is good. If you don't mind, I'm going knock out the rest of this stuff without writing it up. I need to be moving onto other commute-time activities ASAP. Like reading and finding more quotes...and using my new software.

If anyone is interested, I'll post the final source code when I'm finished.

[/projects/quotes] permanent link

Wed, 02 Feb 2005

Quotes Manager Project -- Day 12

An ongoing pet project blog...

I am almost finished with the task of adding a quote to an XML file. I have the GUI. I have the class that updates the XML Document. Now what I need is a class that loads the file into a Document and writes the updated Document back to the file.

But how does this class interact with XmlQuoteManager? It should probably be listening for calls to XmlQuoteManager.newQuote in order to know when to write to the file. Maybe I'll just start with that...

    public void testShouldInformListenerOnCallsToNewQuote() throws Exception {
        Mock listener = mock(QuoteListener.class);
        listener.expects(once()).method("update");

        QuoteManager manager = new XmlQuoteManager(parseStub());
        manager.addListener((QuoteListener) listener.proxy());
        manager.newQuote(new Quote("ingored", new Person("joe", "schmoe")));
    }
This leads me to (allow IDEA to) create the QuoteListener interface and add a new method to the (recently renamed) QuoteManager interface and XmlQuoteManager. I run the test and jMock tells me that my QuoteListener interface doesn't have an update method. I'm feeling confident this morning so I'll take a big step and get to green right away...
public class XmlQuoteManager implements QuoteManager {
    private final Document document;
    private final Collection<QuoteListener> listeners = new HashSet<QuoteListener>();

    public XmlQuoteManager(Document document) {
        this.document = document;
    }

    public void newQuote(Quote quote) {
        Node quoteNode = document.createElement("quote");

        quoteNode.appendChild( createTextNode(quote) );

        if (quote.source != NO_SOURCE)
            quoteNode.appendChild( createSourceNode(quote) );

        for (Person author : quote.authors)
            quoteNode.appendChild( createPersonNode(author, "author") );

        if (quote.quoter != NOBODY)
            quoteNode.appendChild( createPersonNode(quote.quoter, "quoter") );

        document.getDocumentElement().appendChild( quoteNode );

        for (QuoteListener listener : listeners)
            listener.update();
    }

    public void addListener(QuoteListener quoteListener) {
        listeners.add(quoteListener);
    }

    ...
}
But that won't even compile. I still need to add the update method to the QuoteListener interface. F2, then ALT-ENTER, then run tests and we're green.

At this point I could actually code the rest up in the main method. I think I'll do that just to see everything in action...

package com.redsquirrel.quotes.gui;

import com.redsquirrel.quotes.xml.QuoteListener;
import com.redsquirrel.quotes.xml.XmlQuoteManager;
import org.w3c.dom.Document;

import javax.swing.JFrame;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileOutputStream;

public class MAIN {
    private static final String TITLE = "Quote QuoteManager";

    public static void main(String[] args) throws Exception {
        JFrame frame = new JFrame(TITLE);

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();

        final String xmlFile = "play.xml";
        final Document document = builder.parse(xmlFile);

        QuoteManager manager = new XmlQuoteManager(document);
        manager.addListener(new QuoteListener() {
            public void update() {
                try {
                    Transformer transformer = TransformerFactory.newInstance().newTransformer();
                    Source source = new DOMSource(document);
                    Result result = new StreamResult(new FileOutputStream(xmlFile));
                    transformer.transform(source, result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        QuoteManagerPanel quoterManager = new QuoteManagerPanel(manager);
        frame.getContentPane().add(quoterManager);

        frame.pack();
        frame.setVisible(true);
    }
}
I entered a quote and it updated the XML! But there were a few issues...it added an extra (empty) author and an empty quoter. Rather than fix it now, with the new code in the main method, I'll extract it and test. Or more accurately, test then extract.

Starting with an empty test, I pause for a moment to consider what this listener will need in order to do its work... Oh, that's easy, just check for which variables had to be declared final in the main method...the file name and the Document. But I don't want to use a file name, I'd rather stick with streams. So I'll do it like this...

package com.redsquirrel.com.quotes.xml;

import org.jmock.MockObjectTestCase;
import org.w3c.dom.Document;
import com.redsquirrel.quotes.xml.QuoteListener;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import java.io.ByteArrayOutputStream;

public class XmlQuoteListenerTest extends MockObjectTestCase {
    public void testShouldWriteDocumentToFileWhenUpdated() throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("stub.xml");

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        QuoteListener listener = new XmlQuoteListener(document, output);
    }
}
I'll start by just getting that to compile. I'm tired and I'm not ready to think of any asserts yet. Here is the glorious class in it's primordial state:
package com.redsquirrel.com.quotes.xml;

import com.redsquirrel.quotes.xml.QuoteListener;
import org.w3c.dom.Document;

import java.io.OutputStream;

public class XmlQuoteListener implements QuoteListener {
    public XmlQuoteListener(Document document, OutputStream output) {
    }

    public void update() {
    }
}
OK, no more laziness, time for an assert (or two)...
    public void testShouldWriteDocumentToFileWhenUpdated() throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("stub.xml");

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        QuoteListener listener = new XmlQuoteListener(document, output);

        assertEquals("", output.toString());

        listener.update();

        String expectedOutput = documentToText(document);
        assertEquals(expectedOutput, output.toString());
    }
And that fails. I'll extract the stuff in the main method and see if that does it... Yep, back to green. Here it is...
public class XmlQuoteListener implements QuoteListener {
    private final Document document;
    private final OutputStream output;

    public XmlQuoteListener(Document document, OutputStream output) {
        this.document = document;
        this.output = output;
    }

    public void update() {
        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            Source source = new DOMSource(document);
            Result result = new StreamResult(output);
            transformer.transform(source, result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Hmm, so all my tests are passing, but the problem still persists with the extra XML elements. I'm going to explore for a while to see where the problem is...

[/projects/quotes] permanent link

Sat, 29 Jan 2005

Quotes Manager Project -- Day 11

An ongoing pet project blog...

I'm back to green. I needed a CDataSection rather than an ordinary Element. I broke the tests because a CDataSection wraps the text with <![CDATA[]]>. Here's the code:

    public void newQuote(Quote quote) {
        Node root = document.getDocumentElement();
        Node quoteNode = document.createElement("quote");

        Node textNode = document.createElement("text");
        Node textSection = document.createCDATASection(quote.text);
        textNode.appendChild(textSection);
        quoteNode.appendChild(textNode);

        Node sourceNode = document.createElement("source");
        Attr sourceIsbn = document.createAttribute("isbn");
        sourceIsbn.setValue(quote.source.isbn);
        sourceNode.getAttributes().setNamedItem(sourceIsbn);
        sourceNode.setTextContent(quote.source.title);
        quoteNode.appendChild(sourceNode);

        for (Person author : quote.authors) {
            Attr authorUrl = document.createAttribute("url");
            authorUrl.setValue(author.url);
            Node authorNode = document.createElement("author");
            authorNode.getAttributes().setNamedItem(authorUrl);

            Node authorFirst = document.createElement("first");
            authorFirst.setTextContent(author.firstName);
            authorNode.appendChild(authorFirst);

            Node authorLast = document.createElement("last");
            authorLast.setTextContent(author.lastName);
            authorNode.appendChild(authorLast);

            quoteNode.appendChild(authorNode);
        }

        root.appendChild(quoteNode);
    }

And here's the latest test...
    public void testShouldWriteXmlWhenGivenNewQuoteWithPageNumber() throws Exception {
        String text = "The major problems of our work are not so much <i>technological</i> as <i>sociological</i> in nature.";
        String isbn = "0932633439";
        String title = "Peopleware: Productive Projects and Teams";
        String page = "4";
        String firstName1 = "Tom";
        String lastName1 = "DeMarco";
        String url1 = "http://www.systemsguild.com/GuildSite/TDM/Tom_DeMarco.html";
        String firstName2 = "Timothy";
        String lastName2 = "Lister";
        String url2 = "http://www.cutter.com/consultants/tlbio.html";
        Collection<Person> authors = new ArrayList<Person>(2);
        authors.add(new Person(firstName1, lastName1, url1));
        authors.add(new Person(firstName2, lastName2, url2));
        Quote quote = new Quote(text, new Source(title, page, isbn), authors, null);

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("stub.xml");

        assertEquals(0, document.getElementsByTagName("quote").getLength());

        Manager manager = new XmlQuoteManager(document);
        manager.newQuote(quote);

        assertEquals(1, document.getElementsByTagName("quote").getLength());

        String textContent = documentToText(document);
        assertTrue(-1 != textContent.indexOf("<text><![CDATA[" + text + "]]></text>"));
//        assertTrue(-1 != textContent.indexOf("<source isbn=\""+ isbn + "\" page=\"" + page + "\">" + title + "</source>"));
//        assertTrue(-1 != textContent.indexOf("<author url=\""+ url1 + "\"><first>" + firstName1 + "</first><last>" + lastName1 + "</last></author>"));
//        assertTrue(-1 != textContent.indexOf("<author url=\""+ url2 + "\"><first>" + firstName2 + "</first><last>" + lastName2 + "</last></author>"));
    }

I'll uncomment those tests and see where I am. Arg, they're failing on the next line. Something to do with the source, it's missing a page. Oh, there's a good reason for that, I never coded it. This is why I write tests first.

So I added the page attribute, but then my other test broke because it doesn't have a page. Time for a handy-dandy if statement. Ahhh, back to green. Here's the if block:

        if (quote.source.page != null && !quote.source.page.trim().equals("")) {
            Attr sourcePage = document.createAttribute("page");
            sourcePage.setValue(quote.source.page);
            sourceNode.getAttributes().setNamedItem(sourcePage);
        }
I need a test with a quoter.
    public void testShouldWriteXmlWhenGivenNewQuoteWithQuoter() throws Exception {
        String text = "To be able to understand when the rules don't apply, you need to completely understand when they do.";
        String isbn = "0201760436";
        String title = "Agile Software Development Ecosystems";
        String page = "31";
        String quoterFirstName = "Bob";
        String quoterLastName = "Charette";
        String quoterUrl = "http://www.cutter.com/consultants/charetter.html";
        Person quoter = new Person(quoterFirstName, quoterLastName, quoterUrl);
        String firstName = "Jim";
        String lastName = "Highsmith";
        String url = "http://www.adaptivesd.com/";
        Collection<Person> authors = new ArrayList<Person>(1);
        authors.add(new Person(firstName, lastName, url));
        Quote quote = new Quote(text, new Source(title, page, isbn), authors, quoter);

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("stub.xml");

        assertEquals(0, document.getElementsByTagName("quote").getLength());

        Manager manager = new XmlQuoteManager(document);
        manager.newQuote(quote);

        assertEquals(1, document.getElementsByTagName("quote").getLength());

        String textContent = documentToText(document);
        assertTrue(-1 != textContent.indexOf("<text><![CDATA[" + text + "]]></text>"));
        assertTrue(-1 != textContent.indexOf("<source isbn=\""+ isbn + "\" page=\"" + page + "\">" + title + "</source>"));
        assertTrue(-1 != textContent.indexOf("<author url=\""+ url + "\"><first>" + firstName + "</first><last>" + lastName + "</last></author>"));
        assertTrue(-1 != textContent.indexOf("<quoter url=\""+ quoterUrl + "\"><first>" + quoterFirstName + "</first><last>" + quoterLastName + "</last></quoter>"));
    }

This fails where I expected, on the last assert. I'll put in the quoter code on the way home. We just pulled into Chicago...

Back to green. I basically just copied and pasted the body of the author loop and after a couple SHIFT-F6's the variables had the appropriate names.

    public void newQuote(Quote quote) {
        Node root = document.getDocumentElement();
        Node quoteNode = document.createElement("quote");

        Node textNode = document.createElement("text");
        Node textSection = document.createCDATASection(quote.text);
        textNode.appendChild(textSection);
        quoteNode.appendChild(textNode);

        Node sourceNode = document.createElement("source");
        Attr sourceIsbn = document.createAttribute("isbn");
        sourceIsbn.setValue(quote.source.isbn);
        sourceNode.getAttributes().setNamedItem(sourceIsbn);

        if (quote.source.page != null && !quote.source.page.trim().equals("")) {
            Attr sourcePage = document.createAttribute("page");
            sourcePage.setValue(quote.source.page);
            sourceNode.getAttributes().setNamedItem(sourcePage);
        }
        sourceNode.setTextContent(quote.source.title);
        quoteNode.appendChild(sourceNode);

        for (Person author : quote.authors) {
            Attr authorUrl = document.createAttribute("url");
            authorUrl.setValue(author.url);
            Node authorNode = document.createElement("author");
            authorNode.getAttributes().setNamedItem(authorUrl);

            Node authorFirst = document.createElement("first");
            authorFirst.setTextContent(author.firstName);
            authorNode.appendChild(authorFirst);

            Node authorLast = document.createElement("last");
            authorLast.setTextContent(author.lastName);
            authorNode.appendChild(authorLast);

            quoteNode.appendChild(authorNode);
        }

        if (quote.quoter != null) {
            Attr quoterUrl = document.createAttribute("url");
            quoterUrl.setValue(quote.quoter.url);
            Node quoterNode = document.createElement("quoter");
            quoterNode.getAttributes().setNamedItem(quoterUrl);

            Node quoterFirst = document.createElement("first");
            quoterFirst.setTextContent(quote.quoter.firstName);
            quoterNode.appendChild(quoterFirst);

            Node quoterLast = document.createElement("last");
            quoterLast.setTextContent(quote.quoter.lastName);
            quoterNode.appendChild(quoterLast);

            quoteNode.appendChild(quoterNode);
        }

        root.appendChild(quoteNode);
    }
There's nothing that drives me crazier than developers who copy and paste code without refactoring it out. So it's time to refactor this monsterous method. After a series of CTRL-ALT-M's and CTRL-ALT-N's, otherwise known as extract method and inline variable, the method looks a lot nicer.
    public void newQuote(Quote quote) {
        Node quoteNode = document.createElement("quote");

        quoteNode.appendChild( createTextNode(quote) );
        quoteNode.appendChild( createSourceNode(quote) );

        for (Person author : quote.authors)
            quoteNode.appendChild( createAuthorNode(author) );

        if (quote.quoter != null)
            quoteNode.appendChild( createQuoterNode(quote) );

        document.getDocumentElement().appendChild( quoteNode );
    }
The tests are still green. There is duplication between the createQuoterNode and createAuthorNode. Let's get rid of that ASAP. I'll start by seeing what happens if I use the createAuthorNode rather than the createQuoterNode.
    public void newQuote(Quote quote) {
        Node quoteNode = document.createElement("quote");

        quoteNode.appendChild( createTextNode(quote) );
        quoteNode.appendChild( createSourceNode(quote) );

        for (Person author : quote.authors)
            quoteNode.appendChild( createAuthorNode(author) );

        if (quote.quoter != null)
            quoteNode.appendChild( createAuthorNode(quote.quoter) );

        document.getDocumentElement().appendChild( quoteNode );
    }
That breaks the quoter test because now the quoter is showing up as an author. I introduce parameter (CTRL-ALT-P) on tag name, make sure the quoter code is passing in "quoter" and rename method (SHIFT-F6). Voila! Back to green, and IDEA tells me I have dead code (createQuoterNode) I can delete. So I do (I really love that feeling).

Here's the current state of the method:

    public void newQuote(Quote quote) {
        Node quoteNode = document.createElement("quote");

        quoteNode.appendChild( createTextNode(quote) );
        quoteNode.appendChild( createSourceNode(quote) );

        for (Person author : quote.authors)
            quoteNode.appendChild( createPersonNode(author, "author") );

        if (quote.quoter != null)
            quoteNode.appendChild( createPersonNode(quote.quoter, "quoter") );

        document.getDocumentElement().appendChild( quoteNode );
    }
I should find some other permutations to test before moving on. Like authors without URLs or sources without ISBNs. But I won't bore you with them now.

[/projects/quotes] permanent link

Fri, 28 Jan 2005

Quote Manager Project -- Day 10

An ongoing pet project blog...

So I RTFM and found that you can add XML Nodes like this:

        Node root = document.getElementsByTagName("quotes").item(0);
        Node quoteNode = document.createElement("quote");
        root.appendChild(quoteNode);
That passes the test. I should have this task completed in (as my buddy Jeff says) two shakes of a lamb's tail. I'm going to make the test a bit more rigorous...
    public void testShouldWriteXmlWhenGivenNewQuote() throws Exception {
        String text = "Program to an interface, not an implementation.";
        String isbn = "0201633612";
        String title = "Design Patterns";
        String firstName = "Gang";
        String lastName = "of Four";
        String url = "http://c2.com/cgi/wiki?GangOfFour";
        Collection<Person> authors = new ArrayList<Person>(1);
        authors.add(new Person(firstName, lastName, url));
        Quote quote = new Quote(text, new Source(title, "", isbn), authors, null);

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("stub.xml");

        assertEquals(0, document.getElementsByTagName("quote").getLength());

        Manager manager = new XmlQuoteManager(document);
        manager.newQuote(quote);

        assertEquals(1, document.getElementsByTagName("quote").getLength());

        String textContent = documentToText(document);
        assertTrue(-1 != textContent.indexOf("<text>" + text + "</text>"));
    }
Not a lot more rigor, but I want to take small steps. The documentToText method is boring so I left it out. The test is failing. I just need to add a text Node to the quote Node. Like so...
   public void newQuote(Quote quote) {
        Node root = document.getElementsByTagName("quotes").item(0);

        Node textNode = document.createElement("text");
        textNode.setTextContent(quote.text);

        Node quoteNode = document.createElement("quote");
        quoteNode.appendChild(textNode);
        root.appendChild(quoteNode);
    }
And we're green. As I was looking at the code, I figured there might be a better way to get the root element. There is...
    public void newQuote(Quote quote) {
        Node root = document.getDocumentElement();

        Node textNode = document.createElement("text");
        textNode.setTextContent(quote.text);

        Node quoteNode = document.createElement("quote");
        quoteNode.appendChild(textNode);
        root.appendChild(quoteNode);
    }
Still green. Adding more rigor...
    public void testShouldWriteXmlWhenGivenNewQuote() throws Exception {
        String text = "Program to an interface, not an implementation.";
        String isbn = "0201633612";
        String title = "Design Patterns";
        String firstName = "Gang";
        String lastName = "of Four";
        String url = "http://c2.com/cgi/wiki?GangOfFour";
        Collection authors = new ArrayList(1);
        authors.add(new Person(firstName, lastName, url));
        Quote quote = new Quote(text, new Source(title, "", isbn), authors, null);

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("stub.xml");

        assertEquals(0, document.getElementsByTagName("quote").getLength());

        Manager manager = new XmlQuoteManager(document);
        manager.newQuote(quote);

        assertEquals(1, document.getElementsByTagName("quote").getLength());

        String textContent = documentToText(document);
        assertTrue(-1 != textContent.indexOf("" + text + ""));
        assertTrue(-1 != textContent.indexOf("" + title + ""));
        assertTrue(-1 != textContent.indexOf("" + firstName + "" + lastName + ""));
    }
It's not a perfect test, but it's the simplest test (that I can come up with) that will drive me to where I want to go. This code passes the test:
    public void newQuote(Quote quote) {
        Node root = document.getDocumentElement();
        Node quoteNode = document.createElement("quote");

        Node textNode = document.createElement("text");
        textNode.setTextContent(quote.text);
        quoteNode.appendChild(textNode);

        Node sourceNode = document.createElement("source");
        Attr sourceIsbn = document.createAttribute("isbn");
        sourceIsbn.setValue(quote.source.isbn);
        sourceNode.getAttributes().setNamedItem(sourceIsbn);
        sourceNode.setTextContent(quote.source.title);
        quoteNode.appendChild(sourceNode);

        for (Person author : quote.authors) {
            Attr authorUrl = document.createAttribute("url");
            authorUrl.setValue(author.url);
            Node authorNode = document.createElement("author");
            authorNode.getAttributes().setNamedItem(authorUrl);

            Node authorFirst = document.createElement("first");
            authorFirst.setTextContent(author.firstName);
            authorNode.appendChild(authorFirst);

            Node authorLast = document.createElement("last");
            authorLast.setTextContent(author.lastName);
            authorNode.appendChild(authorLast);

            quoteNode.appendChild(authorNode);
        }

        root.appendChild(quoteNode);
    }
Very straightforward. I'm feeling good about this right now. I'm going to add more tests to handle some different variations of authors, quoters, and sources. I won't bore you with those unless something interesting comes up.

Well, that didn't take long. Something interesting came up. Here's the new test...

    public void testShouldWriteXmlWhenGivenNewQuoteWithPageNumber() throws Exception {
        String text = "The major problems of our work are not so much <i>technological</i> as <i>sociological</i> in nature.";
        String isbn = "0932633439";
        String title = "Peopleware: Productive Projects and Teams";
        String page = "4";
        String firstName1 = "Tom";
        String lastName1 = "DeMarco";
        String url1 = "http://www.systemsguild.com/GuildSite/TDM/Tom_DeMarco.html";
        String firstName2 = "Timothy";
        String lastName2 = "Lister";
        String url2 = "http://www.cutter.com/consultants/tlbio.html";
        Collection<Person> authors = new ArrayList<Person>(2);
        authors.add(new Person(firstName1, lastName1, url1));
        authors.add(new Person(firstName2, lastName2, url2));
        Quote quote = new Quote(text, new Source(title, page, isbn), authors, null);

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("stub.xml");

        assertEquals(0, document.getElementsByTagName("quote").getLength());

        Manager manager = new XmlQuoteManager(document);
        manager.newQuote(quote);

        assertEquals(1, document.getElementsByTagName("quote").getLength());

        String textContent = documentToText(document);
        assertTrue(-1 != textContent.indexOf("<text>" + text + "</text>"));
        assertTrue(-1 != textContent.indexOf("<source isbn=\""+ isbn + "\" page=\"" + page + "\">" + title + "</source>"));
        assertTrue(-1 != textContent.indexOf("<author url=\""+ url1 + "\"><first>" + firstName1 + "</first><last>" + lastName1 + "</last></author>"));
        assertTrue(-1 != textContent.indexOf("<author url=\""+ url2 + "\"><first>" + firstName2 + "</first><last>" + lastName2 + "</last></author>"));
    }
I expected this to fail because the code doesn't handle pages. But it failed for a different reason. The HTML in the quote got escaped and failed at the first assertTrue! I'm going to comment out all the other asserts while I resolve this issue...

[/projects/quotes] permanent link

Thu, 27 Jan 2005

Quote Manager Project -- Day 9

An ongoing pet project blog...

I'm trying to hook my new, more tested/able AddButtonActionListener into the QuoteManagerPanel and having some difficulties. Everything compiles but when I inject this anonymous Manager into QuoteManagerPanel, I get nothing after filling out the quote form and clicking 'Add'...

        Manager manager = new Manager() {
            public void newQuote(Quote quote) {
                System.out.println(quote.text);
            }
        };
        QuoteManagerPanel quoterManager = new QuoteManagerPanel(manager);
I get something when I print out quote.source.title, but the text property is blank. Hmmm...got it! The problem is that when I create my AddButtonActionListener, I pass in the value of the quote text then and there. Like this:
    public QuoteManagerPanel(Manager manager) {
        setLayout(new GridBagLayout());

        for (int i = 0; i < NUMBER_OF_AUTHORS; i++)
            authorPanels[i] = new PersonPanel("Author " + (i + 1));

        ActionListener actionListener = new AddButtonActionListener(manager,
                                                                    quoteText.getText(),
                                                                    sourcePanel,
                                                                    authorPanels,
                                                                    quoterPanel);
        addButton.addActionListener(actionListener);
        layoutForm();
    }
At that point, the field is blank. The simplest thing right now would be to pass in the JTextField rather than the String. I'll change the tests and see what happens... Ah, that's better.

To complete my current task, I need to add a quote to an XML file. A Manager can handle that. I want to start with a test...

package com.redsquirrel.com.quotes.xml;

import com.redsquirrel.quotes.gui.Manager;
import com.redsquirrel.quotes.model.Person;
import com.redsquirrel.quotes.model.Quote;
import com.redsquirrel.quotes.model.Source;
import org.jmock.MockObjectTestCase;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

public class XmlQuoteManagerTest extends MockObjectTestCase {
    public void testShouldWriteXmlWhenGivenNewQuote() throws ParserConfigurationException, IOException, SAXException {
        String text = "Program to an interface, not an implementation.";
        String isbn = "0201633612";
        String title = "Design Patterns";
        String firstName = "Gang";
        String lastName = "of Four";
        String url = "http://c2.com/cgi/wiki?GangOfFour";
        Collection authors = new ArrayList(1);
        authors.add(new Person(firstName, lastName, url));
        Quote quote = new Quote(text, new Source(title, "", isbn), authors, null);

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("stub.xml");

        assertEquals(0, document.getElementsByTagName("quote").getLength());

        Manager manager = new XmlQuoteManager(document);
        manager.newQuote(quote);

        assertEquals(1, document.getElementsByTagName("quote").getLength());
    }
}

Here's stubs.xml:
<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="sample.xsl"?>

<quotes>
</quotes>
The test simply checks to make sure the document has gone from zero to one quote. I'll start by letting IDEA create the XmlQuoteManager. The test fails. To get the test to pass I need to add an element of type "quote". Shoot, I'm not sure how to do that. In my spike I simply updated an attribute of an existing Node. Here's where I'm stuck...
package com.redsquirrel.com.quotes.xml;

import com.redsquirrel.quotes.gui.Manager;
import com.redsquirrel.quotes.model.Quote;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

public class XmlQuoteManager implements Manager {
    private final Document document;

    public XmlQuoteManager(Document document) {
        this.document = document;
    }

    public void newQuote(Quote quote) {
        Node root = document.getElementsByTagName("quotes").item(0);

        Node quoteNode = ????
        root.appendChild(quoteNode);
    }
}
Time to experiment. I don't feel like RTFM right now. Famous last words, I know...

[/projects/quotes] permanent link

Wed, 26 Jan 2005

Quote Manager Project -- Day 8

An ongoing pet project blog...

I could pass the test by changing the hard-coded values of the Quote I'm passing into the Manager (and provide an equals method), but I'd rather get to the point and inject the dependencies that AddButtonActionListener needs to do its work.

My first instinct is to extract interfaces from PersonPanel and SourcePanel in order to mock them more easily, but they would just be a bunch of getters. Bletch. But since I can't think of anything better right now, that's just what I'll do.

Ahh, back to green. That was painful, and I'm doubting if all of the extra steps I just took were worth it. I had IDEA generate equals (and hashCode) for all of my model classes so that I could verify that the expected Quote matched the Quote created by AddButtonActionListener. I also had to create two interfaces: PersonComponent and SourceComponent.

Here's the test:

    public void testShouldProvideNewQuoteToManager() {
        Collection<Person> authors = new HashSet<Person>();
        String firstName = "Dee";
        String lastName = "Hock";
        String url = "http://chaordic.org/";
        authors.add(new Person(firstName, lastName, url));

        String text = "Community is not about profit, it is about benefit.  We confuse them at our peril.";
        String title = "Birth of the Chaordic Age";
        String page = "43";
        String isbn = "1576750744";
        Source source = new Source(title, page, isbn);
        Quote expected = new Quote(text, source, authors, null);

        Mock manager = mock(Manager.class);
        manager.expects(once()).method("newQuote").with(eq(expected));

        Mock mockSource = mock(SourceComponent.class);
        mockSource.expects(once()).method("getTitle").will(returnValue(title));
        mockSource.expects(once()).method("getPage").will(returnValue(page));
        mockSource.expects(once()).method("getIsbn").will(returnValue(isbn));

        Mock mockAuthor = mock(PersonComponent.class);
        mockAuthor.expects(once()).method("hasText").will(returnValue(true));
        mockAuthor.expects(once()).method("getFirstName").will(returnValue(firstName));
        mockAuthor.expects(once()).method("getLastName").will(returnValue(lastName));
        mockAuthor.expects(once()).method("getUrl").will(returnValue(url));

        Collection<PersonComponent> mockAuthors = new HashSet<PersonComponent>();
        mockAuthors.add((PersonComponent) mockAuthor.proxy());

        AddButtonActionListener actionListener = new AddButtonActionListener((Manager) manager.proxy(),
                                                                             text,
                                                                             (SourceComponent) mockSource.proxy(),
                                                                             mockAuthors);
        actionListener.actionPerformed(null);
    }
Ack! That's a bunch of crap. I don't like it. And here's the class...
package com.redsquirrel.com.quotes.gui;

import com.redsquirrel.quotes.model.Person;
import com.redsquirrel.quotes.model.Quote;
import com.redsquirrel.quotes.model.Source;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collection;
import java.util.HashSet;

public class AddButtonActionListener implements ActionListener {
    private final Manager manager;
    private final String text;
    private final SourceComponent source;
    private final Collection<PersonComponent> authors;

    public AddButtonActionListener(Manager manager, String text, SourceComponent source, Collection<PersonComponent> authors) {
        this.manager = manager;
        this.text = text;
        this.source = source;
        this.authors = authors;
    }

    public void actionPerformed(ActionEvent e) {
        Source source = new Source(this.source.getTitle(), this.source.getPage(), this.source.getIsbn());

        Collection<Person> authors = new HashSet<Person>();
        for (PersonComponent author : this.authors)
            if (author.hasText())
                authors.add(new Person(author.getFirstName(), author.getLastName(), author.getUrl()));

        //Person quoter = new Person(quoterPanel.getFirstName(), quoterPanel.getLastName(), quoterPanel.getUrl());
        Quote quote = new Quote(text, source, authors, null);

        manager.newQuote(quote);
    }
}
The quote I'm testing doesn't have a quoter, so I'm not handling that case yet. I guess that would be a good next step...

Here's a new test that includes a quoter...

    public void testShouldProvideNewQuoteWithOptionalQuoterToManager() {
        Collection<Person> authors = new HashSet<Person>();
        String firstName = "Frederick";
        String lastName = "Brooks";
        String url = "http://www.cs.unc.edu/~brooks/";
        authors.add(new Person(firstName, lastName, url));

        String quoterFirst = "Jim";
        String quoterLast = "McCarthy";
        String quoterUrl = "http://www.mccarthy-tech.com/";
        Person quoter = new Person(quoterFirst, quoterLast, quoterUrl);

        String text = "I can't emphasize enough the importance of empowerment, of the team being accountable to itself for its success.";
        String title = "The Mythical Man Month";
        String page = "279";
        String isbn = "0201835959";
        Source source = new Source(title, page, isbn);
        Quote expected = new Quote(text, source, authors, quoter);

        Mock manager = mock(Manager.class);
        manager.expects(once()).method("newQuote").with(eq(expected));

        Mock mockSource = mock(SourceComponent.class);
        mockSource.expects(once()).method("getTitle").will(returnValue(title));
        mockSource.expects(once()).method("getPage").will(returnValue(page));
        mockSource.expects(once()).method("getIsbn").will(returnValue(isbn));

        Mock mockAuthor = mock(PersonComponent.class);
        mockAuthor.expects(once()).method("hasText").will(returnValue(true));
        mockAuthor.expects(once()).method("getFirstName").will(returnValue(firstName));
        mockAuthor.expects(once()).method("getLastName").will(returnValue(lastName));
        mockAuthor.expects(once()).method("getUrl").will(returnValue(url));

        Collection<PersonComponent> mockAuthors = new HashSet<PersonComponent>();
        mockAuthors.add((PersonComponent) mockAuthor.proxy());

        Mock mockQuoter = mock(PersonComponent.class);
        mockQuoter.expects(once()).method("getFirstName").will(returnValue(quoterFirst));
        mockQuoter.expects(once()).method("getLastName").will(returnValue(quoterLast));
        mockQuoter.expects(once()).method("getUrl").will(returnValue(quoterUrl));

        AddButtonActionListener actionListener = new AddButtonActionListener((Manager) manager.proxy(),
                                                                             text,
                                                                             (SourceComponent) mockSource.proxy(),
                                                                             mockAuthors,
                                                                             (PersonComponent) mockQuoter.proxy());
        actionListener.actionPerformed(null);
    }
That fails (after I add a new constructor to take a PersonComponent). Looks like it should be easy to get back to green... Yep, it was.
    public void actionPerformed(ActionEvent e) {
        Source source = new Source(this.source.getTitle(), this.source.getPage(), this.source.getIsbn());

        Collection<Person> authors = new HashSet<Person>();
        for (PersonComponent author : this.authors)
            if (author.hasText())
                authors.add(new Person(author.getFirstName(), author.getLastName(), author.getUrl()));

        Person quoter = null;
        if (this.quoter != null)
            quoter = new Person(this.quoter.getFirstName(), this.quoter.getLastName(), this.quoter.getUrl());

        Quote quote = new Quote(text, source, authors, quoter);

        manager.newQuote(quote);
    }
Now that I've got a green bar, I think I'd like to do some refactoring to make this code a bit easier on the eyes. Why not give the model objects additional constructors to take in their corresponding component objects? I was reluctant to do this before because I didn't want to couple the model to GUI code, but since they're just simple interfaces, I'm OK with it now...
    public void actionPerformed(ActionEvent e) {
        Source source = new Source(this.source);

        Collection<Person> authors = new HashSet<Person>();
        for (PersonComponent author : this.authors)
            if (author.hasText())
                authors.add(new Person(author));

        Person quoter = null;
        if (this.quoter != null)
            quoter = new Person(this.quoter);

        Quote quote = new Quote(text, source, authors, quoter);

        manager.newQuote(quote);
    }
Ahh, that's much better. And the tests are still passing. Time to test the for-if condition when there are multiple AuthorComponents.
    public void testShouldProvideNewQuoteWithTwoAuthorsToManager() {
        Collection<Person> authors = new HashSet<Person>();
        String firstName = "Mary";
        String lastName = "Poppendieck";
        String url = "http://poppendieck.com/";
        authors.add(new Person(firstName, lastName, url));

        String firstName2 = "Tom";
        String lastName2 = "Poppendieck";
        String url2 = "http://poppendieck.com/";
        authors.add(new Person(firstName2, lastName2, url2));

        String text = "It is necessary to have a reasonable failure rate in order to generate a reasonable amount of information.";
        String title = "Lean Software Development: An Agile Toolkit";
        String page = "19";
        String isbn = "0321150783";
        Source source = new Source(title, page, isbn);
        Quote expected = new Quote(text, source, authors, null);

        Mock manager = mock(Manager.class);
        manager.expects(once()).method("newQuote").with(eq(expected));

        Mock mockSource = mock(SourceComponent.class);
        mockSource.expects(once()).method("getTitle").will(returnValue(title));
        mockSource.expects(once()).method("getPage").will(returnValue(page));
        mockSource.expects(once()).method("getIsbn").will(returnValue(isbn));

        Mock blankAuthor = mock(PersonComponent.class);
        blankAuthor.expects(once()).method("hasText").will(returnValue(false));

        Mock mary = mock(PersonComponent.class);
        mary.expects(once()).method("hasText").will(returnValue(true));
        mary.expects(once()).method("getFirstName").will(returnValue(firstName));
        mary.expects(once()).method("getLastName").will(returnValue(lastName));
        mary.expects(once()).method("getUrl").will(returnValue(url));
`
        Mock tom = mock(PersonComponent.class);
        tom.expects(once()).method("hasText").will(returnValue(true));
        tom.expects(once()).method("getFirstName").will(returnValue(firstName2));
        tom.expects(once()).method("getLastName").will(returnValue(lastName2));
        tom.expects(once()).method("getUrl").will(returnValue(url2));

        Collection<PersonComponent> mockAuthors = new HashSet<PersonComponent>();
        mockAuthors.add((PersonComponent) blankAuthor.proxy());
        mockAuthors.add((PersonComponent) mary.proxy());
        mockAuthors.add((PersonComponent) tom.proxy());

        AddButtonActionListener actionListener = new AddButtonActionListener((Manager) manager.proxy(),
                                                                             text,
                                                                             (SourceComponent) mockSource.proxy(),
                                                                             mockAuthors);
        actionListener.actionPerformed(null);
    }
And that passes without any changes to the code. Good times. I wish those test cases were shorter.

[/projects/quotes] permanent link

Tue, 25 Jan 2005

Quote Manager Project -- Day 7

An ongoing pet project blog...

Must...write...test...for anonymous ActionListener...

Let's look at our ActionListener:

        addButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Source source = new Source(QuoteManagerPanel.this.source.getText(), page.getText(), isbn.getText());
                Collection<Person> authors = new ArrayList<Person>();

                for (int i = 0; i < NUMBER_OF_AUTHORS; i++)
                    if (authorPanels[i].hasText())
                        authors.add(new Person(authorPanels[i].getFirstName(), authorPanels[i].getLastName(), authorPanels[i].getUrl()));

                Person quoter = new Person(quoterFirstName.getText(), quoterLastName.getText(), quoterUrl.getText());
                new Quote(quoteText.getText(), source, authors, quoter);
            }
        });
I have reduced the number of objects the ActionListener depends on by creating the AuthorPanel. Looks like I could do something similar with the quoter fields. Here we go again...how'd I start last time? Oh yeah, extract method...
    private void layoutForm() {
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;

        int y = 0;
        addLabel("Quote", constraints, y++);
        addField(quoteText, constraints);
        addLabel("Source", constraints, y++);
        addField(source, constraints);
        addLabel("ISBN", constraints, y++);
        addField(isbn, constraints);
        addLabel("Page", constraints, y++);
        addField(page, constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 0;

        for (int i = 0; i < NUMBER_OF_AUTHORS; i++) {
            constraints.gridy = y++;
            add(authorPanels[i], constraints);
        }

        layoutQuoter(constraints, y);
       y += 2;

        constraints.anchor = GridBagConstraints.EAST;
        constraints.gridx = 0;
        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridy = y++;
        add(addButton, constraints);
    }

    private void layoutQuoter(GridBagConstraints constraints, int y) {
        addLabel("Quoter", constraints, y);
        addFullName(quoterFirstName, quoterLastName, constraints);
        addLabel("URL", constraints, y+1);
        addField(quoterUrl, constraints);
    }

And then an inline method time four, and return a JPanel...
    private void layoutForm() {
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;

        int y = 0;
        addLabel("Quote", constraints, y++);
        addField(quoteText, constraints);
        addLabel("Source", constraints, y++);
        addField(source, constraints);
        addLabel("ISBN", constraints, y++);
        addField(isbn, constraints);
        addLabel("Page", constraints, y++);
        addField(page, constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 0;

        for (int i = 0; i < NUMBER_OF_AUTHORS; i++) {
            constraints.gridy = y++;
            add(authorPanels[i], constraints);
        }

        constraints.gridy = y++;
       add(layoutQuoter(), constraints);

        constraints.anchor = GridBagConstraints.EAST;
        constraints.gridx = 0;
        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridy = y++;
        add(addButton, constraints);
    }

    private JPanel layoutQuoter() {
        JPanel quoterPanel = new JPanel(new GridBagLayout());

        GridBagConstraints constraints = new GridBagConstraints();

        constraints.gridx = 0;
        constraints.gridwidth = 1;
        constraints.gridy = 0;
        quoterPanel.add(new JLabel("Quoter"), constraints);

        constraints.gridwidth = GridBagConstraints.RELATIVE;
        constraints.gridx = 1;
        quoterPanel.add(quoterFirstName, constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 2;
        quoterPanel.add(quoterLastName, constraints);

        constraints.gridx = 0;
        constraints.gridwidth = 1;
        constraints.gridy = 1;
        quoterPanel.add(new JLabel("URL"), constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 1;
        quoterPanel.add(quoterUrl, constraints);

        return quoterPanel;
    }
This looks very familiar, which is good, because it means I can probably share code with the AuthorPanel. But not yet...first the inner class...

QuoterPanel is almost an exact replica of the AuthorPanel. Let's see if I can extract the duplication... Yep, no problem. I created a concrete PersonPanel which was an exact copy of AuthorPanel except that the constructor takes the entire String for the label, rather than the authorNumber. That way when I need a PersonPanel to represent a quoter, I just pass in "Quoter". Pretty simple.

Now my anonymous ActionListener depends on six objects. I can get that down to four if I group the source fields into their own panel. I think I can do this quickly...I've gone through the steps twice already. I'll take some bigger steps and not bore you with the details...

I have a SourcePanel. There is a lot of GridBagLayout duplication between the three different JPanel derivatives, but I'm not going to worry about that right now. It's time to test...

package com.redsquirrel.com.quotes.gui;

import org.jmock.MockObjectTestCase;
import org.jmock.Mock;

public class AddButtonActionListenerTest extends MockObjectTestCase {
    public void testShouldProvideNewQuoteToManager() {
        Mock manager = mock(Manager.class);
        AddButtonActionListener actionListener = new AddButtonActionListener((Manager) manager.proxy());
        actionListener.actionPerformed(null);
    }
}
This test doesn't even come close to compiling, so I'll let IDEA do the work for me with a series of wonderous F2/ALT-ENTER steps. This leaves me with an empty Manager interface and a stubbed out AddButtonActionListener. I'll run the test.

It passes. Doh! I didn't add any expectations! Here's a simple one...

    public void testShouldProvideNewQuoteToManager() {
        Mock manager = mock(Manager.class);
        manager.expects(once()).method("newQuote").with(isA(Quote.class));

        AddButtonActionListener actionListener = new AddButtonActionListener((Manager) manager.proxy());
        actionListener.actionPerformed(null);
    }
That fails. Good. Ahhh, gotta love that red bar. Gives you a nice sense of purpose: get it to green ASAP. Back to green with this ugly sin.
public class AddButtonActionListener implements ActionListener {
    private final Manager manager;

    public AddButtonActionListener(Manager manager) {
        this.manager = manager;
    }

    public void actionPerformed(ActionEvent e) {
        manager.newQuote(new Quote("foo", new Source("foo", "foo", "foo"), new HashSet(), new Person("foo", "foo", "foo")));
    }
}
I'll make that test a bit more rigorous...
    public void testShouldProvideNewQuoteToManager() {
        Collection<Person> authors = new HashSet<Person>();
       authors.add(new Person("Dee", "Hock", "http://chaordic.org/"));

       String text = "Community is not about profit, it is about benefit.  We confuse them at our peril.";
       Source source = new Source("Birth of the Chaordic Age", "43", "1576750744");
       Quote expected = new Quote(text, source, authors, null);

        Mock manager = mock(Manager.class);
        manager.expects(once()).method("newQuote").with(eq(expected));

        AddButtonActionListener actionListener = new AddButtonActionListener((Manager) manager.proxy());
        actionListener.actionPerformed(null);
    }

That fails, and I'll leave it failing until tomorrow. Here's my stop...

[/projects/quotes] permanent link

Sat, 22 Jan 2005

Quote Manager Project -- Day 6

An ongoing pet project blog...

I've added multiplie author fields to the interface. Here is the ugly screen...

And the code that produced it...

package com.redsquirrel.quotes.gui;

import ...

public class QuoteManagerPanel extends JPanel {
    private static final int NUMBER_OF_AUTHORS = 6;
    ...
    private final JTextField[] authorUrls = new JTextField[NUMBER_OF_AUTHORS];
    private final JTextField[] authorFirstNames = new JTextField[NUMBER_OF_AUTHORS];
    private final JTextField[] authorLastNames = new JTextField[NUMBER_OF_AUTHORS];
    ...

    public QuoteManagerPanel() {
        setLayout(new GridBagLayout());

        for (int i = 0; i < NUMBER_OF_AUTHORS; i++) {
            authorUrls[i] = new JTextField(15);
            authorFirstNames[i] = new JTextField(7);
            authorLastNames[i] = new JTextField(7);
        }

        addButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Source source = new Source(QuoteManagerPanel.this.source.getText(), page.getText(), isbn.getText());
                Collection<Person> authors = new ArrayList<Person>();

                for (int i = 0; i < NUMBER_OF_AUTHORS; i++)
                    if (authorFirstNames[i].getText() != "")
                        authors.add(new Person(authorFirstNames[i].getText(), authorLastNames[i].getText(), authorUrls[i].getText()));

                Person quoter = new Person(quoterFirstName.getText(), quoterLastName.getText(), quoterUrl.getText());
                new Quote(quoteText.getText(), source, authors, quoter);
            }
        });

        layoutForm();
    }

    private void layoutForm() {
        ...
    }
}
Dave the customer is feeling a bit better now that I can enter the authors I need, but Dave the developer is not happy...still no tests to speak of. Let's take a quick look around and find some easy wins... How about that anonymous ActionListener? I'm going to try to test it...

I've brought up my test template in IDEA and I have a blank test method...

package com.redsquirrel.com.quotes.gui;

import org.jmock.MockObjectTestCase;

public class AddButtonActionListenerTest extends MockObjectTestCase {
    public void testShould() {

    }
}
So how do I test this thing? Currently it is coupled to all of the JTextComponents in QuoteManagerPanel. The code that I'm most nervous about is the for loop with the if condition. I'm going to need to promote this ActionListener to a concrete class, but I don't want to pass in ten objects to the constructor...or call ten different setters. How about I pull the related fields into their own JPanels? The thought occurred to me when I had to create three separate author arrays.

I'm pulling into the station. I'll check in and extract the fields into JPanels on the way home...

I'll start with the code that handles authors' JTextComponents. Here's it is in its current glory (in bold):

    private void layoutForm() {
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;

        int y = 0;
        addLabel("Quote", constraints, y++);
        addField(quoteText, constraints);
        addLabel("Source", constraints, y++);
        addField(source, constraints);
        addLabel("ISBN", constraints, y++);
        addField(isbn, constraints);
        addLabel("Page", constraints, y++);
        addField(page, constraints);

        for (int i = 0; i < NUMBER_OF_AUTHORS; i++) {
            addLabel("Author " + (i+1), constraints, y++);
            addFullName(authorFirstNames[i], authorLastNames[i], constraints);
            addLabel("URL", constraints, y++);
            addField(authorUrls[i], constraints);
        }

        addLabel("Quoter", constraints, y++);
        addFullName(quoterFirstName, quoterLastName, constraints);
        addLabel("URL", constraints, y++);
        addField(quoterUrl, constraints);

        constraints.anchor = GridBagConstraints.EAST;
        constraints.gridx = 0;
        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridy = y++;
        add(addButton, constraints);
    }
Time to extract method...
    private void layoutForm() {
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;

        int y = 0;
        addLabel("Quote", constraints, y++);
        addField(quoteText, constraints);
        addLabel("Source", constraints, y++);
        addField(source, constraints);
        addLabel("ISBN", constraints, y++);
        addField(isbn, constraints);
        addLabel("Page", constraints, y++);
        addField(page, constraints);

        for (int i = 0; i < NUMBER_OF_AUTHORS; i++) {
            layoutAuthor(i, constraints, y);
            y += 2;
        }

        addLabel("Quoter", constraints, y++);
        addFullName(quoterFirstName, quoterLastName, constraints);
        addLabel("URL", constraints, y++);
        addField(quoterUrl, constraints);

        constraints.anchor = GridBagConstraints.EAST;
        constraints.gridx = 0;
        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridy = y++;
        add(addButton, constraints);
    }

    private void layoutAuthor(int i, GridBagConstraints constraints, int y) {
        addLabel("Author " + (i+1), constraints, y);
        addFullName(authorFirstNames[i], authorLastNames[i], constraints);
        addLabel("URL", constraints, y+1);
        addField(authorUrls[i], constraints);
    }
It's a start. But I need to group the fields into a JPanel. I'll start with inlining the methods inside layoutAuthor...
    private void layoutAuthor(int i, GridBagConstraints constraints, int y) {
        constraints.gridx = 0;
        constraints.gridwidth = 1;
        constraints.gridy = y;
        add(new JLabel("Author " + (i+1)), constraints);

        constraints.gridwidth = GridBagConstraints.RELATIVE;
        constraints.gridx = 1;
        add(authorFirstNames[i], constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 2;
        add(authorLastNames[i], constraints);

        constraints.gridx = 0;
        constraints.gridwidth = 1;
        constraints.gridy = y+1;
        add(new JLabel("URL"), constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 1;
        add(authorUrls[i], constraints);
    }
Bletch! That's ugly. Let's introduce that JPanel ASAP...
    private void layoutForm() {
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;

        int y = 0;
        addLabel("Quote", constraints, y++);
        addField(quoteText, constraints);
        addLabel("Source", constraints, y++);
        addField(source, constraints);
        addLabel("ISBN", constraints, y++);
        addField(isbn, constraints);
        addLabel("Page", constraints, y++);
        addField(page, constraints);

        for (int i = 0; i < NUMBER_OF_AUTHORS; i++) {
            JPanel authorPanel = layoutAuthor(i, constraints, y);
            y += 2;
        }

        addLabel("Quoter", constraints, y++);
        addFullName(quoterFirstName, quoterLastName, constraints);
        addLabel("URL", constraints, y++);
        addField(quoterUrl, constraints);

        constraints.anchor = GridBagConstraints.EAST;
        constraints.gridx = 0;
        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridy = y++;
        add(addButton, constraints);
    }

    private JPanel layoutAuthor(int i, GridBagConstraints constraints, int y) {
        JPanel authorPanel = new JPanel(new GridBagLayout());

        constraints.gridx = 0;
        constraints.gridwidth = 1;
        constraints.gridy = y;
        authorPanel.add(new JLabel("Author " + (i+1)), constraints);

        constraints.gridwidth = GridBagConstraints.RELATIVE;
        constraints.gridx = 1;
        authorPanel.add(authorFirstNames[i], constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 2;
        authorPanel.add(authorLastNames[i], constraints);

        constraints.gridx = 0;
        constraints.gridwidth = 1;
        constraints.gridy = y+1;
        authorPanel.add(new JLabel("URL"), constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 1;
        authorPanel.add(authorUrls[i], constraints);

        return authorPanel;
    }
Ouch, that's not much better, and now the author fields are missing in the GUI. I need to add that JPanel... Hack, hack, hack, it's looking OK...
    private void layoutForm() {
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;

        int y = 0;
        addLabel("Quote", constraints, y++);
        addField(quoteText, constraints);
        addLabel("Source", constraints, y++);
        addField(source, constraints);
        addLabel("ISBN", constraints, y++);
        addField(isbn, constraints);
        addLabel("Page", constraints, y++);
        addField(page, constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 0;

        for (int i = 0; i < NUMBER_OF_AUTHORS; i++) {
            constraints.gridy = y++;
            JPanel authorPanel = layoutAuthor(i);
            add(authorPanel, constraints);
        }

        addLabel("Quoter", constraints, y++);
        addFullName(quoterFirstName, quoterLastName, constraints);
        addLabel("URL", constraints, y++);
        addField(quoterUrl, constraints);

        constraints.anchor = GridBagConstraints.EAST;
        constraints.gridx = 0;
        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridy = y++;
        add(addButton, constraints);
    }

    private JPanel layoutAuthor(int i) {
        JPanel authorPanel = new JPanel(new GridBagLayout());

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;
        constraints.gridwidth = 1;
        authorPanel.add(new JLabel("Author " + (i+1)), constraints);

        constraints.gridwidth = GridBagConstraints.RELATIVE;
        constraints.gridx = 1;
        authorPanel.add(authorFirstNames[i], constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 2;
        authorPanel.add(authorLastNames[i], constraints);

        constraints.gridx = 0;
        constraints.gridwidth = 1;
        constraints.gridy = 1;
        authorPanel.add(new JLabel("URL"), constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 1;
        authorPanel.add(authorUrls[i], constraints);

        return authorPanel;
    }
Now I can extract the body of layoutAuthor into an inner class and inline the layoutAuthor method...
    private void layoutForm() {
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;

        int y = 0;
        addLabel("Quote", constraints, y++);
        addField(quoteText, constraints);
        addLabel("Source", constraints, y++);
        addField(source, constraints);
        addLabel("ISBN", constraints, y++);
        addField(isbn, constraints);
        addLabel("Page", constraints, y++);
        addField(page, constraints);

        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridx = 0;

        for (int i = 0; i < NUMBER_OF_AUTHORS; i++) {
            constraints.gridy = y++;
            add(new AuthorPanel(i), constraints);
        }

        addLabel("Quoter", constraints, y++);
        addFullName(quoterFirstName, quoterLastName, constraints);
        addLabel("URL", constraints, y++);
        addField(quoterUrl, constraints);

        constraints.anchor = GridBagConstraints.EAST;
        constraints.gridx = 0;
        constraints.gridwidth = GridBagConstraints.REMAINDER;
        constraints.gridy = y++;
        add(addButton, constraints);
    }

    private class AuthorPanel extends JPanel {
        AuthorPanel(int i) {
            setLayout(new GridBagLayout());

            GridBagConstraints constraints = new GridBagConstraints();
            constraints.anchor = GridBagConstraints.WEST;
            constraints.gridwidth = 1;
            add(new JLabel("Author " + (i+1)), constraints);

            constraints.gridwidth = GridBagConstraints.RELATIVE;
            constraints.gridx = 1;
            add(authorFirstNames[i], constraints);

            constraints.gridwidth = GridBagConstraints.REMAINDER;
            constraints.gridx = 2;
            add(authorLastNames[i], constraints);

            constraints.gridx = 0;
            constraints.gridwidth = 1;
            constraints.gridy = 1;
            add(new JLabel("URL"), constraints);

            constraints.gridwidth = GridBagConstraints.REMAINDER;
            constraints.gridx = 1;
            add(authorUrls[i], constraints);
        }
    }
Great, still looking good, but those three author arrays are bugging me. Let's get rid of those...

I could show you all the things I did to remove those arrays, but this post is getting way too long...and I'm sure it's very boring. It only took a few minutes. I simply replaced the three arrays with an AuthorPanel array.

Hopefully we're a bit closer to testing. I'll have to wait until tomorrow, though. My stop is coming up...here's what the AuthorPanel class looks like...

    private class AuthorPanel extends JPanel {
        private final JTextField firstName = new JTextField(7);
        private final JTextField lastName = new JTextField(7);
        private final JTextField url = new JTextField(15);

        AuthorPanel(int authorNumber) {
            setLayout(new GridBagLayout());

            GridBagConstraints constraints = new GridBagConstraints();
            constraints.anchor = GridBagConstraints.WEST;
            constraints.gridwidth = 1;
            add(new JLabel("Author " + authorNumber), constraints);

            constraints.gridwidth = GridBagConstraints.RELATIVE;
            constraints.gridx = 1;
            add(firstName, constraints);

            constraints.gridwidth = GridBagConstraints.REMAINDER;
            constraints.gridx = 2;
            add(lastName, constraints);

            constraints.gridx = 0;
            constraints.gridwidth = 1;
            constraints.gridy = 1;
            add(new JLabel("URL"), constraints);

            constraints.gridwidth = GridBagConstraints.REMAINDER;
            constraints.gridx = 1;
            add(url, constraints);
        }

        public boolean hasText() {
            return firstName.getText() != "" || lastName.getText() != "" || url.getText() != "";
        }

        public String getFirstName() {
            return firstName.getText();
        }

        public String getLastName() {
            return lastName.getText();
        }

        public String getUrl() {
            return url.getText();
        }
    }

[/projects/quotes] permanent link

Fri, 21 Jan 2005

Quote Manager Project -- Day 5

An ongoing pet project blog...

OK, I've hacked together an interface that has the fields I need...

And I just did a bit of refactoring to remove most of the duplication. Checking in...

This is a passive application for the moment. I'd like to provide it with an object that it can store the quote data in so that when I click the "Add" button, this object is populated with data. Perhaps this will be called a Quote? I'll just have it be an immutable value object for now...

package com.redsquirrel.quotes.model;

import java.util.Collection;

public class Quote {
    public final String text;
    public final Source source;
    public final Collection<Person> authors;
    public final Person quoter;

    public Quote(String text, Source source, Collection<Person> authors, Person quoter) {
        this.text = text;
        this.source = source;
        this.authors = authors;
        this.quoter = quoter;
    }
}

package com.redsquirrel.quotes.model;

public class Source {
    public final String title;
    public final String page;
    public final String isbn;

    public Source(String title, String page, String isbn) {
        this.title = title;
        this.page = page;
        this.isbn = isbn;
    }
}

package com.redsquirrel.quotes.model;

public class Person {
    public final String firstName;
    public final String lastName;
    public final String url;

    public Person(String firstName, String lastName, String url) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.url = url;
    }
}
This is kinda boring, which probably means I'm doing something wrong...or I am about to. I wish I had a pair programmer to reassure (or warn) me.

So now that I have my immutable value objects, I see that I'm going to be creating these objects rather than populating previously existing ones. Not sure if that's bad or good. I'm really tired right now.

OK, I just realized one of the things I've overlooked. I've only given the user the ability to enter one author per quote! Yikes, I'll have to deal with that later. I want to keep making progress...

OK, I guess this is progress. I've extracted the JButton and all of the JTextComponents into private final fields. I extracted the layout code to a private method. Finally, I added a anonymous ActionListener. Here's what it looks like (sans layout code)...

package com.redsquirrel.quotes.gui;

import com.redsquirrel.quotes.model.Quote;
import com.redsquirrel.quotes.model.Source;
import com.redsquirrel.quotes.model.Person;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JButton;
import javax.swing.JComponent;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.util.Collection;
import java.util.ArrayList;

public class QuoteManagerPanel extends JPanel {
    private final JTextArea quoteText = new JTextArea(3, 15);
    private final JButton addButton = new JButton("Add");
    private final JTextField source = new JTextField(15);
    private final JTextField isbn = new JTextField(15);
    private final JTextField page = new JTextField(5);
    private final JTextField authorUrl = new JTextField(15);
    private final JTextField quoterUrl = new JTextField(15);
    private final JTextField authorFirstName = new JTextField(7);
    private final JTextField authorLastName = new JTextField(7);
    private final JTextField quoterFirstName = new JTextField(7);
    private final JTextField quoterLastName = new JTextField(7);

    public QuoteManagerPanel() {
        setLayout(new GridBagLayout());

        addButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Source source = new Source(QuoteManagerPanel.this.source.getText(), page.getText(), isbn.getText());
                Collection authors = new ArrayList();
                authors.add(new Person(authorFirstName.getText(), authorLastName.getText(), authorUrl.getText()));
                Person quoter = new Person(quoterFirstName.getText(), quoterLastName.getText(), quoterUrl.getText());
                new Quote(quoteText.getText(), source, authors, quoter);
            }
        });

        layoutForm();
    }

    private void layoutForm() {
		...
    }
}
I have created a Quote! How sad that I don't know what to do with it. And damn that's some tedious code, just mapping GUI components to structs. As a developer, I'm not happy that there are no tests. And as a customer, I'm not happy that I can only input one author per quote. Before I figure out what to do with our freshly baked Quote, I need to figure out an acceptable solution to the N authors problem.

Checking in...

To solve the N authors problem, I will peruse the quotes file, find the quote with the greatest number of authors, and provide that many author fields. Let's see...it appears that I quote at least two books that have three authors. I quote the Gang of Four, but refer to the authors in bulk, not individually. I'll update the GUI code to handle up to three authors...

[/projects/quotes] permanent link

Thu, 20 Jan 2005

Quote Manager Project -- Day 4

An ongoing pet project blog...

I set up a project in IDEA. It's time to start hacking together some GUI code. The first task I am looking to complete is to simply add a new quote to the XML file.

OK, so let's do some GUI work. Hopefully I will be able to write a test or two along the way... Here's what I'm starting with:

package com.redsquirrel.quotes.gui;

public class MAIN {
    public static void main(String[] args) {
        new QuoteManagerPanel();
    }
}
IDEA tells me to Create Class 'QuoteManagerPanel', and I hit the magical ALT-ENTER combination. And away we go! Hack, hack, hack. I've got something visible...

Here's the code that did it:

package com.redsquirrel.quotes.gui;

import javax.swing.JFrame;

public class MAIN {
    private static final String TITLE = "Quote Manager";

    public static void main(String[] args) {
        JFrame frame = new JFrame(TITLE);

        frame.getContentPane().add(new QuoteManagerPanel());

        frame.pack();
        frame.setVisible(true);
    }
}

package com.redsquirrel.quotes.gui;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;

public class QuoteManagerPanel extends JPanel {

    public QuoteManagerPanel() {
        add(new JLabel("Quote: "));
        add(new JTextArea(3, 15));
    }
}
Time to add all of the data field for a quote. I won't bore you with anymore Swing code for a while. It's gonna get ugly...

[/projects/quotes] permanent link

Tue, 18 Jan 2005

Quote Manager Project -- Day 3

An ongoing pet project blog...

Looks like the code snippet I took from the Java XSL tutorial uses DOM parsing (as opposed to SAX parsing). This doesn't mean much to me, other than pointing me toward the correct documentation. OK, the documentation sucks. I'm going to experiment...

Made some progress. Experimenting with a nice Java IDE like IDEA turned out to be more useful than the documentation. Here's what I have so far. This tells me that I have 6 quotes in my data file...

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("sample.xml");

        NodeList quotes = document.getElementsByTagName("quote");
        System.out.println(quotes.getLength());
Still trying to unwind this XML. I'm having flashbacks to using Perl's XML::Parser. OK, looks like I've got a handle on it. Here's what I have now...it prints out all of the child tags of each quote, and accesses the isbn attribute of the source tag:
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("sample.xml");

        NodeList quotes = document.getElementsByTagName("quote");
        for (int i = 0; i < quotes.getLength(); i++) {
            NodeList children = quotes.item(i).getChildNodes();
            for (int j = 0; j < children.getLength(); j++) {
                Node node = children.item(j);
                String nodeName = node.getNodeName();
                if (nodeName == "#text") continue;
                System.out.print(nodeName + ":");
                if (nodeName == "source") {
                    NamedNodeMap attributes = node.getAttributes();
                    Node isbn = attributes.getNamedItem("isbn");
                    if (isbn != null)
                        System.out.print(isbn + ":");
                }
                System.out.println(node.getTextContent().trim());
            }
            System.out.println("-----------------");
        }
Here is the output:
text:We in the software industry are working with a more or less invisible product,
			yet this very invisibility only heightens our need for feedback.
quoter:GeraldWeinberg
source:isbn="0932633447":Project Retrospectives
author:NormKerth
-----------------
text:By building tests before you implement the code,
			you get to try out the interface before you commit to it.
source:isbn="020161622X":The Pragmatic Programmer
author:DaveThomas
author:AndyHunt
-----------------
text:For everyone who exalts himself will be humbled,
			and he who humbles himself will be exalted.
source:Luke 18:14b
-----------------
text:Programming is the Great Game.
			It consumes you, body and soul.
			When you're caught up in it, nothing else matters.
source:How Software Companies Die
author:Orson ScottCard
-----------------
text:It's never the size of the step that a person takes that counts,
			but its direction.
source:isbn="0393700984":Narrative Means to Therapeutic Ends
author:MichaelWhite
author:DavidEpston
-----------------
text:Having a plan isn't everything, but planning is.
source:isbn="0201708426":Extreme Programming Installed
author:RonJeffries
author:AnnAnderson
author:ChetHendrickson
-----------------
I'm in the station now. Looks like updating XML files will wait until the ride home.

OK, let's see how to update an XML document. I want to change Jerry's URL to http://ayeconference.com/...

Got it. It works like this:

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("sample.xml");

        NodeList quoters = document.getElementsByTagName("quoter");
        NamedNodeMap jerryAttributes = quoters.item(0).getAttributes();

        Node node = jerryAttributes.getNamedItem("url");
        node.setNodeValue("http://ayeconference.com/");
But that doesn't change the actual file, just the in-memory XML structure. How do I write the Document to a file? Judging from the public methods I see in IDEA, it doesn't look like a Document knows how to write itself, or even provide an OutputStream. Back to the tutorial...

Oh, it looks like I already had it. Here's the code. It should look familiar:

        String xmlFile = "sample.xml";

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse(xmlFile);

        NodeList quoters = document.getElementsByTagName("quoter");
        NamedNodeMap jerryAttributes = quoters.item(0).getAttributes();

        Node node = jerryAttributes.getNamedItem("url");
        node.setNodeValue("http://ayeconference.com/");

        Transformer transformer = TransformerFactory.newInstance().newTransformer();

        Source source = new DOMSource(document);
        Result result = new StreamResult(new FileOutputStream(xmlFile));
        transformer.transform(source, result);

This is just like the code I used to generate HTML, except I don't specify an XSL file in my call to TransformerFactory.newInstance().newTransformer(). This code updates the file lickety-split. Cool.

Time for a check in.

As the customer, I'd like to get an update on the progress of the project. As the developer, I will give my appraisal of how far I've come:

  • I want to change the quotes page to be sorted by quoter author. DONE. There should be a list of all the quoters at the top. NOT DONE. Their names should link down to their quotes on the page below. NOT DONE.
  • I want to use a simple Swing application to manage the quotes. NOT DONE.
  • The Swing app will store the quotes in XML. NOT DONE. The app will have the ability to transform this XML into HTML via XSLT. NOT DONE.
Dang, that's depressing. As the customer, I'm disappointed. As the developer looking at the plan, I am nervous about how little I actually got done. But my confidence about how to accomplish these tasks is much, much higher.

So what is my next task? It's time to view the quote data in a Swing app. Finally! Some actual coding! No more spiking!

Crap, my stop is coming up. This will have to wait until tomorrow...

[/projects/quotes] permanent link

Sat, 15 Jan 2005

Quote Manager Project -- Day 2

An ongoing pet project blog...

So now I know how to read XML data and, using Java 5.0, transform it into HTML. I also have an XSL style sheet that duplicates the look and feel of my current quotes page. Again I find myself wondering what to do next. Again I will put on my customer hat and consider what I want. Let's look at the original plan:

  • I want to change the quotes page to be sorted by quoter. There should be a list of all the quoters at the top. Their names should link down to their quotes on the page below.
  • I want to use a simple Swing application to manage the quotes.
  • The Swing app will store the quotes in XML. The app will have the ability to transform this XML into HTML via XSLT.
I could add sorting to my style sheet. Or I could do another spike that reads in XML data and writes updates. Updating XML sounds more intersting, but as a customer, I'd like to see the sorting I've asked for. So let's sort...

OK, sorting in XSL is easy. I just had to add two lines beneath my <xsl:for-each>...

		<xsl:for-each select="quotes/quote">
			<xsl:sort select="quoter"/>
			<xsl:sort select="author"/>
But that doesn't do what I want. First of all, it sorts by first name. Secondly, it puts all of the quoter-less quotes first, and then adds the quote by Weinberg last. As my fellow ThoughtWorker Dave Wood would say: "Boooo."

Actually, this is good. It looks like I'm going to need to change my XML data structure in order to allow for the sorting I need. I'm glad I chose to do this task before the XML update spike, since the data format would have changed. Pulling into the station...

OK, I've changed the XML data format to break quoter and author names down into first and last names like:

		<quoter url="http://www.geraldmweinberg.com/">
			<first>Gerald</first><last>Weinberg</last>
		</quoter>
But that still doesn't solve the problem of wanting to sort by quoter, since most quotes just have authors. As the customer, I'm OK with sorting by authors for now, so I'll change the sorting tags to:
		<xsl:for-each select="quotes/quote">
			<xsl:sort select="author/last"/>
			<xsl:sort select="author/first"/>
I'm checking in.

My next task is to spike reading and updating the XML file from Java. I wonder if I could use some of my XSL transformation code? It looks like the first half of my previous spike could come in handy...

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("sample.xml");
But I'm not familiar with how to use a Document. Back to the Java XML tutorial...

[/projects/quotes] permanent link

Fri, 14 Jan 2005

Quotes Manager Project -- Day 1

An ongoing pet project blog...

OK, that wasn't very fun. I grabbed the XSLT specfication and wrestled my style sheet into shape. I've got it looking exactly the way my quotes page does now. Hoo-boy. Here's the stylesheet:

<?xml version="1.0" encoding="ISO-8859-1"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:variable name="book_url" select="'http://amazon.com/ISBN'"/>

<xsl:template match="/">
	<html>
	<body>
		<xsl:for-each select="quotes/quote">
			<p>
			<xsl:value-of select="text"/>

			<xsl:text>--</xsl:text>

			<xsl:if test="quoter">
				<a href="{quoter/@url}"><xsl:value-of select="quoter"/></a> in
			</xsl:if>

			<xsl:choose>
				<xsl:when test="source/@isbn">
					<a href="{$book_url}/{source/@isbn}">
					<i><xsl:value-of select="source"/></i>
					</a>
				</xsl:when>
				<xsl:otherwise>
					<xsl:value-of select="source"/>
				</xsl:otherwise>
			</xsl:choose>

			<xsl:text>, </xsl:text>

			<xsl:if test="source/@page">
				p. <xsl:value-of select="source/@page"/>,
			</xsl:if>

			<xsl:for-each select="author">
				<xsl:choose>
					<xsl:when test="@url">
						<a href="{@url}"><xsl:apply-templates/></a>
					</xsl:when>
					<xsl:otherwise>
						<xsl:apply-templates/>
					</xsl:otherwise>
				</xsl:choose>
				<xsl:choose>
					<xsl:when test="position()=last()-1"> and </xsl:when>
					<xsl:when test="not(position()=last())">, </xsl:when>
				</xsl:choose>
			</xsl:for-each>
			</p>
		</xsl:for-each>
	</body>
	</html>
</xsl:template>

</xsl:stylesheet>
Here is the data I'm using. It has most of the quote variations that I can remember:
<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="sample.xsl"?>

<quotes>
	<quote>
		<text>
			We in the software industry are working with a more or less invisible product,
			yet this very invisibility only heightens our need for feedback.
		</text>
		<quoter url="http://www.geraldmweinberg.com/">Gerald Weinberg</quoter>
		<source isbn="0932633447" page="xiii">Project Retrospectives</source>
		<author url="http://www.retrospectives.com/">Norm Kerth</author>
	</quote>
	<quote>
		<text>
			By building tests <i>before</i> you implement the code,
			you get to try out the interface before you commit to it.
		</text>
		<source isbn="020161622X" page="192">The Pragmatic Programmer</source>
		<author url="http://www.pragmaticprogrammer.com/">Dave Thomas</author>
		<author url="http://www.pragmaticprogrammer.com/">Andy Hunt</author>
	</quote>
	<quote>
		<text>
			For everyone who exalts himself will be humbled,
			and he who humbles himself will be exalted.
		</text>
		<source>Luke 18:14b</source>
	</quote>
	<quote>
		<text>
			Programming is the Great Game.
			It consumes you, body and soul.
			When you're caught up in it, nothing else matters.
		</text>
		<source url="http://www.apocalypse.org/pub/u/kjc/cool/Card.on.Software.html">How Software Companies Die</source>
		<author url="http://www.hatrack.com/osc/about.shtml">Orson Scott Card</author>
	</quote>
	<quote>
		<text>
			It's never the size of the step that a person takes that counts,
			but its direction.
		</text>
		<source isbn="0393700984">Narrative Means to Therapeutic Ends</source>
		<author>Michael White</author>
		<author>David Epston</author>
	</quote>
	<quote>
		<text>
			Having a plan isn't everything, but planning is.
		</text>
		<source isbn="0201708426">Extreme Programming Installed</source>
		<author url="http://www.xprogramming.com/">Ron Jeffries</author>
		<author url="http://c2.com/cgi/wiki?AnnAnderson">Ann Anderson</author>
		<author url="http://c2.com/cgi/wiki?ChetHendrickson">Chet Hendrickson</author>
	</quote>
</quotes>
Time for a check in. OK, so what's next? Let's take a look at the original plan...
  • I want to change the quotes page to be sorted by quoter. There should be a list of all the quoters at the top. Their names should link down to their quotes on the page below.
  • I want to use a simple Swing application to manage the quotes.
  • The Swing app will store the quotes in XML. The app will have the ability to transform this XML into HTML via XSLT.
I could start setting up the Swing app, which would be a nice change of pace. Or I could learn how to do XSL tranformations in Java. Or I could add sorting and a quoter list to the style sheet. Hmm, if I were a customer, which would I think was more important? For now, I think I'm satisfied with seeing what I already have on the quotes page. But I'm uncomfortable with having Firefox do the transformations. I want to see the HTML generated by Java. Looks like we're pulling into the station...

Reading about JAXP Transformation Packages... I learned that Java 5.0 ships with JAXP 1.3, so I don't need to download anything.

OK, that was a straightforward spike.

import org.w3c.dom.Document;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;


public class TransformSample {
    public static void main(String[] args) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.parse("sample.xml");

        StreamSource stylesource = new StreamSource(new File("sample.xsl"));
        Transformer transformer = TransformerFactory.newInstance().newTransformer(stylesource);

        Source source = new DOMSource(document);
        Result result = new StreamResult(System.out);
        transformer.transform(source, result);
    }
}

[/projects/quotes] permanent link

Quote Manager Project -- Day O

The beginning of a pet project blog...

I love to read. As I read, I am constantly underlining, writing in the margin and transcribing quotes onto scratch paper. When I finish a book, I read through the quotes I've collected. I choose my favorites and transcribe them into my quotes page. I've been doing this since I started learning how to develop software. Actually, I started the habit before I was a programmer, but the web page thing didn't start until after I learned HTML.

My quotes page has grown pretty hefty, and I would like to develop some software to help me manage it.

Last year I read Ron Jeffries' Extreme Programming Adventures in C#. I liked Ron's idea of keeping a public journal as he developed software. I am going to do something similar.

"...the best way to create is by imitation, refinement and combination." --Object-Oriented Software Construction, Second Edition, Bertrand Meyer
Ron's "adventures" were learning a new language (C#) while developing an XML editor. I'm already learning a new language right now (C++), so I think I'll stick with one that I'm familiar with (Java). But I will try out a technology (XSL) that's new to me, though it's not new to many.

Here's the plan:

  • I want to change the quotes page to be sorted by quoter. There should be a list of all the quoters at the top. Their names should link down to their quotes on the page below.
  • I want to use a simple Swing application to manage the quotes.
  • The Swing app will store the quotes in XML. The app will have the ability to transform this XML into HTML via XSLT.
The only time I will work on this project is on the train, going to and from Chicago on my way to work. It's about a 40 minute ride each way.

So where to start, where to start... What aspect of the project do I know the least about? XSL. I'll start there. I'll start with creating a sample XML file. No, wait. Source control first. I just imported what I've written so far into Subversion. OK, now I'm ready to create my XML sample.

<quotes>
	<quote>
		<text>
			We in the software industry are working with a more or less invisible product,
			yet this very invisibility only heightens our need for feedback.
		</text>
		<quoter url="http://www.geraldmweinberg.com/">Gerald Weinberg</quoter>
		<source isbn="0932633447" page="xiii">Project Retrospectives</source>
		<author url="http://www.retrospectives.com/">Norm Kerth</author>
	</quote>
</quotes>
OK, checked that in.

What's next? XSL-ify the XML into something resembling the way I want my quotes page to look. I printed out an XSL tutorial from W3Schools, which should be helpful. Looks like the first thing I need to do is define an XSL style sheet...

<?xml version="1.0" encoding="ISO-8859-1"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
	<html>
	<body>
		<xsl:for-each select="quotes/quote">
			<p><xsl:value-of select="text"/></p>
		</xsl:for-each>
	</body>
	</html>
</xsl:template>

</xsl:stylesheet>
I'll start with that. I need to tell my XML document that it should be rendered with my style sheet. Looks like adding these lines to the top of my XML file should do it...
<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="sample.xsl"?>
Let's see what that looks like in Firefox. Looks pretty boring, but just what I expected. Jerry's quote.

I'm going to add another quote to make sure that <xsl:for-each> is working right. Yep, looks good. Time for a check in.

I'm going to keep building my style sheet until it looks close enough for now.

Stumbling forward, wishing I had web access right now. Looks like my battery is going to run out. I guess this stuff will need to wait until tomorrow...

[/projects/quotes] permanent link


powered by blosxom