You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@cocoon.apache.org by sy...@apache.org on 2004/11/21 23:29:27 UTC

svn commit: r106132 - in cocoon/trunk/src/blocks/forms: java/org/apache/cocoon/forms/formmodel test/org/apache/cocoon/forms/formmodel

Author: sylvain
Date: Sun Nov 21 14:29:26 2004
New Revision: 106132

Added:
   cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/
   cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.java   (contents, props changed)
   cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.model.xml   (contents, props changed)
   cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.xtest
   cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/WidgetTestHelper.java   (contents, props changed)
Modified:
   cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/Field.java
Log:
Fix a bug when Field.setValue() is called, add some tests

Modified: cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/Field.java
Url: http://svn.apache.org/viewcvs/cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/Field.java?view=diff&rev=106132&p1=cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/Field.java&r1=106131&p2=cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/Field.java&r2=106132
==============================================================================
--- cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/Field.java	(original)
+++ cocoon/trunk/src/blocks/forms/java/org/apache/cocoon/forms/formmodel/Field.java	Sun Nov 21 14:29:26 2004
@@ -155,11 +155,27 @@
                            "\" (expected " + getDatatype().getTypeClass() +
                            ", got " + newValue.getClass() + ").");
         }
-        Object oldValue = this.value;
-        boolean changed = ! (oldValue == null ? "" : oldValue).equals(newValue == null ? "" : newValue);
+
+        // Is it a new value?
+        boolean changed;
+        if (this.valueState == VALUE_UNPARSED) {
+            // Current value was not parsed
+            changed = true;
+        } else if (this.value == null) {
+            // Is current value not null?
+            changed = (newValue != null);
+        } else {
+            // Is current value different?
+            changed = !this.value.equals(newValue);
+        }
+
         // Do something only if value is different or null
         // (null allows to reset validation error)
         if (changed || newValue == null) {
+            // Do we need to call listeners? If yes, keep (and parse if needed) old value.
+            boolean callListeners = changed && hasValueChangedListeners();
+            Object oldValue = callListeners ? getValue() : null;
+            
             this.value = newValue;
             this.validationError = null;
             // Force validation, even if set by the application
@@ -169,7 +185,8 @@
             } else {
                 this.enteredValue = null;
             }
-            if (changed && hasValueChangedListeners()) {
+
+            if (callListeners) {
                 getForm().addWidgetEvent(new ValueChangedEvent(this, oldValue, newValue));
             }
         }
@@ -200,7 +217,13 @@
 
         // Only convert if the text value actually changed. Otherwise, keep the old value
         // and/or the old validation error (allows to keep errors when clicking on actions)
-        if (!(newEnteredValue == null ? "" : newEnteredValue).equals((enteredValue == null ? "" : enteredValue))) {
+        boolean changed;
+        if (enteredValue == null) {
+            changed = (newEnteredValue != null);
+        } else {
+            changed = !enteredValue.equals(newEnteredValue);
+        }
+        if (changed) {
             
             // If we have some value-changed listeners, we must make sure the current value has been
             // parsed, to fill the event. Otherwise, we don't need to spend that extra CPU time.

Added: cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.java
Url: http://svn.apache.org/viewcvs/cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.java?view=auto&rev=106132
==============================================================================
--- (empty file)
+++ cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.java	Sun Nov 21 14:29:26 2004
@@ -0,0 +1,227 @@
+/*
+ * Copyright 1999-2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cocoon.forms.formmodel;
+
+import org.apache.cocoon.core.container.ContainerTestCase;
+import org.apache.cocoon.environment.mock.MockRequest;
+import org.apache.cocoon.forms.FormContext;
+import org.apache.cocoon.forms.event.ValueChangedEvent;
+import org.apache.cocoon.forms.event.ValueChangedListener;
+import org.w3c.dom.Document;
+
+/**
+ * Test case for CForm's Field widget
+ *
+ * @version $Id$
+ */
+
+public class FieldTestCase extends ContainerTestCase {
+    
+    public static final String VALUE_PATH = "fi:fragment/fi:field/fi:value";
+    public static final String VALIDATION_PATH = "fi:fragment/fi:field/fi:validation-message";
+    
+    
+    /**
+     * Nominal test where the request data is syntactically correct and validates
+     */
+    public void testValueDoesParseAndValidate() throws Exception {
+        Form form = WidgetTestHelper.loadForm(getManager(), this, "FieldTestCase.model.xml");
+        Field field = (Field)form.getChild("intfield");
+        Action button = (Action)form.getChild("action");
+        MockRequest request;
+        
+        request = new MockRequest();
+        request.addParameter("intfield", "11");
+        request.addParameter("action", "pressed");
+        form.process(new FormContext(request));
+        
+        // No parsing nor validation where performed
+        Document doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathEquals("Displayed value", "11", VALUE_PATH, doc);
+        WidgetTestHelper.assertXPathNotExists("Validation error", VALIDATION_PATH, doc);
+        
+        // Now do some parsing.
+        assertEquals("Field value", new Integer(11), field.getValue());
+        // And still no validation error (do not call getValidationError() as it does validate)
+        doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathNotExists("Validation error", VALIDATION_PATH, doc);
+        
+        // Now validate
+        assertTrue("Field does validate", field.validate());
+        assertNull("getValidationError() null after validation", field.getValidationError());
+        doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathNotExists("Validation error", VALIDATION_PATH, doc);        
+    }
+    
+    /**
+     * Request data is not syntactically correct
+     */
+    public void testValueDoesNotParse() throws Exception {
+        Form form = WidgetTestHelper.loadForm(getManager(), this, "FieldTestCase.model.xml");
+        Field field = (Field)form.getChild("intfield");
+        Action button = (Action)form.getChild("action");
+        MockRequest request;
+        
+        request = new MockRequest();
+        request.addParameter("intfield", "foo");
+        request.addParameter("action", "pressed");
+        form.process(new FormContext(request));
+        
+        // No parsing nor validation where performed
+        Document doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathEquals("Displayed velue", "foo", VALUE_PATH, doc);
+        WidgetTestHelper.assertXPathNotExists("Validation error before parse", VALIDATION_PATH, doc);
+        
+        // Now do some parsing. Will return null as it's not parseable
+        assertNull("Field value", field.getValue());
+        // But still no validation error
+        doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathEquals("Displayed value", "foo", VALUE_PATH, doc);
+        WidgetTestHelper.assertXPathNotExists("Validation error after parse", VALIDATION_PATH, doc);
+        
+        // Now validate
+        assertFalse("Field validation", field.validate());
+        doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathEquals("Displayed velue", "foo", VALUE_PATH, doc);
+        WidgetTestHelper.assertXPathExists("Validation not null after parse", VALIDATION_PATH, doc);
+        assertNotNull("getValidationError() not null after validation", field.getValidationError());
+    }
+    
+    /**
+     * Request data is syntactically correct but doesn't validate
+     */
+    public void testValueDoesNotValidate() throws Exception {
+        Form form = WidgetTestHelper.loadForm(getManager(), this, "FieldTestCase.model.xml");
+        Field field = (Field)form.getChild("intfield");
+        Action button = (Action)form.getChild("action");
+        MockRequest request;
+        
+        request = new MockRequest();
+        request.addParameter("intfield", "1");
+        request.addParameter("action", "pressed");
+        form.process(new FormContext(request));
+        
+        // No parsing nor validation where performed
+        Document doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathEquals("Displayed value", "1", VALUE_PATH, doc);
+        WidgetTestHelper.assertXPathNotExists("Validation error before parse", VALIDATION_PATH, doc);
+        
+        // Now do some parsing. Will return null although syntactically correct as it's invalid
+        assertNull("Field value", field.getValue());
+        // But still no validation error
+        doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathNotExists("Validation error after parse", VALIDATION_PATH, doc);
+        
+        // Now validate
+        assertFalse("Field validation", field.validate());
+        doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathExists("Validation error after validation", VALIDATION_PATH, doc);
+        assertNotNull("getValidationError() not null after validation", field.getValidationError());
+    }
+    
+    /**
+     * Test that a field's value is properly set by a call to setValue("") with an
+     * empty string when the field is in unparsed state (there used to be a bug in
+     * that case)
+     */
+    public void testSetEmptyValueWhenValueChangedOnRequest() throws Exception {
+        Form form = WidgetTestHelper.loadForm(getManager(), this, "FieldTestCase.model.xml");
+        Field field = (Field)form.getChild("stringfield");
+        Action button = (Action)form.getChild("action");
+        MockRequest request;
+        
+        // Set a value in stringfield and submit with an action
+        // (no validation, thus no call to doParse())
+        request = new MockRequest();
+        request.addParameter("stringfield", "bar");
+        request.addParameter("action", "pressed");
+        form.process(new FormContext(request));
+        
+        // Verify submit widget, just to be sure that validation did not occur
+        assertEquals("Form submit widget", button, form.getSubmitWidget());
+        
+        // Set the value to an empty string. In that case, a faulty test made
+        // it actually ignore it when state was VALUE_UNPARSED
+        field.setValue("");
+        
+        // Check value by various means
+        Document doc = WidgetTestHelper.getWidgetFragment(field, null);
+        WidgetTestHelper.assertXPathEquals("Displayed value", "", VALUE_PATH, doc);
+        assertEquals("Datatype string conversion", "", field.getDatatype().convertToString(field.value, null));
+        assertEquals("Field value", "", (String)field.getValue());
+    }
+    
+    /**
+     * Test that the previous field value is correctly passed to event listeners
+     * even if it was not already parsed.
+     */
+    public void testOldValuePresentInEventEvenIfNotParsed() throws Exception {
+        Form form = WidgetTestHelper.loadForm(getManager(), this, "FieldTestCase.model.xml");
+        Field field = (Field)form.getChild("stringfield");
+        Action button = (Action)form.getChild("action");
+        MockRequest request;
+        
+        // Set a value on "stringfield", and submit using an action so that
+        // it stays in unparsed state
+        request = new MockRequest();
+        request.addParameter("stringfield", "foo");
+        request.addParameter("action", "pressed");
+        form.process(new FormContext(request));
+
+        // Now add an event listener that will check old an new value
+        field.addValueChangedListener(new ValueChangedListener (){
+            public void valueChanged(ValueChangedEvent event) {
+                assertEquals("Old value", "foo", (String)event.getOldValue());
+                assertEquals("New value", "bar", (String)event.getNewValue());
+            }
+        });
+        
+        // Change value to "bar", still without explicit validation
+        // That will call the event listener
+        request = new MockRequest();
+        request.addParameter("stringfield", "bar");
+        request.addParameter("button", "pressed");
+        form.process(new FormContext(request));
+    }
+    
+    /**
+     * Request parameters are not read when a field is not in active state
+     */
+    public void testParameterNotReadWhenDisabled() throws Exception {
+        Form form = WidgetTestHelper.loadForm(getManager(), this, "FieldTestCase.model.xml");
+        Field field = (Field)form.getChild("stringfield");
+        MockRequest request;
+
+        // Disable the form
+        form.setState(WidgetState.DISABLED);
+        field.setValue("foo");
+        
+        request = new MockRequest();
+        request.addParameter("stringfield", "bar");
+        form.process(new FormContext(request));
+        
+        // Check that "bar" was not read
+        assertEquals("foo", field.getValue());
+        
+        // Switch back to active and resumbit the same request
+        form.setState(WidgetState.ACTIVE);
+        form.process(new FormContext(request));
+        
+        // Should have changed now
+        assertEquals("bar", field.getValue());
+    }
+}

Added: cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.model.xml
Url: http://svn.apache.org/viewcvs/cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.model.xml?view=auto&rev=106132
==============================================================================
--- (empty file)
+++ cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.model.xml	Sun Nov 21 14:29:26 2004
@@ -0,0 +1,19 @@
+<fd:form xmlns:fd="http://apache.org/cocoon/forms/1.0#definition">
+    <fd:widgets>
+        <fd:field id="stringfield">
+            <fd:datatype base="string"/>
+        </fd:field>
+        
+        <fd:field id="intfield">
+            <fd:datatype base="integer">
+                <fd:convertor type="plain"/>
+            </fd:datatype>
+            <fd:validation>
+                <fd:range min="10"/>
+            </fd:validation>
+        </fd:field>
+        
+        <fd:action id="action" action-command="blah"/>
+        
+    </fd:widgets>
+</fd:form>
\ No newline at end of file

Added: cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.xtest
Url: http://svn.apache.org/viewcvs/cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.xtest?view=auto&rev=106132
==============================================================================
--- (empty file)
+++ cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/FieldTestCase.xtest	Sun Nov 21 14:29:26 2004
@@ -0,0 +1,81 @@
+<?xml version="1.0" ?>
+<!--
+  Copyright 1999-2004 The Apache Software Foundation
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<testcase>
+ <roles>
+
+  <role name="org.apache.cocoon.forms.datatype.DatatypeManager"
+        shorthand="forms-datatype"
+        default-class="org.apache.cocoon.forms.datatype.DefaultDatatypeManager"/>
+
+  <role name="org.apache.cocoon.forms.expression.ExpressionManager"
+    shorthand="forms-expression"
+    default-class="org.apache.cocoon.forms.expression.DefaultExpressionManager"/>
+
+  <role name="org.apache.cocoon.forms.FormManager"
+        shorthand="forms-formmanager"
+        default-class="org.apache.cocoon.forms.DefaultFormManager"/>
+
+  <role name="org.apache.cocoon.forms.CacheManager"
+        shorthand="forms-cachemanager"
+        default-class="org.apache.cocoon.forms.DefaultCacheManager"/>
+
+  <role name="org.apache.cocoon.forms.validation.WidgetValidatorBuilderSelector"
+        shorthand="forms-validators"
+        default-class="org.apache.cocoon.components.ExtendedComponentSelector"/>
+
+  <role name="org.apache.cocoon.forms.event.WidgetListenerBuilderSelector"
+        shorthand="forms-widgetlisteners"
+        default-class="org.apache.cocoon.components.ExtendedComponentSelector"/>
+</roles>
+
+ <components>
+  <forms-datatype logger="forms">
+    <datatypes>
+      <datatype name="string" src="org.apache.cocoon.forms.datatype.typeimpl.StringTypeBuilder">
+        <convertors default="dummy" plain="dummy">
+          <convertor name="dummy" src="org.apache.cocoon.forms.datatype.convertor.DummyStringConvertorBuilder"/>
+        </convertors>
+      </datatype>
+      <datatype name="integer" src="org.apache.cocoon.forms.datatype.typeimpl.IntegerTypeBuilder">
+        <convertors default="formatting" plain="plain">
+          <convertor name="plain" src="org.apache.cocoon.forms.datatype.convertor.PlainIntegerConvertorBuilder"/>
+          <convertor name="formatting" src="org.apache.cocoon.forms.datatype.convertor.FormattingIntegerConvertorBuilder"/>
+        </convertors>
+      </datatype>  
+    </datatypes>
+    <!--validation-rules>
+      <validation-rule name="range" src="org.apache.cocoon.forms.datatype.validationruleimpl.RangeValidationRuleBuilder"/>
+    </validation-rules-->
+  </forms-datatype>
+ 	
+  <forms-validators>
+    <validator name="range" class="org.apache.cocoon.forms.validation.impl.RangeValidatorBuilder"/>
+  </forms-validators>
+  
+  <forms-formmanager>
+    <widgets>
+      <widget name="form" src="org.apache.cocoon.forms.formmodel.FormDefinitionBuilder"/>
+      <widget name="field" src="org.apache.cocoon.forms.formmodel.FieldDefinitionBuilder"/>
+      <widget name="action" src="org.apache.cocoon.forms.formmodel.ActionDefinitionBuilder"/>
+    </widgets>
+  </forms-formmanager>
+  
+  <forms-expression logger="forms.expression"/>
+
+ </components>
+
+</testcase>

Added: cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/WidgetTestHelper.java
Url: http://svn.apache.org/viewcvs/cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/WidgetTestHelper.java?view=auto&rev=106132
==============================================================================
--- (empty file)
+++ cocoon/trunk/src/blocks/forms/test/org/apache/cocoon/forms/formmodel/WidgetTestHelper.java	Sun Nov 21 14:29:26 2004
@@ -0,0 +1,140 @@
+/*
+ * Copyright 1999-2004 The Apache Software Foundation.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cocoon.forms.formmodel;
+
+import java.util.Locale;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import junit.framework.Assert;
+
+import org.apache.avalon.framework.service.ServiceManager;
+import org.apache.cocoon.forms.Constants;
+import org.apache.cocoon.forms.FormManager;
+import org.apache.cocoon.xml.AttributesImpl;
+import org.apache.cocoon.xml.dom.DOMBuilder;
+import org.apache.commons.jxpath.JXPathContext;
+import org.apache.commons.jxpath.Pointer;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.NamespaceSupport;
+
+/**
+ * Helper class to build Widget test cases.
+ * 
+ * @version $Id$
+ */
+public class WidgetTestHelper {
+    
+    // Private constructor as we only have static methods
+    private WidgetTestHelper() {}
+
+    /**
+     * Get the result of a widget's generateSaxFragment() method as a Document.
+     * <p>
+     * The widget's fragment is encapsulated in a root &lt;fi:fragment&gt; element,
+     * since there's no guarantee that a widget outputs a single top-level element
+     * (there can be several elements, or even none if the widget is invisible)
+     * 
+     * @param widget the widget of which we want the fragment
+     * @param locale the locale to be used to generate the fragment
+     * @return the document containing the fragment
+     */
+    public static Document getWidgetFragment(Widget widget, Locale locale) throws SAXException {
+        
+        DOMBuilder domBuilder = new DOMBuilder();
+        // Start document and "fi:fragment" root element
+        domBuilder.startDocument();
+        domBuilder.startPrefixMapping(Constants.INSTANCE_PREFIX, Constants.INSTANCE_NS);
+        // FIXME: why simply declaring the prefix isn't enough?
+        AttributesImpl attr = new AttributesImpl();
+        attr.addCDATAAttribute(NamespaceSupport.XMLNS, "fi:", "xmlns:fi", Constants.INSTANCE_NS);
+        domBuilder.startElement(Constants.INSTANCE_NS, "fragment", Constants.INSTANCE_PREFIX_COLON + "fragment", attr);
+        
+        widget.generateSaxFragment(domBuilder, locale);
+        
+        // End "fi:fragment" element and document
+        domBuilder.endElement(Constants.INSTANCE_NS, "fragment", Constants.INSTANCE_PREFIX_COLON + "fragment");
+        domBuilder.endPrefixMapping(Constants.INSTANCE_PREFIX);
+        domBuilder.endDocument();
+        
+        // Return the document
+        return domBuilder.getDocument();
+    }
+    
+    public static void assertXPathEquals(String expected, String xpath, Document doc) {
+        // use xpath as the message
+        assertXPathEquals(xpath, expected, xpath, doc);
+    }
+    
+    public static void assertXPathEquals(String message, String expected, String xpath, Document doc) {
+        JXPathContext ctx = JXPathContext.newContext(doc);
+        ctx.setLenient(true);
+        Assert.assertEquals(message, expected, ctx.getValue(xpath));
+    }
+    
+    public static void assertXPathExists(String xpath, Document doc) {
+        // use xpath as message
+        assertXPathExists(xpath, xpath, doc);
+    }
+    
+    public static void assertXPathExists(String message, String xpath, Document doc) {
+        JXPathContext ctx = JXPathContext.newContext(doc);
+        ctx.setLenient(true);
+        Pointer pointer = ctx.getPointer(xpath);
+        Assert.assertNotNull(message, pointer.getNode());
+    }
+    
+    public static void assertXPathNotExists(String xpath, Document doc) {
+        // use xpath as message
+        assertXPathNotExists(xpath, xpath, doc);
+    }
+    
+    public static void assertXPathNotExists(String message, String xpath, Document doc) {
+        JXPathContext ctx = JXPathContext.newContext(doc);
+        ctx.setLenient(true);
+        Pointer pointer = ctx.getPointer(xpath);
+        Assert.assertNull(message, pointer.getNode());
+    }
+    
+    /**
+     * Load a Form whose definition relative to a given object (typically, the TestCase class).
+     * 
+     * @param manager the ServiceManager that will be used to create the form
+     * @param obj the object relative to which the resource will be read
+     * @param resource the relative resource name for the form definition
+     * @return the Form
+     * @throws Exception
+     */
+    public static Form loadForm(ServiceManager manager, Object obj, String resource) throws Exception {
+        // Load the document
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        // Grmbl... why isn't this true by default?
+        factory.setNamespaceAware(true);
+        DocumentBuilder parser = factory.newDocumentBuilder();  
+        Document doc = parser.parse(obj.getClass().getResource(resource).toExternalForm());
+
+        // Create the form
+        FormManager formManager = (FormManager)manager.lookup(FormManager.ROLE);
+        try {
+            return formManager.createForm(doc.getDocumentElement());
+        } finally {
+            manager.release(formManager);
+        }
+    }
+}